A Quint model of nisse-dev's control loop, used to search for counterexamples. No behaviour changes: the fixes follow as separate PRs.
The model (specs/orchestrator.qnt, notes in specs/README.md)
Coverscontrol.py's states, the gate, pending questions, Gitea comment cursors, publish intents and the worker queue. Each pure function is named after the method it mirrors.
Interleaves freely: operator messages (every command and free text), comments on the plan issue and the PR, planner/executor/publisher outcomes including exceptions, and a crash at any point. Publishing is split into its four steps so a crash can land between them.
Eight properties. Each message records the plan the operator had seen when sending it, so the model can tell when an approval lands on the wrong plan.
Results: seven counterexamples. Each is written out as a short trace in specs/orchestrator_scenarios.qnt and reproduced against the real code in nisse-dev/tests/test_quint_counterexamples.py, marked xfail(strict=True) until fixed. Each test was checked to fail on its own assertion, not a harness error.
#
Trace
Real behaviour
1
"approve" sent twice, both queued
the second approves the milestone plan, never shown
2
"approve rev-1" meant for the architecture
approves milestone-1 rev-1: tags carry no gate name
3
approve in Matrix and on the Gitea issue
the queued Gitea approval approves the next plan
4
"approve" plus an afterthought
the note rejects the milestone plan, which is re-planned
5
crash after the PR opens, before PR_OPEN
restart → continue → start publishes again; Gitea refuses; no way back to PR_OPEN
6
crash while answering a review comment
the comment is never answered, and nobody is told
7
project complete
the plan issue is left open (abandon closes it)
What held:no_stranding, await_input_has_a_question and execution_needs_approval over 200,000 random traces of up to 40 steps each, and under Apalache's exhaustive check to depth 8. That depth is shallow, but Apalache does find counterexample 1 within it, so the search is exhaustive at that depth.
Limits: the model is hand-written from the code. It finds flaws the design and code share, not places where the code strays from the design; that's why each result is also reproduced against the real code. CI doesn't run Quint: it needs Node for the simulator and Java 21 for Apalache. specs/README.md has the commands.
Proposed fixes, each its own PR with a plan first:
1–4: bind each approval or piece of feedback to the plan current when the message arrived (for Gitea, the latest plan comment before it, since IDs only grow), with gate-qualified revision tags. This mostly implements A4's "only when unambiguous".
5: make publishing adopt an existing PR for the branch instead of failing.
6: record the review comment being worked on, and redeliver it with a notice after a restart.
7: close the plan issue with a note on project complete.
When each fix lands, its scenario's expect flips to the property holding and its xfail comes off.
A [Quint](https://quint-lang.org) model of `nisse-dev`'s control loop, used to search for counterexamples. **No behaviour changes**: the fixes follow as separate PRs.
**The model** (`specs/orchestrator.qnt`, notes in `specs/README.md`)
- **Covers** `control.py`'s states, the gate, pending questions, Gitea comment cursors, publish intents and the worker queue. Each pure function is named after the method it mirrors.
- **Interleaves freely:** operator messages (every command and free text), comments on the plan issue and the PR, planner/executor/publisher outcomes including exceptions, and **a crash at any point**. Publishing is split into its four steps so a crash can land between them.
- **Eight properties.** Each message records the plan the operator had seen when sending it, so the model can tell when an approval lands on the wrong plan.
**Results: seven counterexamples.** Each is written out as a short trace in `specs/orchestrator_scenarios.qnt` and **reproduced against the real code** in `nisse-dev/tests/test_quint_counterexamples.py`, marked `xfail(strict=True)` until fixed. Each test was checked to fail on its own assertion, not a harness error.
| # | Trace | Real behaviour |
|---|---|---|
| 1 | "approve" sent twice, both queued | the second approves the milestone plan, never shown |
| 2 | "approve rev-1" meant for the architecture | approves milestone-1 rev-1: tags carry no gate name |
| 3 | approve in Matrix and on the Gitea issue | the queued Gitea approval approves the next plan |
| 4 | "approve" plus an afterthought | the note rejects the milestone plan, which is re-planned |
| 5 | crash after the PR opens, before `PR_OPEN` | restart → `continue` → `start` publishes again; Gitea refuses; no way back to `PR_OPEN` |
| 6 | crash while answering a review comment | the comment is never answered, and nobody is told |
| 7 | `project complete` | the plan issue is left open (`abandon` closes it) |
**What held:** `no_stranding`, `await_input_has_a_question` and `execution_needs_approval` over 200,000 random traces of up to 40 steps each, and under Apalache's exhaustive check to depth 8. That depth is shallow, but Apalache does find counterexample 1 within it, so the search is exhaustive at that depth.
**Limits:** the model is hand-written from the code. It finds flaws the design and code share, not places where the code strays from the design; that's why each result is also reproduced against the real code. CI doesn't run Quint: it needs Node for the simulator and Java 21 for Apalache. `specs/README.md` has the commands.
**Proposed fixes, each its own PR with a plan first:**
- **1–4:** bind each approval or piece of feedback to the plan current when the message arrived (for Gitea, the latest plan comment before it, since IDs only grow), with gate-qualified revision tags. This mostly implements A4's "only when unambiguous".
- **5:** make publishing adopt an existing PR for the branch instead of failing.
- **6:** record the review comment being worked on, and redeliver it with a notice after a restart.
- **7:** close the plan issue with a note on `project complete`.
When each fix lands, its scenario's `expect` flips to the property holding and its `xfail` comes off.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
specs/orchestrator.qnt models control.py: states, gate, pending
questions, comment cursors, publish intents and the worker queue, with
operator messages, Gitea comments, work outcomes (exceptions included)
and a crash at any point interleaving freely. Eight properties.
Seven counterexamples, each written out as a trace in
specs/orchestrator_scenarios.qnt and reproduced against the real code in
nisse-dev/tests/test_quint_counterexamples.py (xfail, strict, until
fixed):
1-4 an approval or feedback lands on a plan the operator had not seen:
a double 'approve', 'approve rev-1' meant for the architecture,
approving on both channels, and 'approve' plus an afterthought;
5 a crash after the PR opens but before PR_OPEN publishes again and
cannot get back to PR_OPEN;
6 a crash while answering a review comment loses it silently;
7 'project complete' leaves the plan issue open.
no_stranding, await_input_has_a_question and execution_needs_approval
held over 200,000 random traces of 40 steps, and under Apalache's
exhaustive check to depth 8, which does find counterexample 1 at that
depth. No behaviour changes here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A Quint model of
nisse-dev's control loop, used to search for counterexamples. No behaviour changes: the fixes follow as separate PRs.The model (
specs/orchestrator.qnt, notes inspecs/README.md)control.py's states, the gate, pending questions, Gitea comment cursors, publish intents and the worker queue. Each pure function is named after the method it mirrors.Results: seven counterexamples. Each is written out as a short trace in
specs/orchestrator_scenarios.qntand reproduced against the real code innisse-dev/tests/test_quint_counterexamples.py, markedxfail(strict=True)until fixed. Each test was checked to fail on its own assertion, not a harness error.PR_OPENcontinue→startpublishes again; Gitea refuses; no way back toPR_OPENproject completeabandoncloses it)What held:
no_stranding,await_input_has_a_questionandexecution_needs_approvalover 200,000 random traces of up to 40 steps each, and under Apalache's exhaustive check to depth 8. That depth is shallow, but Apalache does find counterexample 1 within it, so the search is exhaustive at that depth.Limits: the model is hand-written from the code. It finds flaws the design and code share, not places where the code strays from the design; that's why each result is also reproduced against the real code. CI doesn't run Quint: it needs Node for the simulator and Java 21 for Apalache.
specs/README.mdhas the commands.Proposed fixes, each its own PR with a plan first:
project complete.When each fix lands, its scenario's
expectflips to the property holding and itsxfailcomes off.🤖 Generated with Claude Code
https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
specs/orchestrator.qnt models control.py: states, gate, pending questions, comment cursors, publish intents and the worker queue, with operator messages, Gitea comments, work outcomes (exceptions included) and a crash at any point interleaving freely. Eight properties. Seven counterexamples, each written out as a trace in specs/orchestrator_scenarios.qnt and reproduced against the real code in nisse-dev/tests/test_quint_counterexamples.py (xfail, strict, until fixed): 1-4 an approval or feedback lands on a plan the operator had not seen: a double 'approve', 'approve rev-1' meant for the architecture, approving on both channels, and 'approve' plus an afterthought; 5 a crash after the PR opens but before PR_OPEN publishes again and cannot get back to PR_OPEN; 6 a crash while answering a review comment loses it silently; 7 'project complete' leaves the plan issue open. no_stranding, await_input_has_a_question and execution_needs_approval held over 200,000 random traces of 40 steps, and under Apalache's exhaustive check to depth 8, which does find counterexample 1 at that depth. No behaviour changes here. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE