Files
hq/03-DESIGN/01-to-be/53-the-issue-tracker.md
T
jschoubben f95f758c87
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Give ADR 0300 its place in to-be 53: the tracker's record is a log of operations (issue 508)
ADR 0300 was accepted with no home, so the knowledge base's whole-set rule fails and holds every rollout of mesh-knowledge.
2026-10-11 20:38:02 +02:00

24 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be in-progress
mesh-catalog modules/mesh-issues
mesh-catalog modules/claude-code
mesh-catalog modules/messenger
mesh-sdk
2026-10-11
ADR 0275
ADR 0278
ADR 0279
ADR 0281
ADR 0284
ADR 0285
ADR 0291
ADR 0294
ADR 0300

53 — The issue tracker

The mesh keeps its issues in a module of its own, mesh-issues, one instance on the control-node, holding the seat issue-tracker. It hands out every issue's number from one counter that two callers can never both advance, holds each issue's status and links, refuses a change the development cycle would refuse, and announces every change on the bus. An issue lives whole in the tracker, its report, diagnosis and resolution included; hq keeps no issue content. (ADR 0275, from research 042; ADR 0291 for an issue whole in the tracker.)

An issue is a question about the mesh that someone must answer. The issue tracker is the module that holds issues, named mesh-issues; the seat it holds is issue-tracker. The glossary holds both words.

Acceptance. An agent opens an issue and gets the next number; two agents opening at the same moment never get the same one.

This document designs slice 1 in detail. The slices after it are listed in § 8, each with what the operator sees when it is done.

1. The parts

  control-node

  mesh-issues  (seat issue-tracker, held once for the mesh)
   ├ its process, launched by the node runtime
   ├ bucket issues     one key per issue, by number; a short history per key
   ├ bucket counters   one key per sequence; slice 1 has `issue` only
   ├ bucket grooming   the backlog pass of the duplicate check (§ 9)
   ├ setting owners    the registered list `located-in` may name
   ├ verbs             open, get, list, move, note, link, import, groom, set, tracking,
   │                   served by the control-node's tool runner
   ├ events            opened, moved, noted, linked, imported, set, triaged, ready, ready-to-verify
   └ consumes          its own `opened` and `moved`: the triage (§ 9);
                       mesh-delivery's `transition`: when a fix is delivered (§ 10)

  claude-code  (on the same node)
   └ claude_code_judge  one question answered by the agent with no tools (§ 9)

  any agent session, on any node
   mesh MCP server ── mesh_search, mesh_describe, mesh_call ──▶ the verbs, over the bus

The module is written in Go against the SDK. It is built on the model of hits, a headless issue tracker on NATS, and carries none of its code; its README says so.

2. The seat and its verbs

The module declares the seat issue-tracker at mesh scope and claims it exclusively (ADR 0126): there is one tracker per mesh. Its verbs are the seat's tools, served through the tool runner and announced on the bus (ADR 0197), so an agent finds them with mesh_search ("open an issue") and calls them as issue-tracker.<verb>.

Verb Takes Does
open a title, the report as text, optionally located-in and a folder path checks the issue, then takes the next number and writes it; returns the number. Refuses an empty title or report, and an owner not on the registered list
get a number the issue whole: fields, notes, links
list optionally a status, an owner, a phrase compact rows, newest first, short enough to read in a chat: number, status, owner, title, and the worker with since and whether it is stale
move a number, a status, and what that status needs; by for diagnosing and fixing, and take-over with a reason to take an issue over from another worker changes the status under the rules of § 5
note a number and a text appends to the issue's notes; a note is never edited
link a number, a kind and a target, or remove adds or removes a typed link
set a number and any of severity, needs, suggested-owner changes them as a person decides (§ 10)
tracking optionally condition keys for every condition key an issue links, the issues that link it and where each stands: status, needs, worker, fix (§ 12)
groom optionally confirm and a pass's id the duplicate check over the backlog (§ 9): without confirm, starts a dry run in the background and answers its id; with the id alone, its progress and findings; with confirm and the id, applies exactly that pass's findings, each checked again
import (slice 3) optionally dry-run, a commit reads every issue folder of hq through the records module and writes each as an issue at its own number; creates what is absent, fills a status the tracker holds empty, never overwrites, never advances the counter; run again, changes nothing and says so

Each verb's description says in plain words what it does and what it refuses, without the mesh's internal names, so an agent picks the right one from mesh_search alone. A refusal says what was wrong and what would be accepted.

The caller is the bus credential, never an argument. The runtime hands the module the identity of each call's caller; the module records that identity as who acted. No verb takes an actor. by on move is not one: it names the issue's worker, who works on it, as the worker states it (ADR 0284).

3. What one issue holds

  • number (its key), title, status (open, diagnosing, located, fixing, resolved, wontfix), opened, closed;
  • worker, while it is diagnosing or fixing: who works on it as who@node, since when, and active, the last time a caller did anything on it (§ 5);
  • located-in: entries of the registered list of owners (repositories and modules), never free text;
  • fixed-by (pull requests or commits), amended-design, replay or replay-none, and a reason for wontfix;
  • notes: appended, each with who wrote it, when, and the text;
  • links, typed: to another issue (duplicates, relates-to), a pull request, a decision record, a design, a condition key (the issue tracks that condition, § 12) or a delivery;
  • documents: the report, and a diagnosis and a resolution when written, each with its author and date, revised in place with its history kept (ADR 0291);
  • record: the path of the issue's folder in hq, while one exists; none is opened any more, and the folders are imported into documents and removed (§ 8, slice 6);
  • severity (critical, major, minor), needs (none, decision, operator), suggested-owner, delivered (the pull requests of fixed-by the delivery said are delivered) and triage (the triage's judgement as made), and the link kind depends-on (§ 10).

The registered list of owners is a setting of the module, changed through the controller's settings verb like any other module setting. Slice 1 starts it with the repositories in hq's 00-META/repos.md and the modules the catalogue registers.

An issue that has a folder in hq kept its status in that folder's frontmatter until the import of slice 3 ran; until then the tracker held its number, title and record, took its notes and links, and move refused it with the reason: its status lives in its folder. Since the import, every issue is whole in the tracker, folder or not, and a report in hq carries no status key. No status is written in two places for one issue.

4. State, the counter and events

Buckets the module declares, the controller creates (ADR 0201). The manifest's state names two buckets, issues and counters, with their options: issues keeps a short history per key, counters one value per key, neither expires. The module never creates a bucket. One issue is one value; at the 256 KiB a value may be, a note that would push an issue past it is refused with that reason (the largest issue folder in hq is 25 KiB).

The counter is advanced by compare-and-set. Beside get, put, delete, keys and watch, the runtime's state verbs hold two writes that are refused when another writer was first, in the runtime and the SDK's state interface together (ADR 0259):

  • create writes a key only if it does not exist;
  • update writes a key only if its revision is still the one the caller read, and returns the new revision.

The SDK's Increment reads a counter and writes the next number with create or update at the revision it read, trying again a bounded number of times when another writer was first. The counter is started once, by create at the number before the first, so it never starts twice. open then runs in this order:

  1. validate the issue; a refused issue spends no number;
  2. read counters/issue and its revision;
  3. update it to the next number at that revision; if another caller moved it first, the write is refused, and open reads again and retries, a bounded number of times;
  4. create the issue's key; an existing key is a fault the module reports loudly and never overwrites.

So the counter does not rely on there being one instance: two instances, or two calls racing in one, still never hand out one number twice. The counter starts above the highest issue number on main and on every open branch of hq at the moment the module is switched on. Numbers already skipped stay unused, and a number never moves.

Every change is an event the module declares under emits (ADR 0041): opened, moved, noted, linked. Each carries the number, what changed, and who changed it, so the events are the audit trail, and anything watching the bucket or the events sees a change as it happens. moved also carries the worker a move names, as by, with since, and who acted as caller; on a take-over took-over-from and the reason, and released when a move ends the work. No event carries a secret, and an issue holds none.

5. The cycle's rules, at the write

move refuses what cycle.py refuses at merge today, and the rules playbook 03 states:

The issue moves to Needs
any status one of the six
diagnosing or fixing by, the worker as who@node; refused while another worker is current, naming them, unless take-over gives a reason, which is noted; a worker with nothing done on the issue for worker-stale-after (24 hours) is stale and may be taken over from without one
any other status no by and no take-over; the move ends the work
located located-in names at least one registered owner
resolved the issue is at fixing (or already resolved, to correct what it holds); located-in, and fixed-by saying what fixed it
resolved, for a core issue (its located-in names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build agent) replay (an id from mesh-lab's replays register) or replay-none saying why none is possible (ADR 0237)
wontfix a reason

A refused move changes nothing and says which rule refused it. The rows for diagnosing, fixing and resolved are ADR 0284's: resolved only from fixing is the check that an agent said it started the fix. cycle.py checked the folders that carried status until slice 3; since then it refuses a status key in any report, and judges no issue's status.

6. How the operator and agents use it

Through the mesh's tools first. An agent opening an issue calls issue-tracker.open, gets the number, and, for a design or governance issue, writes 04-ISSUES/NNN-slug/ under it with the number in the report. Playbook 03's first step changes to that the day slice 1 is live; nothing else in playbook 03 changes in slice 1. list answers "what is open" in one call, from any session, and the responder on a channel answers from it once the seat exists.

An agent says when it starts. Before it starts work on an issue, an agent moves it to diagnosing (to find where it lives) or fixing (to fix it) with by, its own name and node. list then shows who works on what, and a second agent is refused before it starts the same work. An agent that keeps working notes what it finds, which keeps it the worker; one that stops moves the issue on, or is stale a day later.

7. How it is checked

Rule Checked by
Two opens at once never share a number the module's test over a real bus: many concurrent open calls, from two instances, yield distinct consecutive numbers; the runtime's test that update at a stale revision is refused
A refused open spends no number the module's test: an invalid open, then a valid one, gets the next number
An issue's key is never overwritten the module's test: a create on an existing key is refused and reported
The cycle's rules hold at the write the module's table test over § 5, one case per row, each refused case changing nothing
The module creates no bucket the runtime refuses a module that tries (ADR 0201 §3); the controller's raise asserts both buckets
The caller is who acted the module's test that the recorded actor is the call's identity, whatever the arguments say
A worker is named on a move to diagnosing or fixing, another is refused while it is current, a take-over is noted, a stale one may be taken over from, and resolved is reached only from fixing the rows of ADR 0284's "How it is checked"
An issue's delivery line follows the delivery; ready to verify once; a stuck fix said once and cleared the rows of ADR 0281's "How it is checked"
A linked pull request is a fix; a closed unmerged one is withdrawn; delivered and not resolved is said once; an abandoned issue is flagged the rows of ADR 0294's "How it is checked"
The triage fills only what is empty, links no cycle, and an issue is ready only when its dependencies' fixes are delivered the rows of ADR 0279's "How it is checked"
The duplicate check acts only on a clear duplicate, never on an older or a resolved issue, once per issue, loudly when it cannot judge the rows of ADR 0278's "How it is checked"
An issue's documents are kept whole, with author, date and history, and the import of hq's folders loses nothing (ADR 0291) the module's tests of documents and of the import, one case per document kind; the comparison of every issue with its folder before the folders are removed, written on issue 427. Tracked by issue 427
Live on the mesh, an agent calls issue-tracker.open and gets the next number; two sessions on two nodes open at the same moment and get two different numbers; list shows both

8. The slices after the first

Slice Delivers Done when the operator sees
1 The mesh-issues module: open, get, list, move, note, link; a counter by compare-and-set in the SDK; playbook 03 takes its numbers from it an agent opens an issue and gets the next number; two agents opening at the same moment never get the same one
2 Decision and research numbers from the tracker, each its own key in counters; a number is reserved when a branch starts; the knowledge base's reserve asks for them (to-be 55 § 5). Tracked by issue 424 two agents writing a decision at once get two numbers, and no hq pull request is renumbered
3 import: every issue folder in hq read through the records module and written as an issue with its own number, idempotent and showing what it would change before it does (built and run 2026-10-10; one pull request changed playbook 03 and cycle.py and added dated notes to ADR 0019, 0080 and 0275, and once every folder was checked against the tracker a second removed the status keys from every report, as § 3 says); then the four consolidation passes of research 042 (fixed, duplicates, owners, grouping; the duplicates pass is the duplicate check's groom, § 9, judged rather than by shared words), each a list confirmed before it is applied through move, link and note, and an audit of fixed-by against the forge the not-closed list comes from one call and shrinks by what was already fixed or duplicated, each with its reason
4 Conditions and pull requests linked: an issue links a condition by its key, and every message about the condition says the issue and where it stands (§ 12; the controller does not show the link, ADR 0285); a merged pull request notes every issue it names; after a week of that, the controller opens one issue per healer-wanted condition key by itself a condition's message names its issue, and a merge notes the issues it names
5 A read-only view onto the issues bucket over the bus the operator reads issues in a browser without an agent
6 An issue whole (ADR 0291): the documents of § 3; an import of the 361 folders of hq into them, repeatable, keeping each issue's number and dates; once every issue is compared with its folder, one pull request rewrites the 765 path mentions in hq to "issue N", removes the folders, makes records.py refuse a new path into 04-ISSUES/, and changes playbook 03; open loses record. Tracked by issue 427 issue-tracker.get answers an issue's report and diagnosis, and hq has no 04-ISSUES/

Once slice 1 runs, each slice after it is opened as an issue in the tracker, so the tracker carries its own work.

9. The duplicate check

When an agent opens an issue that repeats one not resolved, the tracker closes the new one as its duplicate within a few minutes, keeping its report on the older one; an issue that repeats nothing gains a note that it was checked (ADR 0278).

  • Started by the tracker's own opened event, which the module consumes. Only an issue still at open is checked, against every older issue at open, diagnosing, located or fixing; resolved and wontfix issues are never offered or changed.
  • Judged in two questions to the agent module's claude_code_judge on the tracker's node: a shortlist of at most five by titles, then a verdict on the reports (duplicate, related or none, whether it is certain, and why). The judging session runs as the node's agent account under its licence, with no tools, in safe mode, and answers within 25 seconds.
  • A certain duplicate: the new issue is linked duplicates, its title and report become a note on the older one, its verdict a note on itself, and it moves to wontfix, "duplicate of N". Related, or not certain: a relates-to link and a note on both. None: one note. The older issue never moves. Each step goes through the tracker's own note, link and move, and is recorded as by duplicate-check.
  • Once per issue. The verdict note is written first, in a fixed first line; an event delivered again does only the steps not yet done.
  • Loud when it cannot judge. One note on the new issue, and the event is not taken: after five deliveries the controller raises max-deliveries on the tracker's consumer.
  • The backlog: groom runs the same judgement over every issue not resolved, as a dry run kept in the state grooming, then applies that pass's findings on confirm.
  • Settings: duplicate-check (on) and judge-model (sonnet).

10. Triage, severity and readiness

An agent opening an issue need not know the backlog: the triage estimates its severity and what it needs, links what it depends on, and the issue is said ready when every fix it waits for is delivered (ADR 0279).

  • Triage is the judgement of § 9 widened: its second question is always asked and also answers severity, needs, a suggested owner when the issue names none, and the shortlisted issues it depends on or relates to, each with a one-line reason. The judgement is kept on the issue first, then applied through the tracker's own verbs: one note saying it all, each empty field filled, each link added (never one that closes a cycle, never to a resolved or wontfix issue), the duplicate verdict last. It fills only what is empty; a value a person gave stands, and a disagreement is a line in the note. Recorded as by triage.
  • Severity: critical (something the operator relies on is broken, or data is at risk), major (a rule of the mesh does not hold, or people work around it), minor (a contained fix; nothing the operator sees changes). Needs: none, decision (a decision record first), operator (the operator's answer is required).
  • depends-on: refused when it would close a cycle; get shows what an issue depends on and what it is needed by.
  • Ready: not resolved or wontfix, and every dependency wontfix (one closed as a duplicate counts as its original), or resolved with every pull request its fixed-by names delivered; said once per spell. The tracker consumes mesh-delivery.transition and, at its own moved to resolved, asks mesh-delivery.checks for what it has not heard; an issue made ready gains a note and the event ready.
  • open takes severity and needs; set changes severity, needs and suggested owner; list filters on severity, needs and ready. groom triages the backlog, dry run first.
  • The values are optional keys of an issue's one JSON value: an issue kept before reads as having none, and the key-value store has no schema to migrate.

10a. The record is a log of operations

The tracker's record is its log of operations, and its bucket is that log's fold (ADR 0300, resting on ADR 0297): every write is one operation appended under the issue's number before the bucket is written; the bucket is rebuilt from the log on start; the events announce and the log is the audit trail; list answers from memory with a ranked search and a before cursor. Built under issues 501 and 502.

11. Whether the fix is delivered

An issue knows whether its fix is on the machines, as the delivery says it; delivered is not resolved, and a fix stuck after its merge is said (ADR 0281). Since ADR 0294, an issue's fixes are the pull requests fixed-by names and every pull request linked to it; a pull request closed without merging is withdrawn; an issue left in fixing an hour after its last fix is delivered is said once, "fixed and delivered, not resolved"; and a stale worker with no pull request open is abandoned.

  • For each pull request fixed-by names, the tracker keeps the state mesh-delivery.transition last said — merged, held and why, covered, delivered — or asks mesh-delivery.checks once for one it has not heard of. Nothing sets it by hand.
  • get and each row of list carry delivery, one line: "fix not delivered: …", "held: …", "fix merged, not delivered", "fix not merged", "nothing heard …", "delivered to every machine it should reach".
  • When every fix of an issue not yet resolved is delivered, it gains a note and the event ready-to-verify, once; it is resolved after the acceptance is proven live. A fix is named while the issue stays fixing: move to the same status, with by, changes what the issue holds.
  • A fix merged and not delivered after stuck-after (two hours) is said once: a note on the issue and a warning on the operator's channel, cleared when it is delivered. It is not a controller condition: a module that runs only its bundle cannot raise one.

12. Which issue tracks a condition

Every message about a condition says which issue tracks it and where that issue stands, or that none does (ADR 0285).

  • An issue tracks a condition through a link of kind condition naming the condition's key exactly; set by whoever opens the issue or works on it, never by the triage or the controller. An agent that opens an issue for a condition links its key at once, and one that silences a condition because an issue tracks it links that issue first.
  • tracking answers, for every key an issue links or only the keys asked, the issues that link it, those not closed first, at most 10 per key: number, status, severity, needs, the worker with since, active and stale, and fix (not-merged, merged, held, failed, unheard, delivered, or empty), from the facts of § 11. It carries no title, report or note, and changes nothing.
  • The router (to-be 45 § 5) asks it over the bus, outside its work, at most once a minute, and says from it the tracking line in every condition's message; the words and the verdict it changes are its own (to-be 45 § 5). The controller learns nothing of issues.

What it leaves out

Priorities beyond severity, notifications beyond the ready and ready-to-verify events and the stuck-fix warning, full-text or semantic search, the forge's own issue lists, and an index of any kind: none of them is in any slice above, and each needs a decision of its own. Who works on an issue, listed here until 2026-10-10, is settled by ADR 0284.