M3.3: the trust dial (the agent merges approved PRs)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m29s
CI / e2e (pull_request) Successful in 11m23s

A project's trust.merge is operator (the default) or approved (L3, D5,
#65). Set to approved, the harness, never the model, merges a PR in the
PR watch once an approving review from a gitea.operators account is on
its current commit, its checks pass (none at all counts; pending
waits) and it's mergeable; the item then goes on as after any merge.
Each reason to hold off (failed checks, conflicts, a refused merge) is
said once per commit.

Gitea enforces it: protection requires approvals from an operators-only
whitelist and dismisses stale ones. At start, for each approved
project, the agent checks by behaviour that its own unapproved merge is
refused, with a throwaway branch and PR made through the API (never the
workspace); if it isn't refused, the operator merges until a restart.

- gitea: approvals, combined check status, a PR's head commit and
  mergeability, API-made branches and their deletion.
- Test and bring-up repositories get the protection.
- Tests: the decision (unit), the hard wall on a real Gitea
  (test_trust_live), an e2e test where the operator only approves, and
  a walk property, merges_only_approved, checked against a mutant that
  ignores which commit was approved.

Closes #65

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-08 13:48:16 +00:00
1 parent 21cbf1682c
commit ab469b60bb
17 files changed
+733 -11

No files matched your search

+42
View File
@@ -243,8 +243,33 @@ class Walk:
self.steps: list[str] = []
self.shown_at_go: list[str] = [] # item:revision on screen at each go
self.now = 1000.0
# The trust dial (M3.3): half the walks have the agent merge approved
# PRs, behind protection that refuses anything else.
self.merge = "approved" if seed % 2 == 0 else "operator"
self.agent_merges: list[tuple[int, bool]] = [] # (PR, approved at head)
if self.merge == "approved":
for w in worlds.values():
self._watch_merges(w["forge"])
self.agent, self.transport = self._agent()
def _watch_merges(self, forge: Forge) -> None:
"""Protected, and each merge the agent asks for recorded with
whether an operator had approved the PR as it was."""
forge.protect()
merge = forge.merge_pull_request
def recorded(repo: str, index: int) -> None:
if not forge.prs[index]["head"].startswith("nisse-check-"):
head = forge.head_sha(index)
approved = any(
a.author == "don" and a.commit == head and not a.stale
for a in forge.approved.get(index, [])
)
self.agent_merges.append((index, approved))
merge(repo, index)
forge.merge_pull_request = recorded # type: ignore[method-assign]
def reads_yes(self, proposal: str, reply: str) -> bool:
"""The scripted go-ahead reader: a plain yes usually reads as one,
and nothing else ever does."""
@@ -270,6 +295,7 @@ class Walk:
repo=w["repo_name"],
workspace=Workspace(w["host"], w["repo"].root, timeout_s=30),
milestones=w["milestones"],
merge=self.merge,
)
for name, w in self.worlds.items()
],
@@ -342,6 +368,8 @@ class Walk:
weights.update(
{"comment": 4, "merge": 2, "close-pr": 1, "comment-merge": 2}
)
if self.merge == "approved":
weights.update({"approve": 4})
action = self.rng.choices(list(weights), list(weights.values()))[0]
self.steps.append(f"{action}{self.name(item)}" if item else action)
if action == "intake":
@@ -455,6 +483,16 @@ class Walk:
self.agent.watch()
forge.merge(loop.pr)
self.agent._schedule()
elif action == "approve" and loop and forge.prs[loop.pr]["state"] == "open":
# An operator's approval, or someone else's; of the PR as it is,
# or of an earlier commit.
forge.approve(
loop.pr,
by=self.rng.choice(["don", "don", "mallory"]),
sha=self.rng.choice(["", "0" * 40]),
)
self.agent.watch()
self.agent._schedule()
elif action == "merge" and loop and forge.prs[loop.pr]["state"] == "open":
forge.merge(loop.pr)
self.agent.watch()
@@ -497,6 +535,10 @@ class Walk:
def check(self) -> None:
where = f"seed {self.seed}, steps {self.steps}"
# D5: the agent asks Gitea to merge only what an operator approved
# as it is (Gitea would refuse the rest; the harness mustn't ask).
unapproved = [pr for pr, approved in self.agent_merges if not approved]
assert not unapproved, f"merges_only_approved: {unapproved}: {where}"
self._dangerous_only_when_allowed(where)
store = self.world["store"]
items = self.items()