Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/main.go
T
jochen f58840b8a6
mesh/merge-gate pass: builds claude-code, mesh-issues → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 o…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh-issues: check every new issue for a duplicate; claude-code: one judgement with no tools (hq ADR 0278)
The operator asked for duplicate issues to be consolidated as they are opened.
The tracker consumes its own opened event and asks the agent on its node, through
claude-code's new claude_code_judge, whether an older issue not resolved asks the
same question; it closes only the newer issue of a certain duplicate, keeping its
report on the older one, and fails loudly when it cannot judge. groom runs the
same judgement over the backlog as a dry run, applied on confirm.
2026-10-10 17:11:54 +02:00

500 lines
19 KiB
Go

// mesh-issues: the issue tracker, holder of the issue-tracker seat (novox/hq ADR 0275, to-be 53). A Go bundle
// the control-node's runtime launches. It hands out every issue's number from one counter advanced only by
// compare-and-set, keeps each issue in its declared state, refuses a change the development cycle would
// refuse, and says every change as an event. Built on the model of hits, a headless issue tracker on NATS;
// it carries none of its code. stdout is the MCP channel; what this module says, it says on stderr.
package main
import (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Seat is the role this module holds, once for the mesh.
const Seat = "issue-tracker"
// Verbs are the seat's verbs, as the manifest declares and claims them.
var Verbs = []string{"open", "get", "list", "move", "note", "link", "import", "groom"}
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, format+"\n", a...) }
// Settings are the operator's values for this module, merged by the mesh into one JSON file.
type Settings struct {
// FirstNumber is the number the first issue gets: read once for the counter, when it is started, and
// never again for it. The counter never starts below an issue already kept. The import reads it as the
// boundary between the folders hq numbered before the tracker and the issues the tracker numbered.
FirstNumber uint64 `json:"first-number"`
// Owners is the registered list `located-in` may name (to-be 53 §3).
Owners []string `json:"owners"`
// DuplicateCheck is "off" to stop the duplicate check on new issues; anything else is on (ADR 0278 §7).
DuplicateCheck string `json:"duplicate-check,omitempty"`
// JudgeModel is the model the duplicate check asks; DefaultJudgeModel when empty.
JudgeModel string `json:"judge-model,omitempty"`
}
func readSettings(path string) (Settings, error) {
var s Settings
if path == "" {
return s, errors.New("the module was started without a settings file")
}
raw, err := os.ReadFile(path)
if err != nil {
return s, err
}
if err := json.Unmarshal(raw, &s); err != nil {
return s, fmt.Errorf("the settings file is not the module's settings: %v", err)
}
var owners []string
for _, o := range s.Owners {
if o = strings.TrimSpace(o); o != "" {
owners = append(owners, o)
}
}
s.Owners = owners
return s, nil
}
func main() {
file := os.Getenv("MESH_ISSUES_SETTINGS")
t := &Tracker{
Store: runtimeStore{},
Emit: func(event string, body any) error { return stdio.Emit(event, body) },
Settings: func() (Settings, error) { return readSettings(file) },
Now: time.Now,
Logf: logf,
Sleep: time.Sleep,
Judge: busJudgement(os.Getenv("MESH_NODE")),
}
if len(os.Args) > 1 && os.Args[1] == "dry-run" {
os.Exit(dryRunAtTheTerminal(t, os.Args[2:]))
}
if _, err := t.Settings(); err != nil {
logf("[mesh-issues] the settings cannot be read (%v): every open and move will say so", err)
}
go hearOpened(t)
if err := stdio.Serve("", Tools(t)); err != nil {
logf("%v", err)
os.Exit(1)
}
}
// hearOpened subscribes the duplicate check to the tracker's own `opened` event (ADR 0278 §1), asking again
// until the runtime takes it: the runtime answers only once Serve runs.
func hearOpened(t *Tracker) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
err := stdio.Subscribe("mesh-issues."+EventOpened, t.OnOpened)
if err == nil {
logf("[mesh-issues] the duplicate check hears every issue opened")
return
}
logf("[mesh-issues] the duplicate check does not hear issues opened yet (%v); asking again in %s", err, wait)
}
}
// dryRunAtTheTerminal is `mesh-issues dry-run <hq checkout> <settings.json>`: the import's dry run over a
// checkout on disk, against no tracker, before the module is rolled out. It reads, writes nothing, and
// prints what the import would create from that checkout, as the verb's dry run would answer against an
// empty tracker. Not a way to import: only the verb writes, and it reads the record over the bus.
func dryRunAtTheTerminal(t *Tracker, args []string) int {
if len(args) != 2 {
logf("usage: mesh-issues dry-run <hq checkout> <settings.json>")
return 2
}
t.Store = emptyStore{}
t.Settings = func() (Settings, error) { return readSettings(args[1]) }
out, err := t.Import(ImportAsk{DryRun: true}, dirRecords{dir: args[0]})
if err != nil {
logf("%v", err)
return 1
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(out)
return 0
}
// emptyStore is a Store that holds nothing and takes nothing: the terminal's dry run.
type emptyStore struct{}
func (emptyStore) Get(string, string) (*Entry, error) { return nil, nil }
func (emptyStore) Create(string, string, any) (uint64, error) {
return 0, errors.New("a dry run at the terminal writes nothing")
}
func (emptyStore) Update(string, string, any, uint64) (uint64, error) {
return 0, errors.New("a dry run at the terminal writes nothing")
}
func (emptyStore) Keys(string) ([]string, error) { return nil, nil }
func (emptyStore) Increment(string, string, int) (uint64, error) {
return 0, errors.New("a dry run at the terminal writes nothing")
}
// args reads one call's arguments strictly: an argument the verb does not take is refused, never ignored,
// so nothing a caller sends is silently dropped — and no argument can say who the caller is.
type args struct {
verb string
m map[string]any
}
func readArgs(verb string, m map[string]any, takes ...string) (args, error) {
allowed := map[string]bool{}
for _, k := range takes {
allowed[k] = true
}
var unknown []string
for k := range m {
if !allowed[k] {
unknown = append(unknown, k)
}
}
if len(unknown) > 0 {
sort.Strings(unknown)
return args{}, fmt.Errorf("%s takes %s; it does not take %s", verb, strings.Join(takes, ", "), strings.Join(unknown, ", "))
}
return args{verb: verb, m: m}, nil
}
func (a args) str(k string) (string, error) {
v, ok := a.m[k]
if !ok || v == nil {
return "", nil
}
s, ok := v.(string)
if !ok {
return "", fmt.Errorf("%s is text", k)
}
return s, nil
}
func (a args) list(k string) ([]string, error) {
v, ok := a.m[k]
if !ok || v == nil {
return nil, nil
}
switch t := v.(type) {
case string:
if strings.TrimSpace(t) == "" {
return nil, nil
}
return []string{t}, nil
case []any:
if len(t) > MostEntries {
return nil, fmt.Errorf("%s names %d entries; at most %d", k, len(t), MostEntries)
}
out := make([]string, 0, len(t))
for _, e := range t {
s, ok := e.(string)
if !ok {
return nil, fmt.Errorf("every entry of %s is text", k)
}
out = append(out, s)
}
return out, nil
}
return nil, fmt.Errorf("%s is a list of text", k)
}
func (a args) number(k string, required bool) (uint64, error) {
v, ok := a.m[k]
if !ok || v == nil {
if required {
return 0, fmt.Errorf("%s needs %s: an issue's number", a.verb, k)
}
return 0, nil
}
switch t := v.(type) {
case float64:
if t >= 1 && t <= 1e9 && t == math.Trunc(t) {
return uint64(t), nil
}
case string:
if n, ok := numberOfKey(strings.TrimSpace(t)); ok && n <= 1e9 {
return n, nil
}
}
return 0, fmt.Errorf("%s is a whole number from 1, such as 312", k)
}
func (a args) flag(k string) (bool, error) {
v, ok := a.m[k]
if !ok || v == nil {
return false, nil
}
b, ok := v.(bool)
if !ok {
return false, fmt.Errorf("%s is true or false", k)
}
return b, nil
}
func text(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func texts(description string) map[string]any {
return map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": description}
}
func number(description string) map[string]any {
return map[string]any{"type": "integer", "minimum": 1, "description": description}
}
func schema(required []string, props map[string]any) map[string]any {
s := map[string]any{"type": "object", "properties": props, "additionalProperties": false}
if len(required) > 0 {
s["required"] = required
}
return s
}
// Tools are the seat's verbs, each said in plain words: what it does and what it refuses (to-be 53 §2).
func Tools(t *Tracker) []stdio.Tool {
statuses := strings.Join(Statuses, ", ")
return []stdio.Tool{
{Name: Seat + ".open",
Description: "Open an issue — a question about the mesh someone must answer — and get its number, the next " +
"one, never one another caller got. Give a title and the report in plain words (what was seen, what " +
"was expected); optionally located-in, the owners it is in; and, for a design or governance issue whose " +
"narrative goes in hq, record: the folder's name in lower-case words joined by hyphens, without the " +
"number. Refused, taking no number: an empty title or report, an owner not on the registered list, " +
"a secret, or an unknown argument.",
Input: schema([]string{"title", "report"}, map[string]any{
"title": text("one line: the symptom, in plain words"),
"report": text("what was seen, what was expected, and how it was found"),
"located-in": texts("the owners it is in, each a repository such as mesh-controller or a module such as \"mesh-catalog modules/nats\", optionally followed by where in it"),
"record": text("for an issue with a folder in hq: the folder's name without its number, such as the-counter-skips-a-number"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("open", m, "title", "report", "located-in", "record")
if err != nil {
return nil, err
}
var ask OpenAsk
if ask.Title, err = a.str("title"); err != nil {
return nil, err
}
if ask.Report, err = a.str("report"); err != nil {
return nil, err
}
if ask.LocatedIn, err = a.list("located-in"); err != nil {
return nil, err
}
if ask.Record, err = a.str("record"); err != nil {
return nil, err
}
return t.Open(ask)
}},
{Name: Seat + ".get",
Description: "One issue whole, by its number: title, status, report, who opened it and when, the owners it " +
"is located in, what fixed it, its notes and its links.",
Input: schema([]string{"number"}, map[string]any{"number": number("the issue's number")}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("get", m, "number")
if err != nil {
return nil, err
}
n, err := a.number("number", true)
if err != nil {
return nil, err
}
return t.Get(n)
}},
{Name: Seat + ".list",
Description: "The issues, newest first, one short row each — number, status, owner, title — optionally only " +
"those at one status (" + statuses + "), located in one owner, or whose title or report holds a phrase. " +
"Answers what is open in one call.",
Input: schema(nil, map[string]any{
"status": map[string]any{"type": "string", "enum": Statuses, "description": "only issues at this status"},
"owner": text("only issues located in this owner, such as mesh-controller"),
"phrase": text("only issues whose title or report holds these words"),
"limit": map[string]any{"type": "integer", "minimum": 1, "maximum": MostListResult, "description": "at most this many rows (default 50)"},
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("list", m, "status", "owner", "phrase", "limit")
if err != nil {
return nil, err
}
var ask ListAsk
if ask.Status, err = a.str("status"); err != nil {
return nil, err
}
if ask.Owner, err = a.str("owner"); err != nil {
return nil, err
}
if ask.Phrase, err = a.str("phrase"); err != nil {
return nil, err
}
if _, given := m["limit"]; given {
l, err := a.number("limit", false)
if err != nil {
return nil, err
}
ask.Limit = int(min(l, MostListResult))
}
return t.List(ask)
}},
{Name: Seat + ".move",
Description: "Change an issue's status — " + statuses + " — with what that status needs, under the " +
"development cycle's rules: located needs located-in, owners on the registered list; resolved needs " +
"located-in and fixed-by (the pull requests or commits), and an issue in the mesh's core also replay " +
"(the replay's id, such as R361) or replay-none (why no replay is possible); wontfix needs a reason. " +
"A refused move changes nothing and says which rule refused it.",
Input: schema([]string{"number", "status"}, map[string]any{
"number": number("the issue's number"),
"status": map[string]any{"type": "string", "enum": Statuses, "description": "the status it moves to"},
"located-in": texts("the owners it is in; replaces what it said"),
"fixed-by": texts("what fixed it: pull requests or commits, such as mesh-controller#246; replaces what it said"),
"amended-design": texts("the design documents the fix changed, such as 03-DESIGN/00-as-is/12-the-controller.md"),
"replay": text("for a core issue resolved: the replay's id in mesh-lab's replays register, such as R361"),
"replay-none": text("for a core issue resolved: why no replay is possible"),
"reason": text("for wontfix: why it will not be fixed"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("move", m, "number", "status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "reason")
if err != nil {
return nil, err
}
var ask MoveAsk
if ask.Number, err = a.number("number", true); err != nil {
return nil, err
}
for _, f := range []struct {
k string
p *string
}{{"status", &ask.Status}, {"replay", &ask.Replay}, {"replay-none", &ask.ReplayNone}, {"reason", &ask.Reason}} {
if *f.p, err = a.str(f.k); err != nil {
return nil, err
}
}
for _, f := range []struct {
k string
p *[]string
}{{"located-in", &ask.LocatedIn}, {"fixed-by", &ask.FixedBy}, {"amended-design", &ask.AmendedDesign}} {
if *f.p, err = a.list(f.k); err != nil {
return nil, err
}
}
return t.Move(ask)
}},
{Name: Seat + ".note",
Description: "Add a note to an issue: what was found, tried or decided, in plain words. A note is kept as " +
"written and never edited. Refused: an empty note, a secret, or one that would make the issue larger " +
"than an issue is kept in.",
Input: schema([]string{"number", "text"}, map[string]any{
"number": number("the issue's number"),
"text": text("the note"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("note", m, "number", "text")
if err != nil {
return nil, err
}
n, err := a.number("number", true)
if err != nil {
return nil, err
}
s, err := a.str("text")
if err != nil {
return nil, err
}
return t.Note(n, s)
}},
{Name: Seat + ".link",
Description: "Link an issue to something, or with remove, take a link away. Kinds: duplicates or relates-to " +
"another issue (its number), pull-request (such as mesh-controller#246), decision (a decision record's " +
"number, such as 0275), design (a design document's path), condition (a condition's key) and delivery " +
"(a delivery's id). Refused: a target not of its kind's shape, an issue that does not exist, a link it " +
"already has.",
Input: schema([]string{"number", "kind", "target"}, map[string]any{
"number": number("the issue's number"),
"kind": map[string]any{"type": "string", "enum": LinkKinds, "description": "what the target is"},
"target": text("what it links to, in the kind's shape"),
"remove": map[string]any{"type": "boolean", "description": "take the link away instead"},
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("link", m, "number", "kind", "target", "remove")
if err != nil {
return nil, err
}
var ask LinkAsk
if ask.Number, err = a.number("number", true); err != nil {
return nil, err
}
if ask.Kind, err = a.str("kind"); err != nil {
return nil, err
}
if ask.Target, err = a.str("target"); err != nil {
return nil, err
}
if ask.Remove, err = a.flag("remove"); err != nil {
return nil, err
}
return t.Link(ask)
}},
{Name: Seat + ".import",
Description: "Import hq's issue folders (04-ISSUES/NNN-slug/00-report.md), read through the records module at " +
"the commit its checkout stands at, each as an issue under its own number with the status, owners, fix " +
"and replay its report gives. It only ever creates an issue that is not there, or fills the status of one " +
"the tracker holds without a status; it never overwrites an issue, never advances the counter, refuses a " +
"folder numbered past the counter or past the tracker's first number without an issue, and run again " +
"changes nothing and says so. With dry-run it writes nothing and says what it would do; with commit it " +
"refuses a checkout standing elsewhere. The answer is a row per folder and the counts.",
Input: schema(nil, map[string]any{
"dry-run": map[string]any{"type": "boolean", "description": "read everything, write nothing, say what would be done"},
"commit": text("the commit of hq the record must stand at (a prefix is enough); refused when it stands elsewhere"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("import", m, "dry-run", "commit")
if err != nil {
return nil, err
}
var ask ImportAsk
if ask.DryRun, err = a.flag("dry-run"); err != nil {
return nil, err
}
if ask.Commit, err = a.str("commit"); err != nil {
return nil, err
}
records := t.Records
if records == nil {
records = busRecords{}
}
return t.Import(ask, records)
}},
{Name: Seat + ".groom",
Description: "The duplicate check over the backlog: every issue not resolved is judged, " +
"oldest first, against the older ones not resolved, by the agent on this node; a certain duplicate is " +
"closed as wontfix, \"duplicate of N\", its report kept as a note on the older issue, which never moves; " +
"a related pair is linked relates-to and noted. Without arguments it starts a dry run in the background " +
"that writes nothing on any issue and answers its pass id; with pass, that pass's progress and findings; " +
"with confirm and pass, it applies exactly that pass's findings, each checked again (one whose issues have " +
"since moved on is skipped and said). Resolved and wontfix issues are never touched. One pass at a time.",
Input: schema(nil, map[string]any{
"pass": text("a pass's id, as a dry run answered it"),
"confirm": map[string]any{"type": "boolean", "description": "apply the pass's findings"},
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("groom", m, "pass", "confirm")
if err != nil {
return nil, err
}
var ask GroomAsk
if ask.Pass, err = a.str("pass"); err != nil {
return nil, err
}
if ask.Confirm, err = a.flag("confirm"); err != nil {
return nil, err
}
return t.Groom(ask, func(f func()) { go f() })
}},
}
}