Name issues not closed first, and say a quiet worker before a decision, as ADR 0285 says
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues, messenger → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)

The router's order put a wontfix issue before an open one, and a stale
worker on an issue waiting for a decision said both; review of #191.
This commit is contained in:
jochen
2026-10-10 21:50:54 +02:00
parent 63dba87587
commit a7fc89ef04
3 changed files with 27 additions and 15 deletions
+1 -1
View File
@@ -604,7 +604,7 @@ func Tools(t *Tracker) []stdio.Tool {
}},
{Name: Seat + ".tracking",
Description: "Which issues track which conditions: for every condition key an issue links (link kind condition), " +
"or only the keys given, the issues that link it, those not resolved first, each with its status, " +
"or only the keys given, the issues that link it, those not closed (resolved or wontfix) first, each with its status, " +
"severity, what it needs, who works on it since when and whether they went quiet (stale), and where its " +
"fix is (fix: not-merged, merged, held, failed, unheard or delivered; empty when none is named). The " +
"operator's channel asks it to say in a condition's message which issue tracks it. Refused: a key not of a condition " +
+18 -11
View File
@@ -115,10 +115,12 @@ func (h *Holder) warmTracking() {
now := h.Now()
h.mu.Lock()
tried := h.track.tried
h.mu.Unlock()
if !tried.IsZero() && now.Sub(tried) >= 0 && now.Sub(tried) < TrackingAskEvery {
h.mu.Unlock()
return
}
h.track.tried = now // marked before asking, so two callers at once ask once
h.mu.Unlock()
got, err := h.askTracking()
h.mu.Lock()
defer h.mu.Unlock()
@@ -162,9 +164,10 @@ func withTracking(m Message, r *Record, what string, issues []TrackedIssue, know
return m
}
line, verdict := trackingWords(issues, known, now, loc)
explanation, since, _ := strings.Cut(m.Body, "\n")
if !strings.Contains(m.Body, "\n") {
explanation, since = "", m.Body
// since is the last line, which never holds a line break; the explanation is everything before it.
explanation, since := "", m.Body
if i := strings.LastIndex(m.Body, "\n"); i >= 0 {
explanation, since = m.Body[:i], m.Body[i+1:]
}
if rest, ok := strings.CutPrefix(explanation, verdictNothing); ok {
switch verdict {
@@ -186,12 +189,17 @@ func withTracking(m Message, r *Record, what string, issues []TrackedIssue, know
// needsAnswer is the "Needs you:" sentence for the issues that wait for the operator's answer.
func needsAnswer(issues []TrackedIssue) string {
var ns []string
var nums []uint64
for _, is := range issues {
if waitsForOperator(is) {
ns = append(ns, fmt.Sprint(is.Number))
nums = append(nums, is.Number)
}
}
sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
var ns []string
for _, n := range nums {
ns = append(ns, fmt.Sprint(n))
}
if len(ns) == 1 {
return "answer issue " + ns[0] + "."
}
@@ -209,7 +217,7 @@ func trackingWords(issues []TrackedIssue, known bool, now time.Time, loc *time.L
}
sorted := append([]TrackedIssue(nil), issues...)
sort.SliceStable(sorted, func(i, j int) bool {
ri, rj := sorted[i].Status == "resolved", sorted[j].Status == "resolved"
ri, rj := closed(sorted[i]), closed(sorted[j])
if ri != rj {
return !ri
}
@@ -307,14 +315,13 @@ func issueState(is TrackedIssue, now time.Time, loc *time.Location) string {
case waitsForOperator(is):
return "it waits for your answer"
}
s := "nobody is working on it"
if is.Worker != "" && is.Active != nil {
s = "nobody has worked on it since " + when(*is.Active, now, loc)
return "nobody has worked on it since " + when(*is.Active, now, loc)
}
if is.Needs == "decision" {
s += "; it waits for a decision first"
return "nobody is working on it; it waits for a decision first"
}
return s
return "nobody is working on it"
}
// workerWords is who@node as said: "fix-400 on g14".
@@ -8,7 +8,7 @@ import (
"time"
)
// The alert says which issue tracks its condition and where that issue stands (novox/hq issue 400, ADR 0285):
// A condition's message says which issue tracks it and where that issue stands (novox/hq issue 400, ADR 0285):
// one case per wording, each through the message as composed, and the content rule passes every line.
var trackNow = time.Date(2026, 10, 10, 19, 30, 0, 0, time.UTC)
@@ -29,7 +29,7 @@ func trackedMessage(issues []TrackedIssue, known bool) Message {
return withTracking(m, r, "quiet-reminder", issues, known, trackNow, time.UTC)
}
func TestAnAlertSaysWhereTheIssueThatTracksItStands(t *testing.T) {
func TestAConditionsMessageSaysWhereTheIssueThatTracksItStands(t *testing.T) {
cases := []struct {
name string
issues []TrackedIssue
@@ -65,6 +65,11 @@ func TestAnAlertSaysWhereTheIssueThatTracksItStands(t *testing.T) {
"Tracked as issue 386: it was resolved, yet this still happens.", "A person repaired"},
{"it will not be fixed", []TrackedIssue{{Number: 386, Status: "wontfix"}}, true,
"Tracked as issue 386: it will not be fixed.", "A person repaired"},
{"one not fixed after one open", []TrackedIssue{{Number: 100, Status: "wontfix"}, {Number: 200, Status: "open"}}, true,
"Tracked as issue 200: nobody is working on it. Also as issue 100: it will not be fixed.", "A person repaired"},
{"its worker went quiet on one that waits for a decision", []TrackedIssue{{Number: 400, Status: "fixing", Needs: "decision",
Worker: "fix-400@g14", Since: at(9, 0), Active: at(9, 30), Stale: true}}, true,
"Tracked as issue 400: nobody has worked on it since 09:30.", "A person repaired"},
{"it waits for the operator", []TrackedIssue{{Number: 336, Status: "open", Needs: "operator"}}, true,
"Tracked as issue 336: it waits for your answer.", "Needs you: answer issue 336. A person repaired"},
{"it waits for a decision", []TrackedIssue{{Number: 400, Status: "open", Needs: "decision"}}, true,
@@ -152,7 +157,7 @@ func TestAClearingASilenceAndANoticeSayNoIssue(t *testing.T) {
// Through the router: the tracker is asked outside the work, its word is kept, a word older than TrackingKept is
// not used, and a tracker that fails is said as not checked.
func TestTheRouterAsksTheTrackerAndSaysItsWordInTheAlert(t *testing.T) {
func TestTheRouterAsksTheTrackerAndSaysItsWordInTheMessage(t *testing.T) {
h, tg, dt, c, _ := newHolder(t)
h.Zone = func() *time.Location { return time.UTC }
asked := 0