Bind approvals and feedback to the revision they were about
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s

Fixes counterexamples 1-4 from specs/orchestrator.qnt: a repeated
'approve', 'approve rev-1' meant for the architecture, approving on both
channels, and 'approve' plus an afterthought all landed on the next
plan, one the operator had not seen.

- Revisions are numbered across the work item (A4 already required
  monotonic tags; the code numbered per gate), so rev-N names one plan.
- Each reply is stamped on arrival with the revision that was up: in
  Matrix the one whose approval request had been sent (a plain value
  the transport thread reads, restored on boot), in Gitea the latest
  plan comment older than the reply (board.posts).
- Gate.evaluate applies a reply that names no revision only if its stamp
  is the current revision. Otherwise an approval is not applied and
  feedback rejects nothing (it is still in the transcript), and the
  agent says which revision is up. 'approve rev-N' binds by its number.
- Spec A4 states both rules. The model applies them; scenarios 1-4 now
  expect the properties to hold, and both held over 200,000 random
  traces and Apalache's exhaustive check to depth 8.

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-09-29 19:13:20 +00:00
1 parent 6c83839f4d
commit 97863c1939
10 files changed
+324 -44

No files matched your search

+15 -4
View File
@@ -322,13 +322,24 @@ Requirements:
against a per-channel operator allowlist (Matrix MXID, Gitea username, Vikunja
username).
- **Approvals bind to a plan revision, never to "the current gate."** Every submitted
plan revision carries a monotonically-increasing tag (`rev-N`). An approval naming
a stale revision is rejected with a notice, never applied to a newer one — this is
what makes multi-channel approval race-free.
plan revision carries a monotonically-increasing tag (`rev-N`), numbered across
the whole work item rather than per gate, so a tag names exactly one plan. An
approval naming a stale revision is rejected with a notice, never applied to a
newer one — this is what makes multi-channel approval race-free.
- **A reply that names no revision is about the one up when it arrived.** A bare
`approve`, or any feedback, is stamped on arrival with the revision last put up
for sign-off: in Matrix, the one whose approval request had been sent; in Gitea,
the latest plan comment older than the reply. If that is not the current
revision the reply is ambiguous: an approval is not applied, feedback is recorded
in the transcript but rejects nothing, and the agent says which revision is up.
So a repeated `approve`, an approval given on both channels, or an afterthought
sent while one plan was up can never land on the next. (Found by the Quint model,
`specs/orchestrator.qnt`.)
- **One consistent approval UI across all channels.** Approval is always the typed
word `approve`, attached to a specific revision: in Matrix, a reply to the plan
message (the reply relation carries the binding; a bare `approve` in the room binds
to the latest revision only when unambiguous, otherwise the agent asks); in Gitea
to the latest revision only when unambiguous, in the sense above, otherwise the
agent asks; reply relations are not implemented yet); in Gitea
and Vikunja, a comment `approve rev-N`, with the tag printed in the plan comment
for copy-paste. Any other reply/comment is `reject <feedback>`. Reactions/emoji are
never an approval path. Approvals are consumed at first sight; later edits to an