mesh-issues: check every new issue for a duplicate; claude-code: one judgement with no tools (hq ADR 0278)
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

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.
This commit is contained in:
jochen
2026-10-10 17:11:54 +02:00
parent fb77be42ca
commit f58840b8a6
13 changed files with 1750 additions and 16 deletions
+18 -6
View File
@@ -28,6 +28,9 @@ type Tracker struct {
Sleep func(time.Duration)
// Records is the record the import reads; nil is the records module over the bus.
Records Records
// Judge is the duplicate check's way to a model (ADR 0278); nil judges nothing.
Judge Judgement
groom grooming
}
// The events this module says, one per kind of change.
@@ -317,7 +320,10 @@ func known(status string) bool {
// Move changes an issue's status under the cycle's rules (to-be 53 §5). A refused move changes nothing and
// says which rule refused it.
func (t *Tracker) Move(a MoveAsk) (*Moved, error) {
func (t *Tracker) Move(a MoveAsk) (*Moved, error) { return t.move(t.by(), a) }
// move is Move as the one who acts: a caller, or the duplicate check.
func (t *Tracker) move(by string, a MoveAsk) (*Moved, error) {
to := strings.TrimSpace(a.Status)
if !known(to) {
return nil, fmt.Errorf("status %q is not one of %s", to, strings.Join(Statuses, ", "))
@@ -403,7 +409,7 @@ func (t *Tracker) Move(a MoveAsk) (*Moved, error) {
if err != nil {
return nil, err
}
out := &Moved{Number: is.Number, From: from, To: to, Changed: changed, By: t.by(),
out := &Moved{Number: is.Number, From: from, To: to, Changed: changed, By: by,
Said: fmt.Sprintf("Issue %d moved from %s to %s.", is.Number, from, to)}
body := map[string]any{"number": is.Number, "from": from, "to": to, "by": out.By, "at": t.now()}
for _, c := range changed {
@@ -466,12 +472,15 @@ type Noted struct {
}
// Note appends a note to an issue; a note is never edited.
func (t *Tracker) Note(n uint64, text string) (*Noted, error) {
func (t *Tracker) Note(n uint64, text string) (*Noted, error) { return t.note(t.by(), n, text) }
// note is Note as the one who acts.
func (t *Tracker) note(by string, n uint64, text string) (*Noted, error) {
text, err := multiLine("text", text, MostNote)
if err != nil {
return nil, err
}
note := Note{By: t.by(), At: t.now(), Text: text}
note := Note{By: by, At: t.now(), Text: text}
is, err := t.change(n, func(is *Issue) error {
is.Notes = append(is.Notes, note)
return nil
@@ -507,7 +516,10 @@ type Linked struct {
}
// Link adds or removes a typed link.
func (t *Tracker) Link(a LinkAsk) (*Linked, error) {
func (t *Tracker) Link(a LinkAsk) (*Linked, error) { return t.link(t.by(), a) }
// link is Link as the one who acts.
func (t *Tracker) link(by string, a LinkAsk) (*Linked, error) {
kind := strings.TrimSpace(a.Kind)
target, err := checkTarget(kind, a.Target)
if err != nil {
@@ -525,7 +537,7 @@ func (t *Tracker) Link(a LinkAsk) (*Linked, error) {
return nil, err
}
}
by, at := t.by(), t.now()
at := t.now()
is, err := t.change(a.Number, func(is *Issue) error {
for i, l := range is.Links {
if l.Kind == kind && l.Target == target {