The gate holds no merge for a check: the hold, the turns, their stalled line and tools, and check-gates-merge go; the merge judges the operator's Approve alone (milestone 47/G, W11.14)

ADR 0661: the check gates the ask, in one place (askWaits); once the Approve covers
the heads (approvedWaits) no verdict, carry, composed check or required status is
judged, an Approve given before a trunk move merges onto the new trunk, and a
failure after the Approve is said once on the pull request and as a stalled line
naming the merge it follows. A check failing before the answer withdraws the ask.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

(Joined from 43696644.)
Builds: design 47 G §278, test 52 (GATE10)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:43:01 +01:00
parent ee9c389888
commit 6ac5c934d2
29 changed files with 362 additions and 1183 deletions
@@ -36,15 +36,6 @@ func TestTheBoundsAreSettingsWithTheirDefaults(t *testing.T) {
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
t.Fatalf("empty file: floor %s, author %s", h.refusalFloor(), h.authorBound())
}
if !h.checkGates() {
t.Fatalf("check-gates-merge unset reads false; the default is true")
}
if s, err = read(`{"check-gates-merge": false}`); err != nil || s.CheckGatesMerge == nil || *s.CheckGatesMerge || h.checkGates() {
t.Fatalf("check-gates-merge false: %+v %v, gates %v", s, err, h.checkGates())
}
if _, err := read(`{"check-gates-merge": "no"}`); err == nil {
t.Fatalf("a check-gates-merge that is not true or false was taken")
}
if _, err := read(`{"author-new-head-minutes": 0}`); err == nil {
t.Fatalf("a bound of zero minutes was taken")
}
@@ -139,9 +130,9 @@ func TestAnyOtherRefusalIsStalledAtOnce(t *testing.T) {
}
}
// rejectedAfterItsApprove is a delivery approved and then rejected: its check failed after the ask (as in events_test.go),
// rejectedBeforeItsAnswer is a delivery approved and then rejected: its check failed after the ask (as in events_test.go),
// its pull request's description naming who pushed it when pushedBy is given.
func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
func rejectedBeforeItsAnswer(t *testing.T, pushedBy string) (*world, string) {
t.Helper()
w := newWorld(t)
w.op.auto = false
@@ -163,7 +154,11 @@ func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
d.Check = nil
w.h.mu.Unlock()
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "fail", "t1t1t1t1t1t1"))
approve(w)
w.settleAll()
// A check that fails before the operator answers withdraws the ask (novox/hq ADR 0661 §5, ADR 0441 §1).
if open := w.op.open(); len(open) != 0 {
t.Fatalf("the ask stands after its check failed: %v", open)
}
w.h.React()
if w.state(id) != Rejected {
t.Fatalf("not rejected: %s", w.state(id))
@@ -175,7 +170,7 @@ func rejectedAfterItsApprove(t *testing.T, pushedBy string) (*world, string) {
// with no new head, in words naming its author (its branch, and the session that pushed it) and needing a new head —
// never "stop it", never "the operator's".
func TestARejectedDeliveryIsItsAuthorsForTwoHours(t *testing.T) {
w, id := rejectedAfterItsApprove(t, "quiet-deliveries@shanks")
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries@shanks")
said := false
for _, c := range w.forge.comments {
said = said || strings.Contains(c, "not merged")
@@ -208,7 +203,7 @@ func TestARejectedDeliveryIsItsAuthorsForTwoHours(t *testing.T) {
// ADR 0505 §5: with no Pushed-by line, the branch alone names its author.
func TestARejectedDeliveryWithNoPushedByLineIsNamedByItsBranch(t *testing.T) {
w, id := rejectedAfterItsApprove(t, "")
w, id := rejectedBeforeItsAnswer(t, "")
w.later(2*time.Hour + time.Minute)
if got := linesFor(w, id); len(got) != 1 || got[0].Author != "the author of feat/x" {
t.Fatalf("not named by its branch: %+v", got)
@@ -217,7 +212,7 @@ func TestARejectedDeliveryWithNoPushedByLineIsNamedByItsBranch(t *testing.T) {
// ADR 0505 §5: a new head within the two hours ends it: the rejected head is never the operator's.
func TestANewHeadWithinTwoHoursKeepsTheRejectedHeadFromTheOperator(t *testing.T) {
w, id := rejectedAfterItsApprove(t, "quiet-deliveries@shanks")
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries@shanks")
w.later(time.Hour)
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
@@ -1,244 +0,0 @@
package main
import (
"fmt"
"strings"
"sync"
"testing"
"time"
)
// One check per delivery (novox/hq ADR 0548), the gate's part: nothing merges into a repository while a check of other heads
// judges it (§2); an approved merge holds a turn on its repositories (§3); a held merge past its bound is a stalled line, read
// and handed by two tools (§4); an approved delivery whose trunk moved merges on its carried verdict with no second ask (§8).
const otherHead = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
// approvedLone is a lone delivery into oneRepo whose check passed, approved by the operator (the world approves at once).
func approvedLone(w *world) string {
w.t.Helper()
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
return IDOf(oneRepo, oneHead)
}
// memTurns is the bucket turns in memory; it logs each write with the merges the fake bridge had made by then.
type memTurns struct {
mu sync.Mutex
m map[string]Turn
log []string
merges func() int
}
func newMemTurns(w *world) *memTurns {
return &memTurns{m: map[string]Turn{}, merges: func() int { return len(w.forge.merges) }}
}
func (f *memTurns) All() (map[string]Turn, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := map[string]Turn{}
for k, v := range f.m {
out[k] = v
}
return out, nil
}
func (f *memTurns) Put(t Turn) error {
if _, err := EncodeTurn(t); err != nil {
return err
}
f.mu.Lock()
defer f.mu.Unlock()
f.m[t.Repository] = t
f.log = append(f.log, fmt.Sprintf("put %s after %d merges", t.Repository, f.merges()))
return nil
}
func (f *memTurns) Delete(repository string) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.m, repository)
f.log = append(f.log, fmt.Sprintf("delete %s after %d merges", repository, f.merges()))
return nil
}
func (f *memTurns) said() string { f.mu.Lock(); defer f.mu.Unlock(); return strings.Join(f.log, "; ") }
func TestOneCheckPerDeliveryAtTheGate(t *testing.T) {
for name, run := range map[string]func(t *testing.T){
// §2: a merge into a repository a running verdict names is not written to merges, and is written on that verdict's pass.
"an approved merge waits for a running check on its repository and merges on its end": func(t *testing.T) {
w := newWorld(t)
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("asked %d times, merges %v: want the Approve asked and the merge held", w.op.count(), w.forge.merges)
}
d := w.delivery(id)
if d.Ready == nil || !strings.Contains(d.Ready.Waits, "waits for the check of g") || !strings.Contains(d.Ready.Waits, oneRepo) ||
d.Ready.HeldSince == nil {
t.Fatalf("the merge does not say what it waits for: %+v", d.Ready)
}
// §9: deliveries says what it waits for.
for _, l := range w.h.Deliveries("", "", "", false) {
if l.ID == id && !strings.Contains(l.Waits, "approved; waits for the check of g") {
t.Errorf("deliveries says it waits for %q", l.Waits)
}
}
// §4: past 30 minutes the held merge is a stalled line naming the check it waits for and since when.
for _, l := range w.h.Stalled() {
if l.ID == id {
t.Fatalf("a stalled line within the bound: %+v", l)
}
}
w.later(turnBound + time.Minute)
w.settleAll()
var line *StalledLine
for _, l := range w.h.Stalled() {
if l.ID == id {
line = &l
}
}
if line == nil || line.Bound != "30m0s" || !strings.Contains(line.Says, "waits for the check of g") || !strings.Contains(line.Says, "held since") ||
!strings.Contains(line.Says, "checker_status") || !strings.Contains(line.H2, "delivery_hand_now") {
t.Fatalf("the stalled line of the held merge = %+v", line)
}
putCheckerVerdict(w, "pass", headOf(oneRepo, 9, otherHead))
w.settleAll()
if len(w.forge.merges) != 1 || w.op.count() != 1 {
t.Fatalf("after the check's end: merges %v, asked %d times; want merged with no second ask", w.forge.merges, w.op.count())
}
},
"a running check on another repository holds nothing": func(t *testing.T) {
w := newWorld(t)
putCheckerVerdict(w, "running", headOf(oneRepo2, 9, otherHead))
approvedLone(w)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("merges %v: a check of another repository held the merge (%s)", w.forge.merges, w.logs())
}
},
"the delivery's own running verdict does not hold it": func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := approvedLone(w)
w.settleAll()
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead)) // its own heads judged again (a carry)
for _, k := range w.op.open() {
w.op.answer(k, OptionApprove)
}
w.settleAll()
if d := w.delivery(id); len(w.forge.merges) != 1 || d.Ready == nil || d.Ready.HeldSince != nil {
t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready)
}
},
// §3: the turn is written before the verdicts are read for the merge, and deleted after the last merge.
"a lone merge's turn is written before it is handed and deleted once it merged": func(t *testing.T) {
w := newWorld(t)
turns := newMemTurns(w)
w.h.Turns = turns
approvedLone(w)
w.settleAll()
if len(w.forge.merges) != 1 || turns.said() != "put "+oneRepo+" after 0 merges; delete "+oneRepo+" after 1 merges" {
t.Fatalf("merges %v; the turns were %q", w.forge.merges, turns.said())
}
},
"a group's turns stand on every member's repository until its last member merged": func(t *testing.T) {
w := newWorld(t)
turns := newMemTurns(w)
w.h.Turns = turns
a, b := pr(oneRepo, 1, oneHead, "feat/two"), pr(oneRepo2, 2, oneHead2, "feat/two")
requiresCheck(w, a, b)
w.h.PullUpdated(a)
w.h.PullUpdated(b)
w.h.wantForge()
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead), headOf(oneRepo2, 2, oneHead2))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
w.h.CommitStatus(checkStatus(oneRepo2, oneHead2, "success", oneTrunk))
for range 3 {
w.settleAll()
}
want := "put " + oneRepo2 + " after 0 merges; put " + oneRepo + " after 0 merges; delete " + oneRepo2 + " after 2 merges; delete " + oneRepo + " after 2 merges"
if len(w.forge.merges) != 2 || turns.said() != want {
t.Fatalf("merges %v; the turns were %q, want %q", w.forge.merges, turns.said(), want)
}
},
// §8: an approved delivery whose trunk moved goes back to proposed, waits for its carried verdict rather than being
// handed on the old one, and merges on it with no second ask.
"an approved merge whose trunk moved merges on its carried verdict with no second ask": func(t *testing.T) {
w := newWorld(t)
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead)) // holds the approved merge (§2)
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("asked %d times, merges %v; want approved and held", w.op.count(), w.forge.merges)
}
w.h.PullMerged(merged(oneRepo, 5, otherHead, oneTrunk2)) // a merge at the forge moves the trunk
putCheckerVerdict(w, "pass", headOf(oneRepo, 9, otherHead))
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merges %v: handed on a verdict judged on the old trunk", w.forge.merges)
}
d := w.delivery(id)
if d.Ready == nil || !strings.Contains(d.Ready.Waits, "being carried") {
t.Fatalf("after the trunk moved it waits for %q, want its verdict carried", d.Ready.Waits)
}
// The checker's carried line: the head on the new trunk, carried from the old one.
carried := checkStatus(oneRepo, oneHead, "success", oneTrunk2)
carried.Description = "pass " + oneHead[:12] + " on " + oneTrunk2[:12] + " | carried from " + oneTrunk[:12] + " | unit 0 run, 4 kept"
if head, trunk := checkLine(carried.Description); head != oneHead[:12] || trunk != oneTrunk2[:12] {
t.Fatalf("the carried line reads as %s on %s", head, trunk)
}
if head, trunk := checkLine("failed: a test"); head != "" || trunk != "" {
t.Fatalf("a line that names no commits gave %q %q", head, trunk)
}
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(carried)
w.settleAll()
if w.op.count() != 1 {
t.Errorf("the Approve was asked %d times, want once: it stands through the carry", w.op.count())
}
if d := w.delivery(id); d.Check == nil || d.Check.Trunk != oneTrunk2[:12] || len(w.forge.merges) != 1 {
t.Errorf("after the carry: verdict %+v, merges %v; want merged on the carried verdict", d.Check, w.forge.merges)
}
},
// §4: delivery_turns reads every turn with the check it waits for; delivery_hand_now hands a held merge at once on the
// operator's word, and refuses one no check holds.
"the turns are read, and a held merge is handed now on the operator's word": func(t *testing.T) {
w := newWorld(t)
w.h.Turns = newMemTurns(w)
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
id := approvedLone(w)
w.settleAll()
read := w.h.TurnsRead()
lines := read["turns"].([]TurnLine)
if len(lines) != 1 || lines[0].Repository != oneRepo || len(lines[0].Turn.Waiting) != 1 || lines[0].Turn.Waiting[0].WaitsFor == "" ||
!strings.Contains(lines[0].Waits["feat/one"].(string), "the check g.") || read["written"] != true {
t.Fatalf("delivery_turns = %+v", read)
}
if _, err := w.h.HandNow(id, ""); err == nil || !strings.Contains(err.Error(), "why") {
t.Errorf("a hand without the operator's why: %v", err)
}
if _, err := w.h.HandNow(IDOf(oneRepo2, oneHead2), "x"); err == nil {
t.Error("an unknown delivery was handed")
}
if _, err := w.h.HandNow(id, "the operator, on telegram: merge it now"); err != nil {
t.Fatal(err)
}
w.settleAll()
if len(w.forge.merges) != 1 || w.delivery(id).Ready.HandNow == "" {
t.Fatalf("merges %v after the hand", w.forge.merges)
}
if _, err := w.h.HandNow(id, "again"); err == nil {
t.Error("a merged delivery was handed again")
}
},
} {
t.Run(name, run)
}
}
@@ -0,0 +1,136 @@
package main
import (
"strings"
"testing"
"time"
)
// No check gates a merge (novox/hq ADR 0661), the gate's part: no approved merge waits for another delivery's running check,
// and no turn or stalled line holds it (§1); the merge judges the operator's Approve alone, so an Approve given before a trunk
// move merges onto the new trunk with no wait for its carry (§3); a failure after the Approve is said once on the pull
// request and as a stalled line naming the merge it follows, and refuses nothing (§5).
const otherHead = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
// approvedLone is a lone delivery into oneRepo whose check passed, approved by the operator (the world approves at once).
func approvedLone(w *world) string {
w.t.Helper()
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
return IDOf(oneRepo, oneHead)
}
// sayingFailed is the forge's word that oneHead's check failed on the trunk commit given (a carry's line).
func sayingFailed(trunk string) ForgeStatusEvent {
e := checkStatus(oneRepo, oneHead, "failure", trunk)
e.Description = "failed " + oneHead[:12] + " on " + trunk[:12] + " | a test failed"
return e
}
func lineOf(w *world, id string) *StalledLine {
for _, l := range w.h.Stalled() {
if l.ID == id {
return &l
}
}
return nil
}
func TestNoCheckGatesAMergeAtTheGate(t *testing.T) {
for name, run := range map[string]func(t *testing.T){
// §1: a running check of other heads on the same repository holds nothing, and no stalled line is raised for it.
"an approved merge into a repository under a running check merges at once": func(t *testing.T) {
w := newWorld(t)
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 1 {
t.Fatalf("asked %d times, merges %v: want asked once and merged at once (%s)", w.op.count(), w.forge.merges, w.logs())
}
w.h.PullMerged(merged(oneRepo, 1, oneHead, strings.Repeat("c", 40))) // the seat git says the merge too
w.later(time.Hour)
w.settleAll()
if l := lineOf(w, id); l != nil {
t.Fatalf("a stalled line for a merge a running check once held: %+v", l)
}
},
"the delivery's own running verdict does not hold it": func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := approvedLone(w)
w.settleAll()
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead)) // its own heads judged again (a carry)
for _, k := range w.op.open() {
w.op.answer(k, OptionApprove)
}
w.settleAll()
if d := w.delivery(id); len(w.forge.merges) != 1 || d.Ready == nil {
t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready)
}
},
// §3 (and ADR 0548 §8): the ask stands through a trunk move; the Approve given then merges onto the new trunk on the
// verdict judged against the old one, with no wait for its carry and no second ask.
"an Approve given after a trunk move merges at once, its carry after": func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("asked %d times, merges %v: want asked and not merged yet", w.op.count(), w.forge.merges)
}
w.h.PullMerged(merged(oneRepo, 5, otherHead, oneTrunk2)) // a merge at the forge moves the trunk
w.settleAll()
for _, k := range w.op.open() {
w.op.answer(k, OptionApprove)
}
w.settleAll()
d := w.delivery(id)
if len(w.forge.merges) != 1 || w.op.count() != 1 || d.State != Ready && d.State != Merged {
t.Fatalf("merges %v, asked %d times, %s (waits %q): want merged at once on the old verdict (%s)", w.forge.merges,
w.op.count(), d.State, d.Ready.Waits, w.logs())
}
},
// §5: a failure after the Approve, a carry's, is said once on the pull request and as one stalled line naming the merge;
// it refuses nothing, and a later pass clears the line.
"a failure after the Approve is said once and refuses nothing": func(t *testing.T) {
w := newWorld(t)
id := approvedLone(w)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("merges %v: want merged at once", w.forge.merges)
}
before := len(w.forge.comments)
w.h.CommitStatus(sayingFailed(oneTrunk2))
w.settleAll()
w.h.CommitStatus(sayingFailed(oneTrunk2))
w.settleAll()
said := 0
for _, c := range w.forge.comments[before:] {
if strings.Contains(c, "failed after the Approve") {
said++
}
}
if said != 1 {
t.Fatalf("said %d times on the pull request, want once: %v", said, w.forge.comments[before:])
}
l := lineOf(w, id)
if l == nil || !strings.Contains(l.Says, "after the operator's Approve") || !strings.Contains(l.Says, oneHead[:12]) {
t.Fatalf("the stalled line = %+v", l)
}
if d := w.delivery(id); d.State == Rejected || len(w.forge.merges) != 1 {
t.Fatalf("%s, merges %v: the failure refused the approved merge", d.State, w.forge.merges)
}
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk2))
w.settleAll()
if l := lineOf(w, id); l != nil {
t.Fatalf("a later pass left the stalled line: %+v", l)
}
},
} {
t.Run(name, run)
}
}
@@ -338,9 +338,9 @@ func (h *Holder) endStaleAsks() {
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "its head "+shortOf(d.Commit, 12)+" is no longer the pull request's head, or the pull request closed"))
case !h.headsHold(d, r):
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "a head it covers moved on: the heads it covers are no longer the group's heads"))
case r.Approved == nil && r.AskID != "" && r.AskCheck != "" && !h.checkGates() && h.checkMoved(d, r.AskCheck):
// The check gates no merge, so the ask was made before its verdict: once the check passes or fails (or a pass it
// named is gone), the ask is asked again in words that say so (issue 1733). Running and not started say the same.
case r.Approved == nil && r.AskID != "" && r.AskCheck != "" && h.checkMoved(d, r.AskCheck):
// The check gates the ask, so it was made on a pass: once that pass is gone (a failure, or a check of these heads
// running again), the ask not yet answered is withdrawn in words that say so (novox/hq ADR 0661 §5, ADR 0441 §1).
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, "the check at its heads is now: "+h.checkAt(h.askMembers(d)).Words))
case r.Approved == nil && r.AskID != "" && !r.AskedAt.IsZero() && !h.Now().Before(r.AskedAt.Add(AskLasts)):
// The ask lapsed at the router with nobody's answer, and nothing in the gate's inputs says so: it is taken away and
@@ -225,9 +225,6 @@ func (h *Holder) nextStall(now time.Time) time.Time {
consider(r.RefusedSince.Add(h.answeredRefusalBound(d.Repository)))
consider(r.RefusedSince.Add(outOfDateFor))
}
if r.HeldSince != nil {
consider(r.HeldSince.Add(turnBound))
}
}
return next
}
@@ -67,9 +67,7 @@ func (h *Holder) CommitStatus(e ForgeStatusEvent) {
h.takeCheckStatus(e.Owner+"/"+e.Repo, e.SHA, e.State, e.Description)
}
if e.State != "success" {
if h.checkGates() {
h.withdrawForStatus(e) // while the check gates no merge, its status is shown and takes back no ask
}
h.withdrawForStatus(e) // a status not a success takes back an ask not yet answered (novox/hq ADR 0661 §5)
return
}
repository := strings.ToLower(e.Owner + "/" + e.Repo)
@@ -196,7 +196,7 @@ func TestNoWaitReadsADescription(t *testing.T) {
w.h.mu.Lock()
got := []string{state, why}
for _, d := range ds {
got = append(got, w.h.readyWaits(d), w.h.ungatedWaits(d))
got = append(got, w.h.askWaits(d), w.h.approvedWaits(d))
}
w.h.mu.Unlock()
if i == 0 {
@@ -43,14 +43,24 @@ const (
// conflict); empty when all hold. forge says the reason is the forge's merge answer rather than a status. An error is a
// read that could not be made: the member then counts as not mergeable.
func (h *Holder) notASuccess(m memberRead) (says string, forge bool, err error) {
return h.memberRead(m, true)
}
// notMergeableNow is notASuccess at the merge: the head the Approve covers and the forge's merge answer, and no required
// status read again, for the merge judges the operator's Approve alone (novox/hq ADR 0661 §4).
func (h *Holder) notMergeableNow(m memberRead) (says string, forge bool, err error) {
return h.memberRead(m, false)
}
func (h *Holder) memberRead(m memberRead, statuses bool) (says string, forge bool, err error) {
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
if err != nil {
return "", false, err
}
if fc.Merge.Mergeable == nil {
if statuses && fc.Merge.Mergeable == nil {
return "", false, fmt.Errorf("the forge could not say whether %s may merge: %s", m.id, fc.Merge.Says)
}
if !*fc.Merge.Mergeable {
if statuses && !*fc.Merge.Mergeable {
var parts []string
for _, b := range fc.Merge.Blocking {
parts = append(parts, fmt.Sprintf("%s (%s)", b.Context, b.State))
@@ -82,8 +92,8 @@ func (h *Holder) unmergedLive(g *Group) []*Delivery {
return out
}
// groupMergeable reads every member's required statuses, its head and the forge's merge answer at the head the Approve
// covers, and says whether all hold: no member is merged unless every member merges now (issue 1275, 2026-10-26: two of
// groupMergeable reads every member's head and the forge's merge answer at the head the Approve covers, no required status
// (novox/hq ADR 0661 §4), and says whether all hold: no member is merged unless every member merges now (issue 1275, 2026-10-26: two of
// three merged and the third refused). When one does not, or cannot be read, nothing is handed to the bridge: the group is
// held, said urgent (ADR 0441 §2, §4), and the target's turn is given back so the next event that decides whether it may
// merge asks it again. A read that could not be made is a failed ask, tried again after askAgainAfter; the forge saying a
@@ -97,7 +107,7 @@ func (h *Holder) groupMergeable(gid, target string, reads []memberRead) bool {
}
}
for _, m := range reads {
says, forge, err := h.notASuccess(m)
says, forge, err := h.notMergeableNow(m)
if err == nil && says == "" {
continue
}
@@ -21,12 +21,6 @@ func verdictMoving(repo string, number int, sha string, _ ...string) CheckedEven
return verdict(repo, number, sha, "pass")
}
// ungated is the setting check-gates-merge false: mesh/check gates no merge (the operator, 2026-10-25 22:56).
func ungated(w *world) {
off := false
w.h.Settings = func() Settings { return Settings{CheckGatesMerge: &off} }
}
func statusOn(w *world, repo, commit string) string {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
@@ -85,7 +85,7 @@ type Holder struct {
// unchecked are the open pull requests' heads seen without a merge check (unchecked.go).
unchecked map[string]unchecked
// checker are the checker's verdicts by the hash of the heads they judged, as its verdicts bucket holds them
// (onestatus.go, issue 1302); runningSince when this owner first saw each running (ADR 0548 §2, turns.go).
// (onestatus.go, issue 1302); runningSince when this owner first saw each running, said in the check's words.
checker map[string]checkerVerdict
runningSince map[string]time.Time
// unmergeableSince is when the forge first said a group's member does not merge now, by group and member (issue 1275,
@@ -93,11 +93,6 @@ type Holder struct {
unmergeableSince map[string]time.Time
// writeReads is whether the forge's merging account writes each repository, as last read (writes.go, issue 1945).
writeReads map[string]*writeRead
// Turns is the gate's bucket turns (turns.go, ADR 0548 §3); nil keeps none. turnsHeld are the turns as written there,
// by repository, read once at the first look (turnsLoaded).
Turns TurnStore
turnsHeld map[string]Turn
turnsLoaded bool
// reach is the delivery view's reach of each repository, by its key (reached.go, novox/hq ADR 0556): what `deliveries`
// and `show` derive whether a merge is on the machines from. Nothing here records it.
reach map[string]viewstate.Reach
@@ -152,7 +147,7 @@ func (h *Holder) init() {
h.lostRead = map[string]time.Time{}
h.unchecked = map[string]unchecked{}
h.checker = map[string]checkerVerdict{}
h.runningSince, h.turnsHeld = map[string]time.Time{}, map[string]Turn{}
h.runningSince = map[string]time.Time{}
h.reach = map[string]viewstate.Reach{}
h.started = h.Now()
}
@@ -56,29 +56,16 @@ func urgentLines(w *world) []StalledLine {
return out
}
// 1275/AC2: an approved group whose member has a required status that is not a success at its head when the merge starts
// is merged not at all, though the first member in order is fine; the group says which member and which status.
func TestAnApprovedGroupWithAMemberWhoseRequiredStatusIsNotASuccessMergesNothing(t *testing.T) {
// novox/hq ADR 0661 §4 (changing 1275/AC2 and ADR 0441 §2): an approved group whose member's required status turned
// failed after the Approve merges whole, with no read of its statuses at the merge, and raises no line.
func TestAnApprovedGroupWhoseMemberStatusTurnedAfterTheApproveMergesWhole(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing after the ask
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged with a required status missing: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || !strings.Contains(lines[0].Says, "feat/g") || !strings.Contains(lines[0].Says, IDOf(gCtl, gHc)) ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("the held group is not said as one urgent line naming group, member, status and need: %+v", lines)
}
// The status turns success: the whole group merges.
setChecks(w, gCtl, gHc, "success", "success")
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.later(time30s)
w.settleAll()
if len(w.forge.merges) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("merges %v, lines %+v; wanted both merged and no line", w.forge.merges, urgentLines(w))
t.Fatalf("merges %v, lines %+v; wanted both merged on the Approve and no line", w.forge.merges, urgentLines(w))
}
}
@@ -142,33 +129,6 @@ func TestAStatusThatTurnsWhileTheAskWaitsWithdrawsItAndItsAnswerMergesNothing(t
}
}
// 1275/AC3 (review round 1): a member that is both refused and held for a required status is said by one stalled line,
// not two with the same id.
func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
setChecks(w, gCat, gHk, "success", "") // now its status is missing too; the next pass is held
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "success"})
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if g := w.h.groups["feat/g"]; g.Hold == nil {
t.Fatalf("the group was not held for the missing status")
}
n := 0
for _, l := range w.h.Stalled() {
if l.ID == IDOf(gCat, gHk) {
n++
}
}
if n != 1 {
t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled())
}
}
// 1275/AC6, as novox/hq ADR 0505 §4 changed it: a member refused because its verdict is out of date (the trunk moved; its
// fresh check runs) is no stalled line while the gate answers it, and urgent once ten minutes passed with the group not
// merged; the pass stops at the refusal all the same.
@@ -505,7 +465,7 @@ func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
bridgeMerged(w, gCat, 2, gHk, gT1)
setChecks(w, gCtl, gHc, "success", "") // the second member's status went missing: the pass stops at it
moveForgeHead(w, gCtl, 1, otherHead) // the second member's head moved at the forge: the pass stops at it
w.settleAll()
for range 3 {
w.later(time30s)
@@ -522,7 +482,7 @@ func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
!strings.Contains(c.Summary, "half merged (merged 1 of 2)") {
t.Fatalf("raised %d times, last %+v; wanted once, urgent, naming the group half merged", raised, c)
}
setChecks(w, gCtl, gHc, "success", "success")
moveForgeHead(w, gCtl, 1, gHc)
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.settleAll() // the status is an event: the pass goes on with no timer
handed := false
@@ -770,3 +730,13 @@ func bridgeMerged(w *world, repository string, number int, head, merge string) {
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
Trees: map[string]string{"app": "tree"}})
}
// moveForgeHead sets the head the forge says a pull request has, as a push the gate has not heard yet does.
func moveForgeHead(w *world, repo string, number int, head string) {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
key := fmt.Sprintf("%s#%d", repo, number)
p := w.forge.pulls[key]
p.HeadSHA = head
w.forge.pulls[key] = p
}
@@ -135,62 +135,47 @@ func handedWords(ps []MergePull) string {
// Two approved merges into one repository: the one whose pass started has the repository until every member merged, and
// the other is handed only then (2026-10-26: mesh-delivery#72 of feat/890-assignments merged between fix/734-lost-ask's
// members). This holds while mesh/check gates no merge too (ADR 0551): it waits for a merge, never for a check.
// members). It waits for a merge, never for a check: once the group's pass ended #72 is handed on its Approve, its verdict
// gone out of date with #73's merge or not (novox/hq ADR 0661 §3).
func TestTwoApprovedMergesIntoOneRepositoryDoNotInterleave(t *testing.T) {
for _, gates := range []bool{true, false} {
t.Run(fmt.Sprintf("check-gates-merge %v", gates), func(t *testing.T) {
w := approvedGroupOf(t, lostAsk...)
if !gates {
ungated(w)
}
w.merges.manual = true
allGreenFor(w, lostAsk...)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
t.Fatalf("handed %q, wanted the group's first member", got)
}
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
setChecks(w, wDel, wH72, "success", "success")
w.settleAll()
if open := w.op.open(); len(open) != 1 {
t.Fatalf("the lone pull request is asked once: %v", open)
} else {
w.op.answer(open[0], OptionApprove)
}
w.settleAll()
var order []string
steps := []struct {
repo string
number int
head string
}{{wSDK, 92, wH92}, {wCtl, 472, wH472}, {wDel, 73, wH73}}
if !gates {
// While the check gates the merge, #72's verdict went out of date with #73's merge and it waits for its
// check against the new trunk; while it gates none, #72 is handed once the group's pass ended.
steps = append(steps, struct {
repo string
number int
head string
}{wDel, 72, wH72})
}
for _, next := range steps {
handed := handing(w)
if len(handed) != 1 || handed[0].Repository != next.repo || handed[0].Number != next.number {
t.Fatalf("after %v: handed %q, wanted only %s#%d", order, handedWords(handed), next.repo, next.number)
}
merge := strings.Repeat(string(rune('a'+next.number%20)), 40)
bridgeMerged(w, next.repo, next.number, next.head, merge)
w.h.PullMerged(merged(next.repo, next.number, next.head, merge))
w.settleAll()
order = append(order, fmt.Sprintf("%s#%d", next.repo, next.number))
}
if left := handing(w); len(left) != 0 {
t.Fatalf("still handed: %q", handedWords(left))
}
})
w := approvedGroupOf(t, lostAsk...)
w.merges.manual = true
allGreenFor(w, lostAsk...)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
t.Fatalf("handed %q, wanted the group's first member", got)
}
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
setChecks(w, wDel, wH72, "success", "success")
w.settleAll()
if open := w.op.open(); len(open) != 1 {
t.Fatalf("the lone pull request is asked once: %v", open)
} else {
w.op.answer(open[0], OptionApprove)
}
w.settleAll()
var order []string
steps := []struct {
repo string
number int
head string
}{{wSDK, 92, wH92}, {wCtl, 472, wH472}, {wDel, 73, wH73}, {wDel, 72, wH72}}
for _, next := range steps {
handed := handing(w)
if len(handed) != 1 || handed[0].Repository != next.repo || handed[0].Number != next.number {
t.Fatalf("after %v: handed %q, wanted only %s#%d", order, handedWords(handed), next.repo, next.number)
}
merge := strings.Repeat(string(rune('a'+next.number%20)), 40)
bridgeMerged(w, next.repo, next.number, next.head, merge)
w.h.PullMerged(merged(next.repo, next.number, next.head, merge))
w.settleAll()
order = append(order, fmt.Sprintf("%s#%d", next.repo, next.number))
}
if left := handing(w); len(left) != 0 {
t.Fatalf("still handed: %q", handedWords(left))
}
}
@@ -135,18 +135,15 @@ func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
}
}
// ADR 0441 §2: before a member is handed, every member's required statuses are read again; one that is not a success holds
// the group, even after the first member merged.
func TestTheNextMemberIsNotHandedWhileARequiredStatusOfTheGroupIsNotASuccess(t *testing.T) {
// novox/hq ADR 0661 §4 (changing ADR 0441 §2): the next member is handed on the Approve covering its head, with no read of
// the group's required statuses, after the first member merged.
func TestTheNextMemberIsHandedWithNoReadOfTheGroupsRequiredStatuses(t *testing.T) {
w := answeredFirst(t)
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing meanwhile
w.settleAll()
if len(w.merges.puts) != 1 {
t.Fatalf("the second member was handed with a required status missing: %d merges written", len(w.merges.puts))
}
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "mesh/repo-check (missing)") {
t.Fatalf("the held group is not said urgent: %+v", lines)
if len(w.merges.puts) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("%d merges written, lines %+v: wanted the second member handed on its Approve", len(w.merges.puts), urgentLines(w))
}
}
@@ -6,7 +6,7 @@ import "git.novox.be/novox/mesh-sdk/go/announce"
// issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said
// (clause 3). A state is readable when another module's manifest reads it (keeps_test.go names the readers). keeps_test.go holds the list to the
// manifest's `state`, and keeps_integration_test.go that the module announces it on a bus.
var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}, {Name: "turns", Readable: true}}
var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}}
// announceKept says Kept through keeps, which is stdio.Keeps.
func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) }
@@ -66,7 +66,6 @@ func main() {
Operator: kvOperator{},
Merges: kvMerges{},
Source: kvSource{},
Turns: kvTurns{},
Owed: seatOwed{say: stdio.SeatPublish},
Pulls: kvPulls{},
// The gate says what stalled to the operator itself (issue 1275, ADR 0437 §9).
@@ -435,24 +434,6 @@ func tools(h *Holder, l *listening) []stdio.Tool {
}
return h.Settle(ids, strArg(a, "why"), aPerson)
}},
{Name: "delivery_turns",
Description: "Every merge turn (novox/hq ADR 0548 §3): per repository, the approved merges that wait to merge into it in the order of " +
"their Approve, the check of another delivery each waits for and since when, whether the gate wrote the turn to its bucket " +
"turns yet (the checker claims no check of a repository a turn names), and the bound past which a held merge is a stalled " +
"line. Read-only.",
Run: func(map[string]any) (any, error) { return h.TurnsRead(), nil }},
{Name: "delivery_hand_now",
Description: "On the operator's word, hand an approved merge held for another delivery's running check to the merge at once " +
"(novox/hq ADR 0548 §4): the check it waited for runs on and is carried to the new trunk after the merge. Refused for a " +
"delivery that is not an approved merge held by a check, naming why. The why is kept on the delivery.",
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("the delivery's id (owner/repository@commit)"),
"why": str("the operator's word, kept on the delivery")}, "required": []string{"id", "why"}},
Run: func(a map[string]any) (any, error) {
if err := need(a, "id", "why"); err != nil {
return nil, err
}
return h.HandNow(strArg(a, "id"), strArg(a, "why"))
}},
{Name: "delivery_merge_again",
Description: "On the operator's word, ask again at once the merge of an approved delivery the forge refused, or of an approved " +
"group held or half merged, at the heads the Approve named (novox/hq issue 1275): every member is read first and " +
@@ -120,12 +120,20 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
t.Fatalf("emits %v", m.Emits)
}
// Nine buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3, the conditions it raised (issue 1275) and the
// merge turns (ADR 0548 §3).
// Eight buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3 and the conditions it raised (issue 1275). No
// `turns`, no turn tool and no `check-gates-merge`: no check gates a merge (novox/hq ADR 0661 §1, §2, §6).
buckets := m.buckets()
if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions", "turns"}) {
if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source", "conditions"}) {
t.Fatalf("state %v", buckets)
}
for _, tool := range m.Tools {
if strings.Contains(tool, "turn") || strings.Contains(tool, "hand_now") {
t.Fatalf("the gate lists the turn tool %s", tool)
}
}
if raw, err := os.ReadFile("../../module.json"); err != nil || strings.Contains(string(raw), "check-gates-merge") {
t.Fatalf("the manifest still names check-gates-merge (%v)", err)
}
// What it reads: the teller's answers and the checker's verdicts (issue 1302). The carry from today's module's buckets, the
// seat's holder and the machines' haves went with the seat's hand-over (ADR 0539 §17). The delivery view's reach, which
// `deliveries` and `show` derive whether a merge is on the machines from (ADR 0556 clause 4); no forge tool is asked for it.
@@ -164,6 +164,13 @@ func (v *Verdict) Judged() string {
// outOfDate is why a delivery's verdict no longer counts: it names a trunk commit that is not its trunk's head
// as this owner heard it (novox/hq ADR 0323 §7). Empty when it counts — or names no trunk commit, as a verdict
// of a build seat from before the rule does, which counts as it did.
// approvedAtHead is whether the operator's Approve covers this delivery at its head: once it does, the merge judges that
// Approve alone, not the check (novox/hq ADR 0661 §3, §4).
func (d *Delivery) approvedAtHead() bool {
r := d.Ready
return r != nil && r.Approved != nil && !r.declined() && r.Heads[d.ID] == d.Commit
}
func (d *Delivery) outOfDate() string {
if d.Check == nil || d.Check.Trunk == "" || d.TrunkNow == "" || sameCommit(d.Check.Trunk, d.TrunkNow) {
return ""
@@ -97,7 +97,7 @@ func (h *Holder) CheckerVerdictChanged(key string, value []byte) {
v.Heads = hash
}
if prev, ok := h.checker[v.Heads]; v.State == checkerRunning && (!ok || prev.State != checkerRunning) {
h.runningSince[v.Heads] = h.Now() // a check starts: a merge it holds says since when (ADR 0548 §2)
h.runningSince[v.Heads] = h.Now() // a check starts: its words say since when
} else if v.State != checkerRunning {
delete(h.runningSince, v.Heads)
}
@@ -171,10 +171,12 @@ func (h *Holder) takeCheckStatus(repository, sha, state, desc string) {
}
h.moved(d, []Transition{t})
}
if d.State == Ready && !h.checkGates() {
// The check gates no merge (check-gates-merge false): its verdict is shown on the delivery, and moves nothing.
if d.approvedAtHead() && (d.State == Ready || d.State == Merged) {
// The merge judges the operator's Approve alone (novox/hq ADR 0661 §3): a verdict after it, a carry's, is shown and
// moves nothing; a failure is said (failedAfterApprove).
d.Check = v
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s (shown; the check gates no merge)", d.ID, state, oneLine(clip(desc, 120)))
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s (after the operator's Approve)", d.ID, state, oneLine(clip(desc, 120)))
h.failedAfterApprove(d, v)
h.keep(d)
return
}
@@ -128,12 +128,11 @@ type ReadyRun struct {
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// One check per delivery (novox/hq ADR 0548 §2, §4, turns.go): HeldSince is when the approved merge began to wait for the
// check of other heads HeldFor names (its verdict key), running on a repository it goes into; HandNow the operator's word
// that handed it at once past that check.
HeldSince *time.Time `json:"held_since,omitempty"`
HeldFor string `json:"held_for,omitempty"`
HandNow string `json:"hand_now,omitempty"`
// FailedAfter is a failure heard after the operator's Approve covered the head, a carry's (novox/hq ADR 0661 §5): said
// once on the pull request and as a stalled line naming the merge it follows, never a refusal of the merge; cleared by a
// later pass. FailedAfterSince is when it was heard.
FailedAfter string `json:"failed_after,omitempty"`
FailedAfterSince *time.Time `json:"failed_after_since,omitempty"`
// CannotWrite is why the operator is not asked: the forge's merging account cannot write a repository the ask covers
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
@@ -194,7 +193,6 @@ func (h *Holder) runOf(d *Delivery, now time.Time) *ReadyRun {
func (h *Holder) readyAsks(now time.Time) []func() {
var asks []func()
h.endStaleAsks()
gates := h.checkGates()
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
@@ -205,9 +203,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
if d.Number == 0 || d.State.Final() || d.MergedAs != "" {
continue // not of a pull request, or past its merge
}
if !gates {
h.ungate(d, now)
}
if d.Ready == nil && d.State == Ready {
h.runOf(d, now)
h.Poke() // its check, if none is asked, is worth asking now (askCheck)
@@ -276,9 +271,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
if waits == "" {
waits, approvalAsk = h.approval(d, now)
}
if waits == "" && gates {
waits = h.turnWaits(d, now) // nothing merges into a repository while a check of other heads judges it (ADR 0548 §2)
}
if waits == "" {
waits = h.passWaits(d) // no merge lands in a repository between another approved group's members (issue 1275)
}
@@ -310,7 +302,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
h.noteNotHanded(d, h.notHanded(g, d), now) // a merge never handed says why (2026-10-29: silent for an hour)
}
}
asks = append(asks, h.turnWrites()...) // the merge turns, after the holds of this look (ADR 0548 §3)
asks = append(asks, h.settleMerges()...)
asks = append(asks, h.settleForgeMerges()...)
asks = append(asks, h.recordForgeMade()...)
@@ -321,30 +312,6 @@ func (h *Holder) readyAsks(now time.Time) []func() {
return asks
}
// ungate stands a delivery where it is while mesh/check gates no merge (check-gates-merge false): a proposed one is ready
// with no verdict, and a rejected one, refused on a verdict before the setting changed, is proposed again and so ready. Its
// verdict stays on it, shown. Under the lock.
func (h *Holder) ungate(d *Delivery, now time.Time) {
f := h.factsAt(now)
if d.State == Rejected {
f.By, f.Why = byOwner, "the check gates no merge (check-gates-merge false): its verdict is shown, not waited on"
t, err := Apply(d, EvRecheck, true, f)
if err != nil {
h.refuse(err)
return
}
if d.Ready != nil {
d.Ready.Failed = ""
}
h.moved(d, []Transition{t})
f.By, f.Why = "", ""
}
if d.State == Proposed {
h.moved(d, settle(d, f))
h.closeGroupOf(d)
}
}
// sayAfterLine tells a pull request, once, that its description holds an `after: <repository>` line, which is not read
// (novox/hq ADR 0419 §2): the order of a delivery comes from the module graph alone, and no refusal asks for the line.
// Once per pull request, not per head. Under the lock; the comment is said outside it.
@@ -406,14 +373,21 @@ func (r *ReadyRun) noteRefusal(d *Delivery, answer string) {
r.Repeats, r.RepeatTrunk = 1, d.TrunkNow
}
// readyWaits is what a delivery's merge waits on; empty when the Approve may be asked and the merge asked. A ready
// delivery is asked. So is a proposed one whose verdict went out of date, when an Approve was asked for it already (it
// stays through a trunk move, ADR 0421 §3) and its check against the trunk's head is not on its way; one without an
// Approve waits for that check first.
// readyWaits is what a delivery's merge waits on; empty when the Approve may be asked and the merge asked. Once the
// operator's Approve covers its heads the merge judges that Approve alone (approvedWaits); until then the check gates the
// ask (askWaits), the one place a check gates anything here (novox/hq ADR 0661 §3, §4). Under the lock.
func (h *Holder) readyWaits(d *Delivery) string {
if !h.checkGates() {
return h.ungatedWaits(d)
if c := h.covering(d); c != nil && c.Approved != nil {
return h.approvedWaits(d)
}
return h.askWaits(d)
}
// askWaits is why the operator's Approve is not asked yet: the check gates the ask (novox/hq ADR 0661 §3, ADR 0421 §1). A
// ready delivery is asked once the checker's verdict at exactly the heads the ask covers passed; a proposed one whose
// verdict went out of date while its ask is pending waits for its check against the trunk's head. A group is asked only
// once its composed check passed at its members' heads (ADR 0661 §4).
func (h *Holder) askWaits(d *Delivery) string {
if d.judgedByCheck() && d.State == Proposed && d.outOfDate() != "" && h.covering(d) == nil {
return "its mesh/check is " + d.outOfDate() + ": the checker checks it again against the trunk's head"
}
@@ -421,10 +395,6 @@ func (h *Holder) readyWaits(d *Delivery) string {
case d.State == Ready:
case d.State == Proposed && d.outOfDate() != "" && d.AskedAgain != nil:
return "its check against the trunk's head is asked and has not answered"
case d.State == Proposed && d.outOfDate() != "" && d.Check.Passes() && h.covering(d) != nil:
// Approved, and its trunk moved: the checker carries its verdict to the trunk's head by its package digests (ADR 0548
// §5, §8). The Approve stands and is not asked again; the merge waits for the carried verdict, not a fresh check.
return "approved; its verdict is being carried to the trunk's head " + shortOf(d.TrunkNow, 12) + " (" + d.outOfDate() + ")"
case d.State == Proposed && d.outOfDate() != "":
return "its verdict is out of date: its check against the trunk's head is asked before the operator is"
case d.State == Proposed && d.AskedAgain != nil:
@@ -442,10 +412,16 @@ func (h *Holder) readyWaits(d *Delivery) string {
return h.groupTurnToMerge(d)
}
// ungatedWaits is readyWaits while mesh/check gates no merge (check-gates-merge false): a ready delivery waits for nothing but
// a group member for every member to be ready; no verdict, composed check or trunk is waited on,
// so the Approve is asked at once.
func (h *Holder) ungatedWaits(d *Delivery) string {
// approvedWaits is what a merge the operator's Approve covers waits on: nothing of the check (novox/hq ADR 0661 §3, §4).
// No verdict, carry, composed check or trunk move is waited on, and no status is read again; a group member waits only
// for every member to be ready, at the heads the Approve covers, so the group merges in one pass. A proposed delivery
// the Approve covers (a trunk moved before the Approve took its run) is ready on it. Under the lock.
func (h *Holder) approvedWaits(d *Delivery) string {
if c := h.covering(d); d.State == Proposed && d.Ready != nil && c != nil && c.Approved != nil {
now := h.Now()
h.approval(d, now) // the covering Approve taken onto its own run, so the table reads it
h.moved(d, settle(d, h.factsAt(now)))
}
if d.State != Ready {
return "it is " + string(d.State) + ", not ready yet"
}
@@ -456,9 +432,6 @@ func (h *Holder) ungatedWaits(d *Delivery) string {
}
}
}
// No verdict is waited on (the operator, 2026-11-07: "we aren't running any checks anymore, we're delivering"): while
// the check gates no merge, the Approve is asked once the delivery is ready; a verdict that comes later
// is shown on the pull request and the delivery, as before.
return ""
}
@@ -517,12 +490,50 @@ func (h *Holder) sayOn(d *Delivery, body string) func() {
return func() {}
}
// failedAfterApprove says a failure heard after the operator's Approve covered the head: once on the pull request, and
// as the stalled line readyLines lists; the merge is not refused for it, and the operator decides the remedy, a fix or a
// revert (novox/hq ADR 0661 §5). A later pass clears it. Under the lock.
func (h *Holder) failedAfterApprove(d *Delivery, v *Verdict) {
r := d.Ready
if r == nil {
return
}
if v.Passes() {
r.FailedAfter, r.FailedAfterSince = "", nil
return
}
if r.FailedAfter != "" {
return
}
now := h.Now()
r.FailedAfter, r.FailedAfterSince = "its check failed after the operator's Approve: "+oneLine(clip(v.Summary, 120)), &now
h.Logf("[mesh-delivery] %s (%s): %s", d.ID, h.mergeFollowed(d), r.FailedAfter)
h.sayOn(d, fmt.Sprintf("mesh-delivery: **failed after the Approve.** This pull request (%s) %s. The merge is not "+
"refused for it: the operator decides the remedy, a fix or a revert.", h.mergeFollowed(d), r.FailedAfter))
}
// mergeFollowed names the merge a failure after the Approve follows: the trunk commit it merged as, or its approved head
// not merged yet.
func (h *Holder) mergeFollowed(d *Delivery) string {
if d.MergedAs != "" {
return "merged as " + shortOf(d.MergedAs, 12) + " at its head " + shortOf(d.Commit, 12)
}
return "approved at its head " + shortOf(d.Commit, 12) + ", not merged yet"
}
// readyLines are the approved heads that will not merge: a check that failed at once, a merge refused past its bound;
// as stalled lines, which the gate says as conditions (conditions.go). Under the lock.
func (h *Holder) readyLines(now time.Time) []StalledLine {
var out []StalledLine
for _, d := range h.deliveries {
r := d.Ready
if r != nil && r.FailedAfter != "" && r.FailedAfterSince != nil && d.State != Closed {
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.FailedAfterSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the operator decides the remedy, a fix or a revert; a later pass clears it",
Says: fmt.Sprintf("%s#%d (%s): %s", d.Repository, d.Number, h.mergeFollowed(d), r.FailedAfter)})
continue
}
if r == nil || r.Merged != nil || d.MergedAs != "" || d.State.Final() {
continue
}
@@ -8,7 +8,6 @@ import (
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **A pull request whose checks passed and whose merge the operator approved is merged by this owner** (novox/hq issues
@@ -85,22 +84,9 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
{name: "gated, passed", gated: true, state: "pass", asks: 1, head: "Merge into main: app#7 Fix it",
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
not: []string{"before its check ends", "no verdict yet"}},
// The operator, 2026-11-07: "we aren't running any checks anymore, we're delivering": while the check gates no
// merge the Approve is asked at once, with no verdict or a running check, never waiting for one.
{name: "ungated, no verdict", asks: 1, not: []string{waitsCheckBeforeAsking}},
{name: "ungated, running", state: "running", asks: 1, not: []string{waitsCheckBeforeAsking}},
// Asked at once, then asked again in its words once the verdict comes (a later verdict asks a pending Approve again).
{name: "ungated, passed", state: "pass", asks: 2,
want: []string{"passed its check. Approve merges it into main at " + readyHead[:12] + ";"}, not: []string{"before its check ends"}},
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 2,
head: "Check failed \u2014 merge into main: app#7 Fix it",
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
} {
t.Run(row.name, func(t *testing.T) {
v := newWorld(t)
if !row.gated {
ungated(v)
}
v.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Title = "Fix it"
@@ -158,47 +144,6 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
}
})
}
// check-gates-merge false (the operator, 2026-10-25 22:56): a reviewed pull request is asked with no verdict, another
// merge moving its trunk puts it out of nothing, a failed verdict is shown on it and holds nothing, and the gate's
// status and show say the mode.
u := newWorld(t)
ungated(u)
u.op.auto = false
const other = "3333333333333333333333333333333333333333"
u.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
u.h.PullUpdated(pr("novox/app", 8, other, "feat/y", "src/b.go"))
u.settleAll()
if u.op.count() == 0 || u.state(IDOf("novox/app", readyHead)) != Ready {
t.Fatalf("ungated, two reviewed pull requests with no verdict: %d asks, %s; wanted ready and asked at once", u.op.count(), u.state(IDOf("novox/app", readyHead)))
}
if gates, _ := u.h.Status("listening")["check-gates-merge"].(bool); gates {
t.Fatalf("delivery_status does not say the check gates no merge: %v", u.h.Status("listening"))
}
shown, _ := u.h.Show(IDOf("novox/app", other))
if gates, _ := shown.(map[string]any)["check-gates-merge"].(bool); gates {
t.Fatalf("show does not say the check gates no merge: %v", shown)
}
u.h.Checked(judgedAt("novox/app", 8, other, "fail", "t1t1t1t1t1t1"))
checkerSays(u, "feat/y", "fail", "TestB", headOf("novox/app", 8, other))
u.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
u.settleAll()
if u.op.count() == 0 {
t.Fatalf("ungated, a failed verdict holds the ask (the check gates no merge): %d asks", u.op.count())
}
d = u.delivery(IDOf("novox/app", other))
if d.State != Ready || d.Check == nil || d.Check.Passes() {
t.Fatalf("ungated, after a failed verdict and a trunk's move: %s, verdict %+v", d.State, d.Check)
}
for _, a := range u.op.open() {
if strings.Contains(a, "feat/y") {
u.op.answer(a, OptionApprove)
}
}
u.settleAll()
if len(u.forge.merges) != 1 || u.forge.merges[0] != "novox/app#8@"+other {
t.Fatalf("ungated, the approved head is merged %v; wanted once, with no verdict waited on", u.forge.merges)
}
}
// 1144/AC3: a refused merge is not asked again by any timer; only an event (here, mergeAgainNow called directly, as a
@@ -342,57 +287,6 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
if w.op.count() != 1 {
t.Fatalf("a second Approve was asked: %d", w.op.count())
}
// check-gates-merge false: the group is asked once with no verdict and no composed check, and merges whole in one pass.
u := newWorld(t)
ungated(u)
u.op.auto = false
u.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
u.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
u.settleAll()
group := []mdc.Head{headOf("novox/mesh-controller", 1, ctlHead), headOf("novox/mesh-catalog", 2, catHead)}
checkerSays(u, "feat/x", "running", "", group...)
u.settleAll()
// Issue 1966 (the operator, 2026-11-07): the group is asked at once, its check running or not; nothing waits on it.
if d := u.delivery(IDOf("novox/mesh-catalog", catHead)); u.op.count() != 1 || d.Ready == nil ||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
t.Fatalf("ungated, the group is asked %d times while its check runs; wanted once at once, and the wait said: %+v", u.op.count(), d.Ready)
}
checkerSays(u, "feat/x", "pass", "", group...)
u.settleAll()
if u.op.count() != 2 {
t.Fatalf("ungated, the group is asked %d times once its check passed; wanted once at once and once again in its words", u.op.count())
}
if e := u.op.ask(u.op.open()[0]).Explanation; !strings.Contains(e, "2 pull requests, passed its check. Approve merges all of them into main in one pass, at the heads named;") {
t.Fatalf("ungated, the group's ask does not say its check passed:\n%s", e)
}
// A member re-pushed hours after its sibling became ready: with no verdict at the new heads nothing is asked, however
// long the sibling has waited.
r := newWorld(t)
ungated(r)
r.op.auto = false
const ctlHead2 = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
r.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
r.settleAll()
r.later(5 * time.Hour)
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead2, "feat/x", "internal/a.go"))
// Made ready by a handler, before the look gives it a run: no verdict and no status at the new heads.
r.h.mu.Lock()
r.h.ungate(r.h.deliveries[IDOf("novox/mesh-controller", ctlHead2)], r.h.Now())
r.h.mu.Unlock()
r.settleAll()
// Issue 1966: no verdict is waited on, so the new heads are asked again at once, never held for a check.
if d := r.delivery(IDOf("novox/mesh-catalog", catHead)); r.op.count() == 0 || d.Ready == nil ||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
t.Fatalf("a member re-pushed hours later: asked %d times, wait %+v; wanted asked again at once", r.op.count(), d.Ready)
}
u.op.answer(u.op.open()[0], OptionApprove)
u.settleAll()
if len(u.forge.merges) != 2 {
t.Fatalf("ungated, both members merge in one pass: %v", u.forge.merges)
}
}
// **A merge into another branch than main is its author's** (novox/hq ADR 0398 §4, issue 948): a green pull request
@@ -25,10 +25,6 @@ const (
type Settings struct {
RefusalFloorMinutes *int `json:"refusal-stall-floor-minutes"`
AuthorBoundMinutes *int `json:"author-new-head-minutes"`
// CheckGatesMerge is whether mesh/check gates the merge (the default, true). False (the operator, 2026-10-25 22:56:
// "run the check locally only, but not as a gate, it loses us too much time"): a reviewed pull request is ready and its
// Approve asked without a verdict, a trunk's move puts nothing out of date, and a verdict is shown, never waited on.
CheckGatesMerge *bool `json:"check-gates-merge"`
// RecordRetentionDays is how many days a delivery record is kept from when it opened (design 47 *The delivery
// record*); nil keeps DefaultRecordRetention.
RecordRetentionDays *int `json:"record-retention"`
@@ -73,28 +69,13 @@ func readSettings(path string) (Settings, error) {
default:
return Settings{}, errors.New("record-retention is not a number")
}
switch v := loose["check-gates-merge"].(type) {
case nil:
case bool:
s.CheckGatesMerge = &v
default:
return Settings{}, errors.New("check-gates-merge is not true or false")
}
return s, nil
}
// checkGates is whether mesh/check gates the merge: true unless the setting check-gates-merge is false.
func (h *Holder) checkGates() bool {
if h.Settings == nil {
return true
}
v := h.Settings().CheckGatesMerge
return v == nil || *v
}
// factsAt are the facts a settle reads at now: the moment, and whether the check gates the merge.
// factsAt are the facts a settle reads at now: the moment. No setting switches whether a check gates a merge: the check
// gates the ask, and the merge judges the operator's Approve alone (novox/hq ADR 0661 §2, §3).
func (h *Holder) factsAt(now time.Time) Facts {
return Facts{Now: now, Ungated: !h.checkGates()}
return Facts{Now: now}
}
// minutesOr is a setting's minutes, or the default when it gives none.
@@ -94,9 +94,6 @@ type Facts struct {
Now time.Time
By string
Why string
// Ungated is the setting check-gates-merge false: mesh/check gates no merge, so a proposed delivery is accepted with no
// verdict, none is refused for one, and a trunk's move puts none back to proposed.
Ungated bool
}
// Row is one transition the table holds.
@@ -145,10 +142,10 @@ var Table = []Row{
// cannot loop with `trunk moved`; a recheck or the merge tool's ask judges it again.
{From: []State{Proposed}, Event: EvAccepted, To: Ready,
Guard: "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and " +
"the repository's own check did not fail; or the check gates no merge (check-gates-merge false)",
"the repository's own check did not fail; or the operator's Approve covers this head (the merge judges it alone)",
Holds: func(d *Delivery, f Facts) error {
if f.Ungated {
return nil
if d.approvedAtHead() {
return nil // novox/hq ADR 0661 §3: an Approve given stands across a trunk move, and the merge judges it alone
}
if d.Check == nil {
return errors.New("no verdict")
@@ -160,10 +157,11 @@ var Table = []Row{
}},
{From: []State{Proposed}, Event: EvRefused, To: Rejected,
Guard: "a verdict names this commit and the trunk's head as its trunk commit, and the gate failed or could not run, " +
"or the repository's own check failed; never while the check gates no merge",
"or the repository's own check failed; never once the operator's Approve covers this head",
Holds: func(d *Delivery, f Facts) error {
if f.Ungated {
return errors.New("the check gates no merge (check-gates-merge false)")
if d.approvedAtHead() {
// novox/hq ADR 0661 §5: a failure after the Approve is said, and does not undo it.
return errors.New("the operator's Approve covers this head: a failure is said, the merge is not refused")
}
if d.Check == nil {
return errors.New("no verdict")
@@ -176,10 +174,11 @@ var Table = []Row{
// **A ready delivery whose verdict went out of date goes back to proposed** (novox/hq ADR 0323 §7): another
// merge moved its trunk. A rejected one stays rejected, its status on the forge saying the verdict is out of date.
{From: []State{Ready}, Event: EvTrunkMoved, To: Proposed,
Guard: "its verdict names a trunk commit that is not its trunk's head, and the check gates the merge",
Guard: "its verdict names a trunk commit that is not its trunk's head, and no Approve covers this head yet",
Holds: func(d *Delivery, f Facts) error {
if f.Ungated {
return errors.New("the check gates no merge (check-gates-merge false)")
if d.approvedAtHead() {
// novox/hq ADR 0661 §3: an approved merge waits for no carry; its carry runs after the merge.
return errors.New("the operator's Approve covers this head: it merges onto the new trunk, its carry after")
}
return unless(d.MergedAs == "" && d.outOfDate() != "", "not merged, and its verdict is not out of date")
},
@@ -161,21 +161,34 @@ func TestTheTableKeepsItsRules(t *testing.T) {
if _, err := Apply(d0, EvReask, true, Facts{Now: time.Now(), By: byH2, Why: "stalled"}); err == nil {
t.Fatal("a check re-asked once was re-asked again")
}
// While the check gates no merge (check-gates-merge false): a proposed delivery is accepted with no verdict or a failed
// one, none is refused, and a trunk's move puts no ready one back to proposed.
off := Facts{Now: time.Now(), Ungated: true}
// Once the operator's Approve covers the head (novox/hq ADR 0661 §3, §5): a proposed delivery is accepted with no
// verdict or a failed one, none is refused, and a trunk's move puts no ready one back to proposed.
at := time.Now()
approved := func(d *Delivery) *Delivery {
d.Ready = &ReadyRun{Approved: &at, Heads: map[string]string{d.ID: d.Commit}}
return d
}
f := Facts{Now: at}
for _, check := range []*Verdict{nil, {Gate: "fail"}} {
p := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, Check: check}
if _, err := Apply(p, EvRefused, false, off); err == nil {
t.Fatalf("ungated, a verdict %+v refused the delivery", check)
p := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check})
if _, err := Apply(p, EvRefused, false, f); err == nil {
t.Fatalf("approved, a verdict %+v refused the delivery", check)
}
if _, err := Apply(p, EvAccepted, false, off); err != nil || p.State != Ready {
t.Fatalf("ungated, a delivery with the verdict %+v is %s: %v", check, p.State, err)
if _, err := Apply(p, EvAccepted, false, f); err != nil || p.State != Ready {
t.Fatalf("approved, a delivery with the verdict %+v is %s: %v", check, p.State, err)
}
unapproved := &Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check}
if _, err := Apply(unapproved, EvAccepted, false, f); err == nil {
t.Fatalf("with no Approve, a delivery with the verdict %+v was accepted: the check gates the ask", check)
}
}
stale := &Delivery{ID: "novox/app@c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"}
if _, err := Apply(stale, EvTrunkMoved, false, off); err == nil {
t.Fatal("ungated, a trunk's move put a ready delivery back to proposed")
stale := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"})
if _, err := Apply(stale, EvTrunkMoved, false, f); err == nil {
t.Fatal("approved, a trunk's move put a ready delivery back to proposed")
}
stale.Ready = nil
if _, err := Apply(stale, EvTrunkMoved, false, f); err != nil || stale.State != Proposed {
t.Fatalf("with no Approve, a trunk's move left a ready delivery %s: %v", stale.State, err)
}
// A ready delivery off the trunk is refused its merge, whatever else is true of it.
d := &Delivery{ID: "novox/app@c0", State: Ready}
@@ -1,461 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
checker "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// **One check per delivery** (novox/hq ADR 0548 §2 to §4, issue 568). While a check runs, nothing merges into a repository it
// judges, and an approved merge holds a turn on its repositories so no new check starts there:
//
// - the hold (§2): an approved merge is handed to the forge bridge only when no verdict of the checker's verdicts bucket in
// the state running names, among its trunk heads, a repository the merge goes into; the verdict of the merge's own heads
// excepted. The gate watches that bucket already (onestatus.go), so the verdict leaving running gives the waiting merge its
// turn at once, with no timer. Siblings a check reads beside are not held (§2). Only while mesh/check gates the merge;
// - the turn (§3): before it reads the verdicts for a merge, the gate writes one key per repository the merge goes into in its
// bucket turns (`<owner>.<name>`, mdc.TurnsKey): the deliveries that wait there in the order of their Approve, the check
// each waits for and since when; for a group on every member's repository until its last member merged. The checker reads
// the bucket and claims no check of a repository a turn names. The key goes when the last delivery in it merged or left;
// - the bound and the tools (§4): a merge held past turnBound is a stalled line; delivery_turns reads every turn and
// delivery_hand_now hands a waiting merge at once on the operator's word, the check it waited for then carried.
// turnBound is how long an approved merge may wait for another delivery's check before it is a stalled line (ADR 0548 §4).
const turnBound = 30 * time.Minute
// TurnStore is the gate's bucket turns: one key per repository, written by the gate alone.
type TurnStore interface {
// All reads every turn, by repository (lowercase).
All() (map[string]Turn, error)
Put(t Turn) error
Delete(repository string) error
}
// kvTurns is the bucket turns over the runtime.
type kvTurns struct{}
func (kvTurns) All() (map[string]Turn, error) {
keys, err := stdio.State("turns").Keys()
if err != nil {
return nil, err
}
out := map[string]Turn{}
for _, k := range keys {
e, err := stdio.State("turns").Get(k)
if err != nil {
return nil, err
}
if e == nil {
continue
}
repository, err := mdc.ParseTurnsKey(k)
if err != nil {
continue // not a turn's key: never the gate's, left
}
t, err := DecodeTurn(e.Value)
if err != nil {
t = Turn{Repository: repository} // unreadable: rewritten or deleted at the next look
}
out[repository] = t
}
return out, nil
}
func (kvTurns) Put(t Turn) error {
key, err := mdc.TurnsKey(t.Repository)
if err != nil {
return err
}
raw, err := EncodeTurn(t)
if err != nil {
return err
}
_, err = stdio.State("turns").Put(key, json.RawMessage(raw))
return err
}
func (kvTurns) Delete(repository string) error {
key, err := mdc.TurnsKey(repository)
if err != nil {
return err
}
return stdio.State("turns").Delete(key)
}
// approvedMerge is an approved merge not yet done: the run holding its Approve, the deliveries it covers, the hash of their
// heads as the checker keys their verdict (lowercase, onestatus.go headsOf), and the repositories it goes into (lowercase).
type approvedMerge struct {
owner *Delivery
run *ReadyRun
group string
heads string
repos []string
}
// approvedMerges are the merges the operator approved that are not done, in the order of their Approve: each ask once, its
// run on the delivery that asked it. A declined, refused, merged, rejected or final one has left. Under the lock.
func (h *Holder) approvedMerges() []approvedMerge {
var out []approvedMerge
seen := map[string]bool{}
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids) // the same owner of an ask at every look, whichever member's run holds it
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.State.Final() || d.State == Rejected || d.Base != MergeBranch || r.RefusedSince != nil || h.allMerged(r.Heads) {
continue
}
seen[r.AskID] = true
var heads []mdc.Head
repos := map[string]bool{}
for id, sha := range r.Heads {
m := h.deliveries[id]
if m == nil {
continue
}
heads = append(heads, mdc.Head{Repository: strings.ToLower(m.Repository), Number: m.Number, Head: strings.ToLower(sha)})
repos[strings.ToLower(m.Repository)] = true
}
hash, err := mdc.HeadsHash(heads)
if err != nil {
continue
}
am := approvedMerge{owner: d, run: r, group: askGroup(d), heads: hash}
for repo := range repos {
am.repos = append(am.repos, repo)
}
sort.Strings(am.repos)
out = append(out, am)
}
sort.SliceStable(out, func(i, j int) bool {
if !out[i].run.Approved.Equal(*out[j].run.Approved) {
return out[i].run.Approved.Before(*out[j].run.Approved)
}
return out[i].heads < out[j].heads
})
return out
}
// mergeOfDelivery is the approved merge covering d, if one is approved.
func (h *Holder) mergeOfDelivery(d *Delivery) (approvedMerge, bool) {
c := h.covering(d)
if c == nil || c.Approved == nil {
return approvedMerge{}, false
}
for _, am := range h.approvedMerges() {
// By the ask, not the run: a group's members may each hold a run of the same ask, and the merge is the ask's.
if am.run.AskID == c.AskID {
return am, true
}
}
return approvedMerge{}, false
}
// runningCheck is a check of the checker in the state running that names repo among its trunk heads, its heads not own: its
// verdict key and since when the gate has seen it run. False when none runs there. Under the lock.
func (h *Holder) runningCheck(repo, own string) (key string, v checkerVerdict, since time.Time, ok bool) {
hashes := make([]string, 0, len(h.checker))
for hash := range h.checker {
hashes = append(hashes, hash)
}
sort.Strings(hashes)
for _, hash := range hashes {
v := h.checker[hash]
if v.State != checkerRunning || v.Heads == own {
continue
}
for r := range v.Trunk {
if strings.EqualFold(r, repo) {
key, err := checker.VerdictKey(v.Group, v.Heads)
if err != nil {
key = v.Group + "." + v.Heads
}
return key, v, h.runningSince[v.Heads], true
}
}
}
return "", checkerVerdict{}, time.Time{}, false
}
// turnWaits is what an approved merge waits for before it is handed (ADR 0548 §2, §3): its turns written, then the end of
// every check of other heads running on a repository it goes into. Empty when it may be handed. It keeps on the run since
// when it is held and for which check, for the stalled line and the tools. Under the lock.
func (h *Holder) turnWaits(d *Delivery, now time.Time) string {
am, ok := h.mergeOfDelivery(d)
if !ok {
return ""
}
r := am.run
if h.Turns != nil {
if !h.turnsLoaded {
return "approved; its merge turn waits for the turns the gate wrote before to be read"
}
for _, repo := range am.repos {
if !turnNames(h.turnsHeld[repo], am.heads) {
return "approved; its merge turn on " + repo + " is being written, so no check starts there before it merges"
}
}
}
if r.HandNow != "" {
if r.HeldSince != nil {
r.HeldSince, r.HeldFor = nil, ""
h.keep(am.owner)
}
return "" // handed now on the operator's word: the check it waited for is carried after the merge
}
for _, repo := range am.repos {
key, v, since, running := h.runningCheck(repo, am.heads)
if !running {
continue
}
if r.HeldSince == nil || r.HeldFor != key {
at := now
r.HeldSince, r.HeldFor = &at, key
h.keep(am.owner)
h.Logf("[mesh-delivery] %s is approved and held: the check %s runs on %s, and nothing merges there while it does (ADR 0548 §2)", am.owner.ID, key, repo)
}
words := fmt.Sprintf("approved; waits for the check of %s (heads %s) on %s", v.Group, v.Heads, repo)
if !since.IsZero() {
words += ", running since " + since.UTC().Format(time.RFC3339)
}
return words + " (novox/hq ADR 0548 §2)"
}
if r.HeldSince != nil {
r.HeldSince, r.HeldFor = nil, ""
h.keep(am.owner)
}
return ""
}
// turnNames says whether a turn names the heads.
func turnNames(t Turn, heads string) bool {
for _, w := range t.Waiting {
if w.Heads == heads {
return true
}
}
return false
}
// wantedTurns are the turns the approved merges hold now, by repository: each merge on every repository it goes into, in
// the order of their Approve, with the check of other heads it waits for there. Under the lock.
func (h *Holder) wantedTurns() map[string]Turn {
out := map[string]Turn{}
for _, am := range h.approvedMerges() {
for _, repo := range am.repos {
t := out[repo]
t.Repository = repo
w := TurnWait{Group: am.group, Heads: am.heads, Ask: am.run.AskID, Approved: am.run.Approved.UTC(), Since: am.run.Approved.UTC()}
if key, _, since, ok := h.runningCheck(repo, am.heads); ok {
w.WaitsFor = key
if !since.IsZero() {
w.WaitsSince = since.UTC()
}
}
t.Waiting = append(t.Waiting, w)
out[repo] = t
}
}
return out
}
// turnWrites are the writes that bring the bucket turns to what the approved merges hold: a turn written where it changed,
// a key deleted where its last delivery merged or left. The first look reads what a gate before this one wrote. Under the
// lock; the writes run outside it, and each that lands is kept and asks for a look.
func (h *Holder) turnWrites() []func() {
if h.Turns == nil {
return nil
}
if !h.turnsLoaded {
return []func(){func() {
held, err := h.Turns.All()
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] the merge turns written before cannot be read, so no merge is handed yet: %v; asked again in %s", err, askAgainAfter)
h.again("the merge turns written before, read")
return
}
h.turnsHeld, h.turnsLoaded = held, true
h.Poke()
}}
}
want := map[string]Turn{}
if h.checkGates() {
want = h.wantedTurns() // while the check gates no merge, no merge waits for one, and no turn holds a check
}
var out []func()
repos := make([]string, 0, len(want)+len(h.turnsHeld))
for repo := range want {
repos = append(repos, repo)
}
for repo := range h.turnsHeld {
if _, ok := want[repo]; !ok {
repos = append(repos, repo)
}
}
sort.Strings(repos)
for _, repo := range repos {
t, wanted := want[repo]
held, had := h.turnsHeld[repo]
switch {
case wanted && had && sameTurnValue(held, t):
case wanted:
out = append(out, func() {
err := h.Turns.Put(t)
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] the merge turn on %s cannot be written: %v; asked again in %s", repo, err, askAgainAfter)
h.again("a merge turn, written")
return
}
h.turnsHeld[repo] = t
h.Poke()
})
default:
out = append(out, func() {
err := h.Turns.Delete(repo)
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] the merge turn on %s cannot be deleted: %v; asked again in %s", repo, err, askAgainAfter)
h.again("a merge turn, deleted")
return
}
delete(h.turnsHeld, repo)
h.Logf("[mesh-delivery] the merge turn on %s is deleted: no approved merge waits on it, so the checker may start a check there", repo)
h.Poke()
})
}
}
return out
}
// sameTurnValue says whether two turns hold the same value, the version aside.
func sameTurnValue(a, b Turn) bool {
a.V, b.V = 0, 0
return SameTurn(a, b)
}
// turnLines are the stalled lines of approved merges held past turnBound by another delivery's check (ADR 0548 §4). Under the
// lock.
func (h *Holder) turnLines(now time.Time) []StalledLine {
var out []StalledLine
for _, d := range h.deliveries {
r := d.Ready
if r == nil || r.HeldSince == nil || r.Merged != nil || d.MergedAs != "" || d.State.Final() || now.Sub(*r.HeldSince) < turnBound {
continue
}
words := r.Waits
if words == "" {
words = "approved; waits for the check " + r.HeldFor
}
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(*r.HeldSince).Round(time.Second).String(),
Bound: turnBound.String(),
H2: "none: the check it waits for ends, or the operator hands the merge now with delivery_hand_now — the operator's",
Says: fmt.Sprintf("%s#%d at %s is %s, held since %s; the step that check is at is read with the checker's checker_status",
d.Repository, d.Number, shortOf(r.Head, 12), words, r.HeldSince.UTC().Format(time.RFC3339))})
}
return out
}
// TurnLine is one repository's turn as delivery_turns reads it: the turn as the gate holds it and, for each delivery waiting,
// the check it waits for in words.
type TurnLine struct {
Repository string `json:"repository"`
Turn Turn `json:"turn"`
Waits map[string]any `json:"waits,omitempty"`
}
// TurnsRead is the tool delivery_turns: every turn the gate holds, with the check each waiting delivery waits for, since when,
// and whether the merge was handed now on the operator's word.
func (h *Holder) TurnsRead() map[string]any {
h.mu.Lock()
defer h.mu.Unlock()
want := map[string]Turn{}
if h.checkGates() {
want = h.wantedTurns() // as turnWrites: while the check gates no merge, no turn is written by design
}
repos := make([]string, 0, len(want))
for repo := range want {
repos = append(repos, repo)
}
sort.Strings(repos)
lines := []TurnLine{}
for _, repo := range repos {
l := TurnLine{Repository: repo, Turn: want[repo], Waits: map[string]any{}}
for _, w := range want[repo].Waiting {
says := "nothing: no check of other heads runs on " + repo
if w.WaitsFor != "" {
says = "the check " + w.WaitsFor
if !w.WaitsSince.IsZero() {
says += ", running since " + w.WaitsSince.Format(time.RFC3339)
}
}
l.Waits[w.Group] = says
}
lines = append(lines, l)
}
out := map[string]any{"turns": lines, "check-gates-merge": h.checkGates(), "bound": turnBound.String()}
if !h.checkGates() {
out["why-none"] = "the check gates no merge (check-gates-merge false): no merge waits for a check, so no turn is written"
}
if h.Turns != nil {
out["written"] = h.turnsLoaded
var unwritten []string
for _, repo := range repos {
if held, ok := h.turnsHeld[repo]; !ok || !sameTurnValue(held, want[repo]) {
unwritten = append(unwritten, repo)
}
}
for repo := range h.turnsHeld {
if _, ok := want[repo]; !ok {
unwritten = append(unwritten, repo+" (to delete)")
}
}
sort.Strings(unwritten)
if len(unwritten) > 0 {
out["not-yet-written"] = unwritten
}
}
return out
}
// HandNow is the tool delivery_hand_now (ADR 0548 §4): on the operator's word an approved merge held for another delivery's
// check is handed at once; that check runs on and is carried to the new trunk after the merge. Refused for a delivery that is
// not an approved merge waiting, naming why.
func (h *Holder) HandNow(id, why string) (string, error) {
h.mu.Lock()
defer h.mu.Unlock()
d := h.deliveries[id]
if d == nil {
return "", fmt.Errorf("no delivery %q", id)
}
if strings.TrimSpace(why) == "" {
return "", fmt.Errorf("say why: the hand is the operator's word, kept on the delivery")
}
am, ok := h.mergeOfDelivery(d)
if !ok {
return "", fmt.Errorf("%s is not an approved merge waiting: nothing to hand (its state is %s)", id, d.State)
}
if am.run.HeldSince == nil {
return "", fmt.Errorf("%s waits for no check of another delivery: %s", id, firstOf(am.run.Waits, "it is handed at its turn"))
}
am.run.HandNow = why
h.keep(am.owner)
h.mergeAgainNow(d)
h.Logf("[mesh-delivery] %s is handed now on the operator's word, past the check %s it waited for, which is carried after the merge: %s",
id, am.run.HeldFor, why)
return fmt.Sprintf("%s is handed to the merge at the next look; the check %s it waited for runs on and is carried to the new trunk", id, am.run.HeldFor), nil
}
@@ -3,7 +3,6 @@ package main
import (
"encoding/json"
"reflect"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
@@ -137,63 +136,3 @@ func SameAsk(a, b Ask) bool {
a.Ask, b.Ask = nil, nil
return reflect.DeepEqual(a, b) && reflect.DeepEqual(x, y)
}
// Turn is the state turns' value: the approved deliveries waiting to merge in one repository, in the order of the Approve.
type Turn struct {
V int
Repository string
Waiting []TurnWait
}
// TurnWait is one approved delivery in a turn; WaitsFor, WaitsSince and Step say the check of another delivery it waits on.
type TurnWait struct {
Group string
Heads string
Ask string
Approved time.Time
Since time.Time
WaitsFor string
WaitsSince time.Time
Step string
}
func (t Turn) stored() seat.TurnsByRepositoryValue {
out := seat.TurnsByRepositoryValue{Repository: t.Repository, V: int64(t.V), Waiting: []seat.TurnsByRepositoryValueWaitingItem{}}
for _, w := range t.Waiting {
item := seat.TurnsByRepositoryValueWaitingItem{Approved: w.Approved, Ask: w.Ask, Group: w.Group, Heads: w.Heads,
Since: w.Since, Step: w.Step, WaitsFor: w.WaitsFor}
if !w.WaitsSince.IsZero() {
since := w.WaitsSince
item.WaitsSince = &since
}
out.Waiting = append(out.Waiting, item)
}
return out
}
func turnFromSeat(s seat.TurnsByRepositoryValue) Turn {
out := Turn{V: int(s.V), Repository: s.Repository}
for _, w := range s.Waiting {
item := TurnWait{Group: w.Group, Heads: w.Heads, Ask: w.Ask, Approved: w.Approved, Since: w.Since, WaitsFor: w.WaitsFor, Step: w.Step}
if w.WaitsSince != nil {
item.WaitsSince = *w.WaitsSince
}
out.Waiting = append(out.Waiting, item)
}
return out
}
// EncodeTurn is t as the value of a turns key.
func EncodeTurn(t Turn) ([]byte, error) { return mdc.EncodeTurn(t.stored()) }
// DecodeTurn reads the value of a turns key.
func DecodeTurn(data []byte) (Turn, error) {
s, err := mdc.DecodeTurn(data)
if err != nil {
return Turn{}, err
}
return turnFromSeat(s), nil
}
// SameTurn says whether two turns are the same value.
func SameTurn(a, b Turn) bool { return mdc.SameTurn(a.stored(), b.stored()) }
@@ -4,7 +4,6 @@ import (
"encoding/json"
"reflect"
"testing"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
@@ -12,8 +11,6 @@ import (
// What go/delivery wrote into the seat's states, key order and all, must decode as it did and encode to the same
// content (ADR 0634 §2): the gate's working values are converted to the seat's types at the edge.
func TestStoredValuesOfTheEarlierLayoutDecodeUnchanged(t *testing.T) {
approved := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
since := time.Date(2026, 11, 5, 10, 5, 0, 0, time.UTC)
head := mdc.Head{Repository: "novox/app", Number: 7, Head: "0123456789abcdef0123456789abcdef01234567"}
rows := []struct {
name string
@@ -32,13 +29,6 @@ func TestStoredValuesOfTheEarlierLayoutDecodeUnchanged(t *testing.T) {
{"ask keeps the router's ask as bytes", `{"v":1,"group":"feat/x","heads":[{"repository":"novox/app","number":7,"head":"` + head.Head + `"}],"ask":{"id":"a","options":["Approve"]},"asking":2}`,
func(s string) (any, error) { return DecodeAsk([]byte(s)) }, func(v any) ([]byte, error) { return EncodeAsk(v.(Ask)) },
Ask{V: 1, Group: "feat/x", Heads: []mdc.Head{head}, Ask: json.RawMessage(`{"id":"a","options":["Approve"]}`), Asking: 2}},
{"turn without and with a check waited for", `{"v":1,"repository":"novox/app","waiting":[` +
`{"group":"feat/x","heads":"0123456789ab","ask":"merge.x","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:00:00Z"},` +
`{"group":"feat/y","heads":"ba9876543210","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:00:00Z","waits-for":"feat/x.0123456789ab","waits-since":"2026-11-05T10:05:00Z","step":"unit"}]}`,
func(s string) (any, error) { return DecodeTurn([]byte(s)) }, func(v any) ([]byte, error) { return EncodeTurn(v.(Turn)) },
Turn{V: 1, Repository: "novox/app", Waiting: []TurnWait{
{Group: "feat/x", Heads: "0123456789ab", Ask: "merge.x", Approved: approved, Since: approved},
{Group: "feat/y", Heads: "ba9876543210", Approved: approved, Since: approved, WaitsFor: "feat/x.0123456789ab", WaitsSince: since, Step: "unit"}}}},
{"source", `{"v":1,"group":"feat/x","heads":{"novox/app":"c0ffee"}}`,
func(s string) (any, error) { return mdc.DecodeSource([]byte(s)) }, func(v any) ([]byte, error) { return mdc.EncodeSource(v.(Source)) },
Source{V: 1, Group: "feat/x", Heads: map[string]string{"novox/app": "c0ffee"}}},
@@ -74,8 +64,5 @@ func sameValue(a, b any) bool {
if x, ok := a.(Ask); ok {
return SameAsk(x, b.(Ask))
}
if x, ok := a.(Turn); ok {
return SameTurn(x, b.(Turn)) && reflect.DeepEqual(len(x.Waiting), len(b.(Turn).Waiting)) && x.Waiting[len(x.Waiting)-1].WaitsSince.Equal(b.(Turn).Waiting[len(x.Waiting)-1].WaitsSince)
}
return reflect.DeepEqual(a, b)
}
@@ -84,7 +84,7 @@ func (h *Holder) Show(id string) (any, error) {
h.mu.Lock()
defer h.mu.Unlock()
if d := h.deliveries[id]; d != nil {
out := map[string]any{"delivery": d, "waits": h.waitsFor(d), "check-gates-merge": h.checkGates()}
out := map[string]any{"delivery": d, "waits": h.waitsFor(d)}
if m := h.machines(d); m != "" {
out["machines"] = m
}
@@ -97,7 +97,7 @@ func (h *Holder) Show(id string) (any, error) {
if g := h.groups[id]; g != nil {
members := h.membersOf(g)
state, why := GroupState(g, members)
return map[string]any{"group": g, "state": state, "why": why, "members": members, "check-gates-merge": h.checkGates()}, nil
return map[string]any{"group": g, "state": state, "why": why, "members": members}, nil
}
return nil, fmt.Errorf("no delivery or group %q: `deliveries` lists them", id)
}
@@ -246,7 +246,6 @@ func (h *Holder) Stalled() []StalledLine {
}
out = append(out, h.withChecks(h.uncheckedLines(now))...)
out = append(out, h.readyLines(now)...)
out = append(out, h.turnLines(now)...) // an approved merge held past its bound by another delivery's check (ADR 0548 §4)
out = append(out, h.groupLines(now)...)
out = append(out, h.owedLines(now)...) // an act owed the forge with no outcome past its bound (W11.17)
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
@@ -316,7 +315,7 @@ func (h *Holder) Status(listening string) map[string]any {
byState[string(s)] = n
}
out := map[string]any{"deliveries": byState, "groups": len(h.groups), "owed": owed, "unkept": len(h.dirty),
"events": listening, "started": h.started.Format(time.RFC3339), "check-gates-merge": h.checkGates()}
"events": listening, "started": h.started.Format(time.RFC3339)}
// The owed effects tried since this owner started (novox/hq issue 407): done, failed and to be tried again,
// or given up, and when the last was tried. A count since the start, said with that start: two readings in
// one run give a window's tries.
@@ -1,6 +1,5 @@
{
"refusal-stall-floor-minutes": 10,
"author-new-head-minutes": 120,
"check-gates-merge": true,
"record-retention": 90
}
+1 -14
View File
@@ -316,11 +316,7 @@
"name": "source",
"history": 64
},
"conditions",
{
"name": "turns",
"history": 5
}
"conditions"
],
"reads": [
"mesh-delivery-teller.answers",
@@ -328,10 +324,8 @@
"mesh-delivery-view.reach"
],
"tools": [
"delivery_hand_now",
"delivery_merge_again",
"delivery_status",
"delivery_turns",
"gate_consumer",
"gate_record",
"gate_record_end",
@@ -354,12 +348,6 @@
"trusted": false,
"default": 120
},
"check-gates-merge": {
"type": "boolean",
"description": "Whether mesh/check gates the merge. True (the default): the operator's Approve is asked, and a merge handed, only on a passing verdict judged against the trunk's head, and a trunk's move puts a verdict out of date (ADR 0447, ADR 0505 §4). False (the operator, 2026-10-25 22:56: \"run the check locally only, but not as a gate, it loses us too much time\"; developers run the unit and integration levels locally before a pull request): a ready pull request (no review line is waited for, ADR 0659) has its Approve is asked once the check at its heads (a group: of all its heads together) has a verdict, a pass or a failure said in the ask, or after 60 minutes with none, said so (the operator, 2026-10-27 23:54: \"if you don't want me to approve it, don't send the approval request\"); an approved merge waits for no verdict; no merge is refused for a check judged on an older trunk; a later verdict is shown on the pull request and the delivery, and asks a pending Approve again in its words; a group still merges all its members in one pass. Set it false together with the gitea module's check-gates-merge, which lets its merge tool merge an approved head without a passing mesh/check.",
"trusted": false,
"default": true
},
"record-retention": {
"type": "number",
"description": "days a delivery record is kept from when it opened (design 47 *The delivery record*): ninety covers every trace asked of a past delivery so far; when a record is removed at its retention, the gate says freed for each graph revision it pinned. Nothing else is removed to make room.",
@@ -385,7 +373,6 @@
"takes-config": [
"refusal-stall-floor-minutes",
"author-new-head-minutes",
"check-gates-merge",
"record-retention"
],
"merge": "json",