Skip to content

merge: adding a gate is the supervisor's call; changing one stays the owner's - #217

Merged
hhff merged 3 commits into
mainfrom
gate-add-supervisor
Nov 13, 2026
Merged

hhff merged 3 commits into
mainfrom
gate-add-supervisor

Conversation

@hhff

@hhff hhff commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

What

merge --chore lands a supervisor's chore PR that only adds gates to settings.gates without an owner question. A plan PR is a planner's, so a gate added there is still the owner's, and ticket PRs can't change settings at all. It's the supervisor's call, and merge logs it as its own gate-add act, a new constitutional class. Changing, loosening, reordering, renaming or removing a gate stays the owner's gate-change.

A PR counts as "only adds" when all of these hold. They're read from the base and head trees, never the log:

  • Every gate main has is still there, its JSON text unchanged (field order included), in main's order. ownerSettingsChanged now compares gates as written, not canonically, so a pure reorder of main's gates is a gate-change too (before this PR it slipped through as "unchanged").
  • No gates key on main counts as none, so the first gate is an addition.
  • The new entries come after main's.
  • No key is repeated under gates in the head's settings block, because JSON.parse silently keeps the last one, so a second "lint" could replace main's without anyone seeing it.
  • Each new gate has a lowercase name with a letter first, so a numeric name can't make JavaScript run it ahead of main's gates. It uses only the fields the kit knows (cmd, paths, junit, mode). Its cmd is one line of plain text, its junit is a path inside the checkout, and its mode is enforced or ratchet. A field the kit adds later stays the owner's to set.

Merge then runs each new gate at the PR head in a seeded detached worktree, with the same gateEnv every gate gets. If one fails, merge refuses unless it's in ratchet mode, because a broken gate would block every later merge and only the owner could remove it. Once the PR lands, merge logs gate-add (added gate shellcheck ("shellcheck …") to settings.gates by merging …) with refs to the target and the merge act.

When gates still needs the owner, the refusal says why: changes owner settings: gates (gate-change) (gates: it changes lint; only adding a gate is the supervisor's).

Docs: constitution.md (the decision table, the §2 note on constitutional classes, the §4 one-way-door bullet), supervisor.md ("Development-only tools are yours") and quality.md (the owner-settings paragraph) say that adding a gate that runs only dev-only tools is the supervisor's call, recorded as its own act, and that changing or removing a gate stays the owner's gate-change.

worktree_setup and worktree_files stay owner-only

Neither one gets an additive exception. Both act on the checkout before every gate runs. An appended worktree_setup step runs ahead of main's gates in the same worktree and can change what they see: rewrite files, shadow a tool on PATH, poison DEVTEAM_CACHE. An added worktree_files entry copies a file from the main checkout over the gate's checkout, or brings a production key into every worktree (the existing production-key-in-worktrees finding). So even a pure addition to either can change what an existing gate sees. A linter gate rarely needs them, since the repo's own lockfile and the existing setup install the tool.

Abuse cases

  • A gate that does more than a dev check. Nothing can check from the command that a gate runs only a dev tool. A gate's command runs with the supervisor's own environment and can reach the main checkout (DEVTEAM_HOME) and the shared cache, just as every existing gate's command and test code can. So it gives the supervisor nothing it can't already do in its own shell. That's why the exemption is limited to the supervisor's own chore PR and logged as an act the owner reviews and can correct (not reversible by the supervisor). The docs say this plainly rather than claiming the code enforces "dev-only". A new gate gets nothing existing gates don't: the same gateEnv (the supervisor's environment plus DEVTEAM_MAIN/HOME/CACHE), the same detached worktree, the same sh -c. Its shape is limited to the known fields, so it can't carry an env, a timeout or a future field. Its junit can't point outside the checkout, and its command is one plain line. worktree_files (the way a production key reaches a checkout) stays owner-only.
  • Shadowing or reordering. Main's names must come first, in main's order. New names can't be integer-like, a repeated key under gates is refused, and existing entries must be byte-identical. Gates already run in a fixed order and none can skip another (each failure is collected, none stops the loop), and a new gate runs after all of main's.
  • A renamed gate dressed as new. A rename removes a name, so it's refused.

Not covered (follow-ups)

  • A repo link's links.<id>.gates keeps the owner's per-entry link set --approved flow. Extending the additive rule to it needs the entry-hash acts to learn about it.
  • Removing a gate the supervisor added stays the owner's gate-change, as asked. The run at the head is there so a broken one doesn't land.

Why

The owner, 2026-11-13: "yes, I rule that dev-only tools like linters don't need my approval. I've said this over and over again." v0.44.2 (#198) put that ruling in constitution.md §4 and supervisor.md, but merge still refused any settings.gates change without a gate-change answer, a new shellcheck gate included. That left the supervisor two choices: ask the owner against the ruling, or leave the check off. Adding a check only makes merges stricter, so it's safe. Removing, loosening or editing a gate is how a protection gets switched off, so that stays the owner's.

Tested

  • test/gate-add.test.mjs (new, Node 24.14.1, 6/6):
    • An added gate merges with no question, runs at the head, logs gate-add with refs to the merge act, and lands on main after main's gates.
    • Refused: an edited gate, a loosened one (ratchet), a removed one, a renamed one, an add beside an edit, a reorder, and a field reorder (not byte-identical).
    • Refused: an added gate whose name repeats a key under gates, and a repeated top-level gates.
    • Refused: a numeric or uppercase name, an unknown field (env), a junit of ../.., a multi-line cmd, and a failing new gate. A failing ratchet gate passes.
    • A real act --class gate-add plus hand-written gate-add and gate-change act lines don't let an edit through, and an addition needs no line at all.
    • worktree_setup and worktree_files changes beside an added gate are still refused.
    • Also refused: a pure reorder, a field reorder with nothing else, and a key repeated inside a gate (gates.test.cmd). The first gate on a main with no gates key lands.
  • test/kit.test.mjs (Node 24.14.1): 82/82. merge-admin, components and repo-links tests: green.
  • Not covered by a fixture: a plan PR that adds a gate (a one-line chore ? guard).

Fable adversarial review, and what changed

  • HIGH, a gate is arbitrary shell run as the supervisor. Limited to the supervisor's own chore PR, and the docs and code comments now say the command is the supervisor's judgement, reviewed through the act. I didn't sandbox gates, since that would change what existing gates get.
  • MEDIUM, a pure reorder merged silently. Fixed: gates now compare order-sensitively, with tests.
  • MEDIUM, plan PRs qualified. Fixed: chores only.
  • MEDIUM, docs. supervisor.md's owner-settings line names the exception. "byte-identical" became "identical as JSON, field order included". The constitution no longer claims the code checks "dev-only".
  • LOW, the first gate was the owner's. Fixed. Ratchet pre-run failures are marked preexisting. The gate-add act is reversible: false. The guard is now added.length.
  • LOW, left as is: a new failing ratchet gate passes forever as pre-existing (that's ratchet mode's design), and an --approved passed beside an add-only PR is consumed (existing behaviour).

Migration

None. Every gate change that needed the owner before still does, except a pure addition. Existing projects pick it up with the release.

🤖 Generated with Claude Code

hhff and others added 3 commits November 13, 2026 15:40
… owner's

The owner ruled on 2026-11-13 that dev-only tools like linters don't need
their approval, but merge still refused any change to settings.gates without
an answered gate-change question, even a new shellcheck gate. So the
supervisor had to ask against the ruling or leave the check off.

merge now lands a PR that only adds gates without a question: new entries
after main's, every gate main has byte-identical and in order, none removed or
renamed, no key repeated under gates, each new one a lowercase name with only
cmd, paths, junit or mode. The new gate runs at the PR head first (a failing
one is refused unless in ratchet mode, since removing it would need the
owner), and the merge logs it as its own gate-add act, a new constitutional
class. Everything else that touches gates, and any worktree_setup or
worktree_files change, stays gate-change. The check reads the trees, so no log
line changes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e owner after it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t gate counts

Fable's review of #217:
- A plan PR is a planner's, so only the supervisor's own chore adds a gate
  without the owner; a gate's command runs with the supervisor's environment,
  so that is the line, and the docs say nothing checks "dev-only" for it.
- A pure reorder of main's gates compared equal canonically and merged with no
  owner and no act; gates now compare as written, so it is gate-change.
- No gates key on main counts as none, so the first gate is an addition.
- The gate-add act is not reversible by the supervisor (removal is the
  owner's); a failing ratchet pre-run is marked preexisting.
- supervisor.md's owner-settings line names the exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hhff

hhff commented Nov 13, 2026

Copy link
Copy Markdown
Member Author

Reviewed adversarially at 4bd0f13, looking for any way the supervisor could widen its own authority:

  • gateAdditions reads only the base and head trees. A hand-written gate-add or gate-change line changes nothing, and the test covers that.
  • An absent gates is {} (REPO_OWN_DEFAULTS), so a first gate replaces no implicit default.
  • New gates run after main's gates have already read their junit, so a new gate can't change an existing gate's result in the same run. The shared cache it can reach is already reachable from test code, as the PR body says.
  • Repeated keys are caught at both gates and gates.<name>. Names are restricted, so none can be integer-like or __proto__.
  • Only chores qualify, and worktree_setup and worktree_files stay owner-only.
  • One wording nit: the comment on the head run says "on every kind of PR", but only chores reach it. Harmless.

CI passes 9 of 9, after a rerun of a flaky job. Merging.

@hhff
hhff merged commit 8f54aa8 into main Nov 13, 2026
16 of 17 checks passed
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.

1 participant