Compare commits

..
Author SHA1 Message Date
jochen 81aad4189b Open research 043: whether an issue's complexity can hint which model solves it
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 stopped: the pull request closed unmerged
The operator asked whether the issue tracker could estimate how hard an issue is
and so which model an agent should fix it with. Framed as questions, not answers.
2026-10-10 17:37:28 +02:00
mesh-admin d82dde3dca Merge pull request 'ADR 0278, to-be 53 §9: the issue tracker checks every new issue for a duplicate' (#281) from decisions/0278-the-issue-tracker-checks-each-new-issue-for-a-duplicate into main 2026-10-10 15:22:34 +00:00
jochen 5f2f520018 ADR 0278: the check's worst case is under the tool runner's two minutes, not one
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
2026-10-10 17:21:39 +02:00
jochen f89c2c350d ADR 0278: the check as a whole takes up to a minute, kept alive by the tool runner (issue 387); one judgement at a time
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 superseded: a newer head of the same pull request
Review found the two questions together exceed the consumer's ack wait, which the
tool runner did not extend; the record says so and names the fix it waits on.
2026-10-10 17:20:00 +02:00
jochen cf3065c660 ADR 0278, to-be 53 §9: the issue tracker checks every new issue for a duplicate
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 superseded: a newer head of the same pull request
The operator asked for duplicates to be consolidated, and better, when an issue is
opened rather than on a schedule. A judgement needs a model, and the mesh had no
channel for one; the agent module answers one question with no tools, under the
node's licence, and the tracker acts only on a clear duplicate, closing the newer
issue and keeping its report on the older one.
2026-10-10 17:04:27 +02:00
mesh-admin 894be9971f Merge pull request 'An issue's status lives in the issue tracker: playbook 03, cycle.py and the notes slice 3 of ADR 0275 asked for' (#280) from process/issue-status-lives-in-the-tracker into main 2026-10-10 14:17:46 +00:00
jochen 6d1310f1ea An issue's status lives in the issue tracker: playbook 03, cycle.py and the notes slice 3 of ADR 0275 asked for
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
2026-10-10 15:54:32 +02:00
17 changed files with 504 additions and 138 deletions
+11 -8
View File
@@ -23,14 +23,17 @@ result is not "nothing to learn" — it is the reason the next person repeats th
## Steps
1. Read `00-report.md`. Move `status:` to `diagnosing`.
2. Investigate. Write `01-diagnosis.md` in the same folder: the trail, **dated**, including
what was ruled out and how. Archaeology — which commit, which pull request, which date a
behaviour changed — is the most valuable content here.
3. When the owner is known, set `status: located` and fill `located-in:` with repositories or
modules from [`00-META/repos.md`](../../../00-META/repos.md).
4. On resolution: `status: resolved`, fill `fixed-by:`. If the root cause was a design gap, run
`hq-graduate` for the amendment and fill `amended-design:`.
1. Read the issue: `mesh_call issue-tracker.get` with its number, and `00-report.md` when it has a
folder. Move it to `diagnosing` with `issue-tracker.move`.
2. Investigate. The trail goes in `issue-tracker.note`, **dated** in its text, including what was
ruled out and how; for an issue with a folder, in `01-diagnosis.md` there as well. Archaeology —
which commit, which pull request, which date a behaviour changed — is the most valuable content.
3. When the owner is known, `issue-tracker.move` to `located` with `located-in`: entries of the
tracker's registered list of owners (repositories, and modules as `mesh-catalog modules/<module>`);
the tracker refuses an owner it does not know.
4. On resolution: `issue-tracker.move` to `resolved` with `fixed-by`, and `replay` or `replay-none`
for a core issue. If the root cause was a design gap, run `hq-graduate` for the amendment and give
`amended-design`. Nothing of this is written in the report's frontmatter.
## Rules
+4 -6
View File
@@ -46,16 +46,14 @@ Decide the shape first:
```yaml
---
status: open
opened: YYYY-MM-DD
located-in: []
fixed-by:
amended-design:
---
```
Its status stays in this frontmatter until slice 3 of to-be 53 imports the folders (ADR 0275);
`issue-tracker.move` refuses it until then. Notes and links may still go to the tracker.
then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`,
`amended-design:`, `replay:` or `replay-none:` in a report opened after the import. Notes and links go to the
tracker.
4. Write the symptom **as observed**, in plain terms, with the evidence that it happened.
5. Say why it matters beyond the instance. An issue that is only one occurrence is a knowledge
base entry.
+6 -3
View File
@@ -16,12 +16,14 @@ generated on demand and never written back to disk.
| Research | `01-RESEARCH/NNN-*/00-overview.md` | `status` (`active` / `graduated` / `abandoned`), `initiated`, `touches`, `became` |
| Design | `03-DESIGN/**/*.md` (not READMEs) | `layer` (`as-is` / `to-be`), `status` (`designed` / `in-progress` / `implemented` / `abandoned`), `code`, `updated`, `decisions` |
| Decisions | `02-DECISIONS/NNNN-*.md` | `status` (`proposed` / `accepted` / `superseded`), `date`, `deciders`, `reconstructed`, `superseded-by`, `extends` |
| Issues | `04-ISSUES/NNN-*/00-report.md` | `status` (`open` / `diagnosing` / `located` / `resolved` / `wontfix`), `opened`, `located-in`, `fixed-by`, `amended-design` |
| Issues | the issue tracker, not a file: `mesh_call issue-tracker.list` (optionally `status`, `owner`, `phrase`) and `issue-tracker.get` | `status` (`open` / `diagnosing` / `located` / `resolved` / `wontfix`), `opened`, `located-in`, `fixed-by`, `amended-design`, `record` (the folder here, when it has one) |
## Steps
1. Read the frontmatter block from every file above — a grep across each tree is enough, no
need to load bodies.
need to load bodies — and the issues from the tracker. The `status:` keys the reports numbered
001 to 364 still carry are the import's snapshot of 2026-10-10 (ADR 0275), not where an issue
stands.
2. Render what was asked as Markdown tables. Group and sort sensibly. The **ADR index** is one
of these views, and it is the only place the list of records is shown
([ADR 0248](../../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)):
@@ -44,4 +46,5 @@ generated on demand and never written back to disk.
already drifted after a single addition, which is why it was removed, and the generated one
written into `02-DECISIONS/README.md` made every two open decisions conflict (issue 297), which
is why that went too. `index.py` fails if the list is stored there again.
- Do not infer status from prose. Trust only the frontmatter; if it is wrong, flag it.
- Do not infer status from prose. Trust only the frontmatter, and the tracker for an issue; if
either is wrong, flag it.
+29 -39
View File
@@ -4,7 +4,7 @@
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules,
and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only
frontmatter, because status lives in frontmatter and nowhere else.
frontmatter, because a status this repository holds lives in frontmatter and nowhere else.
What is enforced:
@@ -12,15 +12,15 @@ What is enforced:
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
design without a decision -- and once `in-progress` or `implemented` it names
its owning code (`code:`) -- no development without a design that says where.
issues a known `status:`; once `located`, `located-in:` names the owner;
once `resolved`, `fixed-by:` says what fixed it (prose counts --
"nothing, the capability existed" is an answer). And no two records share a
number -- the number is how a record is cited.
replays a core issue -- one opened from 2026-10-07 whose `located-in:` names a core repository --
resolves only with `replay:` (the replay's id in mesh-lab's replays register) or
`replay-none:` (why no replay is possible): ADR 0237, to-be 45 §9. A replay is what
proves a fix fails before and passes after; without one, a fixed class comes back
through a different door (research 031: four such chains in one week).
issues every report has frontmatter, and no two records share a number -- the number is
how a record is cited. An issue's status, owner, fix and replay live in the issue
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
refuses at the write what this check refused at merge until then: a status outside
the five, `located` without an owner, `resolved` without a fix, a core issue
resolved without its replay (ADR 0237), `wontfix` without a reason. A report opened
after the import carries none of those keys -- the tracker holds them, and two
places holding one fact drift. The reports opened before keep theirs as the dated
snapshot the import took; they are not judged, and not kept up to date.
research a known `status:`; a `graduated` overview says what it `became:`, and every
target it names exists.
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
@@ -28,9 +28,8 @@ What is enforced:
record's extends/supersedes chain. A decision nothing points at is one nobody will
find by following pointers -- which is how records go stale in people's heads.
Deliberately NOT enforced: `resolved` issues may leave `located-in` empty (a symptom that
turned out not to be a defect has no owner), and as-is docs need no decisions (they
describe what exists, not what was decided).
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
was decided).
python3 00-META/checks/cycle.py
"""
@@ -43,18 +42,14 @@ import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
ISSUE_STATUSES = {"open", "diagnosing", "located", "resolved", "wontfix"}
RESEARCH_STATUSES = {"active", "graduated", "abandoned"}
# The core, as to-be 45 names it, by where its code lives: the controller, the node-engine, the node
# tools and the console, the SDK's loops, and the catalogue's bus, forge (the announcer of merges) and
# build agent. A `located-in:` entry naming any of these makes an issue a core issue.
CORE = re.compile(r"\b(mesh-controller|mesh-host|mesh-tools|mesh-sdk|node-tools|"
r"mesh-catalog\s+modules/(nats|gitea|build-agent))\b")
# The day the rule began (ADR 0237): issues opened before it are not held to it.
REPLAYS_FROM = "2026-10-07"
REPLAY_ID = re.compile(r"^R\d+\b")
# The keys an issue report carried while its status lived here, and the day from which a report carries
# none of them: the issue tracker holds them (ADR 0275, slice 3 of to-be 53), and `issue-tracker.move`
# enforces the cycle's rules on them at the write. A report opened on or after this day with one of these
# keys is a status written in two places. The reports opened before keep theirs as the import's snapshot.
STATUS_IN_TRACKER_FROM = "2026-10-11"
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none")
# A decision that defers work names the issue that tracks it (issue 354). ADR 0244 left the code
# renames of the node-engine and the tool runner "for a later rename", with no issue, owner or date;
@@ -231,28 +226,23 @@ def main():
else:
seen_records[number] = name
# Since the import of slice 3 (ADR 0275), an issue's status, owner, fix and replay live in the issue
# tracker, and its `move` refuses at the write what this check refused at merge. A report opened from
# STATUS_IN_TRACKER_FROM carries none of those keys; one opened before keeps the import's snapshot.
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
front = frontmatter(path)
if front is None:
bad(path, "no frontmatter")
continue
status = front.get("status")
if status not in ISSUE_STATUSES:
bad(path, "status %r is not one of %s" % (status, sorted(ISSUE_STATUSES)))
if status in ("located", "resolved") and not listy(front, "located-in"):
bad(path, "status %s but located-in is empty" % status)
if status == "resolved" and not listy(front, "fixed-by"):
bad(path, "status %s but fixed-by says nothing" % status)
# A core issue resolves with its replay, or with why none is possible (ADR 0237).
replay = " ".join(listy(front, "replay"))
if replay and not REPLAY_ID.match(replay):
bad(path, "replay: %r names no replay -- an id from mesh-lab's replays register, R<issue>" % replay)
core = any(CORE.search(entry) for entry in listy(front, "located-in"))
opened = str(front.get("opened") or "")
if (status == "resolved" and core and opened >= REPLAYS_FROM and not replay
and not " ".join(listy(front, "replay-none")).strip()):
bad(path, "a core issue resolved with no replay: name it in `replay:` (mesh-lab replays "
"register.go) or say in `replay-none:` why none is possible (ADR 0237)")
if not opened:
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
carried = [key for key in TRACKER_KEYS if key in front]
if opened >= STATUS_IN_TRACKER_FROM and carried:
bad(path, "carries %s, but an issue's status, owner, fix and replay live in the issue tracker "
"(ADR 0275): move it with `issue-tracker.move`, and keep no copy here"
% ", ".join("`%s:`" % key for key in carried))
# ---- research ----------------------------------------------------------------------
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
+4
View File
@@ -550,6 +550,10 @@ Not a domain of the mesh but of the way it is built, listed because its words me
held by the module `mesh-issues` ([53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md)).
Not hits's *item*; bare *ticket* is a homonym (see [Homonyms](#homonyms)).
*Not:* ~~ticketing system~~ (hq), ~~ticket system~~ (hq)
- **duplicate check** — the issue tracker's judgement, when an issue is opened, of whether it asks a question an
older issue not resolved already asks; a certain duplicate is closed `wontfix` with its report kept on the older
issue ([ADR 0278](../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)).
Its pass over the backlog is the verb `groom`.
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
## Homonyms
+11 -6
View File
@@ -60,17 +60,22 @@ the expensive half.
The flow above is a rule, and a rule states how it is checked:
[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no
decision, an `in-progress`/`implemented` design that names no owning code, an issue marked
`located`/`fixed` with no owner or `fixed`/`resolved` with no fix, and a `graduated`
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
sharing a number or an issue report carrying a status key, and a `graduated`
research overview that does not say what it became. Run it with `records.py`, `index.py` and
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). What the checks cannot see — that code work actually started from a
`words.py` before any HQ merge (`sh merge-check.sh` runs all four). The issue rules the check
once held — `located` with an owner, `resolved` with a fix, a core issue with its replay — are
refused at the write by the issue tracker's `move`
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)). What the checks cannot see — that code work actually started from a
handoff — is held by playbooks [04](04-build-handoff.md) and [07](07-feature-branches.md):
a feature branch exists because a design or an issue sent it.
## Status lives in frontmatter
## Status lives in frontmatter, an issue's in the tracker
Research overviews, design docs, issue reports and decision records each carry their status as
YAML frontmatter (schemas in the section READMEs and playbooks). There are **no central status
Research overviews, design docs and decision records each carry their status as
YAML frontmatter (schemas in the section READMEs and playbooks). An issue's status lives in the
issue tracker, which refuses a wrong status at the write, and its report here carries only
the day it was opened (playbook [03](03-issues.md)). There are **no central status
files** and no decision ledger. **Every decision is a record in
[`02-DECISIONS`](../../02-DECISIONS/)** — if it is worth recording it is worth a record, and if
it is not worth a record it is not recorded. Cross-cutting views, the decision index included,
+34 -26
View File
@@ -19,7 +19,7 @@ The knowledge base already holds the operational record and is indexed on sympto
folder is not a second copy of it. An issue here is a question HQ must **answer**, not an
incident someone must **clear**.
## The issue tracker holds the number
## The issue tracker holds the number and the status
Every issue is opened in the **issue tracker**, the module `mesh-issues` holding the seat `issue-tracker` on the
control-node ([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md),
@@ -28,16 +28,22 @@ compare-and-set, so two authors opening at the same moment never get the same on
reading `main` or the open pull requests** — that is how numbers collided (issue 155: two records sharing a number
reached `main` with every check passing). A session that cannot reach the tracker takes no number and says so.
**The tracker is the truth for every issue's status, owner, fix and links**, whether or not the issue has a folder
here. `issue-tracker.get NNN` says where an issue stands; `issue-tracker.list` says what is open. Nothing here
says it: a report's frontmatter carries no status.
An issue takes one of two shapes:
- **With a folder here** — a design or governance issue, whose narrative belongs in this public record and is
reviewed in a pull request. The tracker holds its number, title, notes and links; its status stays in the folder's
frontmatter until slice 3 of to-be 53 has imported the folders and been checked, and until then the tracker's
`move` refuses it, saying its status lives in its folder.
- **Wholly in the tracker** — every other issue. Its status, owner, notes and links live in the tracker from the
start, and nothing is written here.
reviewed in a pull request. The folder holds the report and the diagnosis; the tracker holds everything else.
- **Wholly in the tracker** — every other issue. Nothing is written here.
No status is ever kept in two places for one issue.
**The reports written before the tracker** (the folders numbered 001 to 364) still carry `status:`, `located-in:`,
`fixed-by:`, `amended-design:`, `replay:` and `replay-none:` in their frontmatter. Those keys are the snapshot the
import of slice 3 took from `main` on the day it ran (the note in ADR 0275 says which day and which commit), and
they are not kept up to date: the tracker is. They are left in place because they are the record of what was known
then, reviewed in pull requests at the time, and because removing 361 keys would say nothing the tracker does not.
Nobody edits them.
## Steps
@@ -52,34 +58,31 @@ No status is ever kept in two places for one issue.
```yaml
---
status: open
opened: YYYY-MM-DD
located-in: [] # owning repo(s)/module(s), filled by diagnosis
fixed-by: # PR or commit reference, filled at resolution
amended-design: # design doc path, when the root cause was a design gap
---
```
Then the symptom as observed, as in the tracker's report.
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
`replay:` or `replay-none:` in a report opened after the import, because the tracker holds them and two places
holding one fact drift.
**An issue wholly in the tracker** is done at this step: what follows happens through the tracker's verbs.
2. Investigate. For a folder-backed issue, the trail goes in `01-diagnosis.md` in the same folder — dated,
including what was ruled out — and `status:` moves to `diagnosing`, then `located` once the owner is known. For an
issue wholly in the tracker, the trail goes in `issue-tracker.note` and the status changes through
`issue-tracker.move`, which refuses `located` without a registered owner in `located-in`.
3. Resolve. Set `status: resolved` (in the frontmatter, or through `issue-tracker.move` for an issue wholly in the
tracker), fill `fixed-by:`, and if the root cause was a design gap,
run playbook [02](02-graduation.md) and fill `amended-design:`. **A core issue** — its `located-in`
**An issue wholly in the tracker** is done at this step.
2. Investigate. The trail goes in `issue-tracker.note`, and, for a folder-backed issue, in `01-diagnosis.md` in the
same folder as well — dated, including what was ruled out. The status moves through `issue-tracker.move`:
to `diagnosing`, then to `located` with `located-in`, which the tracker refuses without a registered owner.
3. Resolve. `issue-tracker.move` to `resolved` with `fixed-by`, and, if the root cause was a design gap,
run playbook [02](02-graduation.md) and give `amended-design`. **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 — resolves with `replay:`, the id of its replay in mesh-lab's replays register, proved to fail on
the commit before the fix and pass on it, or with `replay-none:` saying why none is possible
([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md);
`cycle.py` checks it on a folder, and `issue-tracker.move` refuses the move without it).
agent — resolves with `replay`, the id of its replay in mesh-lab's replays register, proved to fail on
the commit before the fix and pass on it, or with `replay-none` saying why none is possible
([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md));
`issue-tracker.move` refuses the move without it. `wontfix` needs a `reason`.
## Rules
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
- `fixed-by:` names something that will still exist: a commit or a pull request, never a branch. A
- `fixed-by` names something that will still exist: a commit or a pull request, never a branch. A
branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
the time anybody follows it.
- A fix that turns out to have broken something else is written back into the record that asked for
@@ -90,4 +93,9 @@ No status is ever kept in two places for one issue.
sharing a number.
- An issue whose answer is a general lesson should also be written to the knowledge base, so
the next person searching a symptom finds it. Both, not either.
- `status: wontfix` is legitimate and requires a sentence saying why.
- `wontfix` is legitimate and requires a sentence saying why; the tracker refuses it without one.
- **A rule states how it is checked.** The cycle's rules on an issue — a status one of the five, `located` with an
owner, `resolved` with a fix, a core issue with its replay, `wontfix` with a reason — are refused at the write by
`issue-tracker.move` (mesh-catalog `modules/mesh-issues`, its table test over the rules). `cycle.py` checks what
this repository still holds: no two folders share a number, every report has frontmatter, and a report opened
after the import carries no status key.
@@ -0,0 +1,93 @@
---
status: active
initiated: 2026-10-10
touches:
- 03-DESIGN/01-to-be/53-the-issue-tracker.md
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
- 02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
- 00-META/process/03-issues.md
- 00-META/glossary.md
- the agent module's tools and the licences it spends
became: []
---
# 043 — An issue's complexity, as a hint to which model should solve it
## What
Whether the issue tracker can estimate **how hard an issue is to solve**, and use that estimate to
suggest **which model an agent should solve it with**: a smaller, cheaper model for a simple fix, the
strongest available for a change to the core of the mesh. The idea is the operator's, from 2026-10-10,
and he named it a research topic, not a build.
The tracker already asks a model one question about every new issue: the duplicate check
([ADR 0278](../../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md))
reads the new issue's report against the backlog, through the agent module's tool that answers a
structured question with no tools of its own. An estimate of complexity could be one more answer from
the same kind of judgement. Whether it should be, and whether its answer is worth acting on, is what this
effort is for.
## Why
Agents solve most issues on the mesh, and every session spends a licence's allowance. Today the model a
session runs is whatever the session was started with: nothing in an issue says what it needs, and
nothing records afterwards which model solved it, how long it took or whether its fix held. A strong
model on a one-line fix spends more than it has to; a weak model on a change to the controller or the
node-engine can produce a fix that looks done, passes a careless review, and breaks a node that updates
unattended. The second cost is far larger than the first, so a hint is only worth having if it is right
where it matters most.
## The questions
1. **What "complexity" means here.** Lines changed, repositories touched, whether a core component or
the store's schema is involved, whether the cause is known yet, whether a decision is needed first?
Are these one scale or several? Which of them can be read from a **report**, before anyone has
diagnosed the issue, and which only become known once it is `located`? An estimate made at `open` may
have to be revised at `located`, and the question is whether the first estimate is worth making at all.
2. **Who estimates, and when.** A judgement at `open`, beside the duplicate check; one at `located`, when
the owner is known; the agent that diagnoses the issue, in its own words; or the operator. Can the
owner named in `located-in` alone carry most of the signal (a module's own tool versus the controller)?
3. **Where the hint lives.** A field stored on each issue, set and changed through a verb with who set it
and why, or a value **derived** each time from the issue's fields and a setting that maps classes of
work to models? A stored hint can be overruled per issue; a derived one changes everywhere when the
setting changes, which matters when a newer model arrives. Is the hint a model's name, or a tier that a
setting maps to whatever model holds that tier today?
4. **How the outcome is measured, so the choice rests on evidence.** What the mesh would have to record
per solved issue: the model used, tokens and wall time spent, whether the pull request passed its
independent review and both required checks (`mesh/merge-gate`, `mesh/repo-check`) the first time,
how many rounds of rework it took, and whether the issue was reopened or a follow-up opened against
the same cause. Which of these does the mesh already hold (the tracker's notes and links, the
forge's review history, the delivery's checks), and which are recorded nowhere today? Without this,
any mapping from complexity to model is an assertion.
5. **The risk of a weak model on a core change.** Failure must be loud: how does a session that was
given too weak a model say so, rather than hand over a fix that reports success? Should a hint ever
lower the model below what the session was started with, or only raise it? Should some owners (the
controller, the node-engine, the store, the foundation) be outside the hint entirely, always solved
with the strongest model? Is review by a stronger model than the author's a cheaper safeguard than
choosing the author well?
6. **How it relates to the fields being added to the tracker.** Severity says how much an issue hurts;
what an issue needs says what must happen before it can move. Complexity says how hard the fix is.
Are these three separate fields, or is complexity derivable from the other two and the owner? A
severe issue can be simple, and a minor one can sit in the core.
7. **Who acts on the hint.** Does an agent session read it and start a sub-agent with that model; does
the operator see it when choosing what to hand off; does a future dispatcher assign work by it? A
hint nobody reads changes nothing, and the effort should name its reader before anything is built.
8. **Prior art.** Routing a request to the cheapest model that answers it well is studied for single
questions (model cascades and routers that learn from preference data); estimating a task's
difficulty is studied for software issues in agent benchmarks, and in teams as estimation by size or
points. What carries over to a repair done by an agent across a pull request, review and delivery,
and what does not?
## Words
The glossary has no word for an issue's estimated difficulty, nor for a suggested model. This effort
uses **complexity** and **model hint** as plain descriptions until a decision settles them, and
proposes them then through the glossary's own rules.
## What it touches
The issue tracker's fields and verbs ([to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md)),
the judgement the duplicate check already makes (ADR 0278), the agent module and the licences it spends
(ADR 0206), the issues playbook ([03](../../00-META/process/03-issues.md)), and, if outcomes are recorded,
the forge's reviews and the delivery's required checks as sources of evidence.
@@ -114,6 +114,16 @@ documents had failed.
**Every document carries its state in YAML frontmatter** — research overviews, design documents,
decision records, issue reports.
> **Progressive insight — 2026-10-10.** For issue reports this stopped holding the day slice 3 of
> [ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
> landed: an issue's status, owner and fix live in the issue tracker, a module of the mesh, which refuses
> a wrong status at the write; a report here carries the day it was opened and nothing of its status, and
> `cycle.py` refuses a status key in a report opened after the import. The reports written before keep
> their keys as a dated snapshot. The reason stands exactly as written — one place, never two — and the
> one place for an issue is now the tracker, because a file every branch may edit was the place where
> numbers collided and statuses were hard to see. Research overviews, design documents and decision
> records carry their state here as before.
**There are no central status files.** Every cross-cutting view — a status matrix, a decision
index, an open-issue list — is generated from frontmatter when asked for, never written to disk.
Two places holding one fact drift, and the written one wins by being closer to hand.
@@ -46,6 +46,16 @@ The development cycle is enforced mechanically, to the extent frontmatter can ca
> refuses it now. The decision here stands exactly as written: this is one more thing frontmatter
> and file names can carry, found by its absence rather than by reasoning.
> **Progressive insight — 2026-10-10.** The third rule above, no owner-less diagnosis and no fix-less
> fix, and the replay rule `cycle.py` took on under ADR 0237, moved out of `cycle.py` the day slice 3 of
> [ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
> landed: an issue's status lives in the issue tracker, whose `move` refuses at the write what the
> check refused at merge — `located` without an owner, `resolved` without a fix, a core issue without
> its replay, `wontfix` without a reason. Enforced mechanically still, one step earlier, in the one
> place the status is. `cycle.py` keeps the fifth rule, no two records answering to one number, and
> refuses a status key in a report opened after the import. The decision stands: the cycle is checked,
> not trusted; which program checks each rule is a fact that moved.
[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py`
and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work
actually started from a handoff — remains held by playbooks 04 and 07: a feature branch exists
@@ -139,12 +139,28 @@ along the way".
> What slice 1 added is the SDK's `Increment`, which reads a counter and writes the next number only at the revision
> it read. The decision stands as written.
> **Progressive insight — 2026-10-10, slice 3.** The import is built: `issue-tracker.import` reads every issue
> folder through the records module over the bus, at the commit its checkout stands at, and writes each as an issue
> at its own number — create-if-absent on the key, the counter untouched, never an overwrite, a second run changing
> nothing and saying so; `move` no longer refuses an issue with a folder. Its dry run read `main` at commit
> `1ea678c6bd48f62a45db3b5e4b21b9c46867d31a` on 2026-10-10: 361 folders, all readable — 27 `open`, 1 `diagnosing`,
> 48 `located`, 284 `resolved`, 1 `wontfix` — and 29 `located-in` entries naming no registered owner, kept as
> written and marked unverified (issue 366 owns the list). The operator runs the import once after the rollout; the
> commit it read is on every imported issue (`imported.commit`) and in its `imported` events, where the mesh keeps
> its own truth. Two facts differ from §8 as written. **The status keys stay in the reports**: removing 361 keys
> would say nothing the tracker does not, and lose the record of what was known when; they are the dated snapshot
> of that day, not kept up to date, and `cycle.py` refuses them in a report opened from 2026-10-11 on. **The four
> consolidation passes and the audit of `fixed-by`** (to-be 53 §8) are not in the import: each is a list the
> operator confirms before it is applied, done through `move`, `link` and `note` on the imported issues, not by
> the import, which only ever creates. The decision stands as written.
## Consequences
- Collisions end at the source: a number is taken from one counter, not read from a repository many branches write.
- Until the slice 3 import has run and been checked against it, an issue that has a folder here keeps its status in
that folder's frontmatter, and the tracker holds its number, title and folder only. An issue without a folder is
whole in the tracker from the start. No status is written in two places for the same issue at any time.
whole in the tracker from the start. No status is written in two places for the same issue at any time. (Since
slice 3's pull request, dated in the insight above: every issue is whole in the tracker.)
- [ADR 0019](0019-how-this-repository-works.md)'s rule that statuses live only in frontmatter stops holding for issues
on the day slice 3's pull request lands, and part of [ADR 0080](0080-the-development-cycle-is-checked.md)'s check
moves from `cycle.py` into the tracker's `move`. That pull request adds a dated note to each of them saying where
@@ -84,18 +84,6 @@ controller ask it. Nothing bounded how often it could be asked.
fingerprint, and where the whole is read.** Chosen. "recalbox=smb://host/share@/mnt/recalbox" is shown as
"recalbox=‹address›@‹path›", and `settings proposals <id>` shows it whole, with the same fingerprint.
> **Progressive insight — 2026-10-10.** Issue 383 in the issue tracker: the first proposals reached the
> operator's phone as "+ shares: media=‹path›", and the operator said they could not see what they were being
> asked to approve: a mount point, a share name or a private address is the thing being approved. Option 1 was
> rejected on the fact that the content rule refuses a path or an address "in any message that leaves the
> mesh"; the operator decided, and the rule now reads, that on a channel that proves who answers
> (`verified-sender`, Telegram today) the exact values are shown WHOLE, and only a value shaped like a secret
> is withheld there — the ask carries its explanation twice (the SDK's `Whole` beside `Explanation`), the
> router shows the whole only on such a channel and holds it to the secret rule alone, and refuses the ask as
> a whole when a secret's shape is left in it. Option 3 stands as decided for every channel that proves
> nothing (the desk) and for every condition's words. The decision stands: the operator reads the exact
> change and approves it; what changed is where the values may be read whole.
### Where the record of the set is kept
1. **A seat event of the controller's own**, beside the hand-act log. Rejected for now: nothing consumes it, and
@@ -143,14 +131,6 @@ One ask per proposal, published under the controller's name on the `operator-cha
hexadecimal digits of its digest, in groups of four); when a part is not shown, where the whole is read
(`settings proposals <id>`, through the mesh MCP server or `mesh-cli`), whose fingerprint must be the one on
the phone; and that, approved, the layer is set at once and the machine takes it at its next push;
> **Progressive insight — 2026-10-10.** Issue 383: on the phone this read as one wall of text. The
> explanation is now the change alone — one line per key as above, then "Proposed by <who> at <time>" — and
> the router adds what every ask says (who asks, what each answer does, when it expires); on a channel that
> proves who answers the values are whole, as the insight above says. The fingerprint, where the whole is
> read and that the layer is set at once moved to the **Details** answer the ask's own message offers (the
> SDK's `Details`), shown line by line under the content rule. The telegram channel shows every message
> formatted, never reworded: the headline bold, a key line and a fingerprint in monospace, its markup escaped.
- **two options, Approve and Decline, both at the level approve**, as the controller's rehearsal asks, so only a
channel that proves who answered carries either. Each binds the digest of its act: the proposal's id, the
answer, the module, the machine, the digest of the exact values, the digest of the layer they replace,
@@ -246,7 +226,6 @@ channel as every given secret is.
|---|---|
| A proposal changes nothing, and asks at the level approve with the exact change | the controller's test: the ask passes the SDK's check, offers Approve and Decline both at the level approve, each binding an act digest; the explanation carries each key with its value or its shape, the layer's fingerprint and the "was" of a changed key, and passes the content rule; the kept record is the controller's own, and no layer is set (`TestAProposalAsksAtTheLevelApproveWithTheExactChange`, `TestAClearIsProposedAndShowsWhatItRemoves`) |
| What the phone is shown keeps a value's shape and passes the content rule | `TestAValueIsShownInItsShapeAndPassesTheContentRule` |
| On a channel that proves who answers the values are whole and only a secret's shape is withheld; elsewhere they keep their shape; the message carries no fingerprint and no how-to (the insight of 2026-10-10) | the controller's `TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret` (the masked and the whole change differ in exactly the masked lines); the router's `TestAnAsksWholeWordsAreShownWhereTheSenderIsProvenAndMaskedElsewhere` and `TestAnAskWhoseWholeWordsCarryASecretIsRefused`; the telegram channel's `TestAMessageIsRenderedInHTMLWithItsMarkupEscaped` |
| A layer the mesh would refuse, or a silent removal, is not asked | `TestAProposalTheMeshWouldRefuseIsNotAsked` |
| The layer is set once, on the operator's approval, with who approved it; a replay changes nothing | `TestTheLayerIsSetOnceOnTheOperatorsApproval`: the warrant heard twice sets once, no verb is called, the hand-act reads `warrant` with the answer and the values' digest, and counts as a person's decision |
| Decline, expiry, cancel, replacement and the router's refusal discard | `TestDeclineExpiryAndRefusalDiscardAProposal`, each outcome, and an approval after the end refused |
@@ -0,0 +1,214 @@
---
topic: how we work
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
---
# 278. The issue tracker checks every new issue for a duplicate, through a judgement the agent makes without tools
## Context
On 2026-10-10 the operator asked for "a 'groom backlog' tool for our issue tracker which goes over the
non-resolved issues and checks for duplicates and tries to consolidate issues together, 'cancelling' the other
ones with a reference in the open one", and then: "or even better, that runs when a new issue is submitted".
Agents open issues through `issue-tracker.open` ([ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
Nothing tells an agent that the question it is about to open is already open, so the same symptom is opened
again by the next session that meets it. [Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md)
§3 planned a duplicates pass over the imported issues as candidate pairs "whose titles share most words",
each confirmed before it is applied. Shared words find the pairs a reader would find by searching; they do not
find the same question asked in other words, which is the case that costs time, and they cannot tell a
duplicate from two issues in one area. That judgement reads both reports, which needs a language model.
**What the mesh had for a judgement, measured on 2026-10-10.** No seat or tool runs a model for a module:
`mesh_search` for "headless agent", "llm" and "completion" found nothing. The `claude-code` module holds the
agent on every node and the licence the node is bound to ([ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md));
on the control-node the agent runs as an account of its own ([ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)),
and the operator's account, which the tool runner runs bundles as, may become it. `claude` in print mode with
its built-in tools turned off (`--tools ""`) and in safe mode answers a structured question with only its
answer-shaping tool available: measured on the laptop, the session listed `StructuredOutput` as its only tool,
in 2.2 seconds. Ollama provides `model-access` on the home server only, and the control-node runs no model.
**The bounds that shape the design.** A tool call on the bus is answered within 30 seconds or the asker hears a
timeout (the tool runner's request bound). An event a module consumes is redelivered after 30 seconds without
an acknowledgement, five times at most; after that the controller raises its `max-deliveries` condition and
keeps the event in `DEAD_LETTERS` until a person delivers it again. The tool runner gives a handler two minutes,
but said nothing to the bus while the handler worked, so an event handled for longer than 30 seconds was offered
again while it was handled, and events waiting in the runner's buffer ran out their 30 seconds before they were
handed over: a burst of opens would have ended in `max-deliveries` with nothing failed. Found in this record's
review and opened as issue 387; the tool runner now keeps an event alive while its handler works and holds one
at a time, and this record's code is merged only after that fix. A module that runs only its bundle declares
no health ([ADR 0240](0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md) rule 8), and a
module's health is the only way it raises a condition of its own
([ADR 0256](0256-a-machines-hardware-condition-is-read-by-one-module-on-every-machine-and-a-fault-is-said-through-its-health.md)).
The record was searched first ("duplicate", "groom", "issue opened", "headless", "claude-agent -p", "agent
account"): only research 042's pass and the grooming passes made by hand on 2026-09-29.
**A word.** The glossary has none for this check. This record names it the **duplicate check** and adds it.
## Considered Options
### What starts the check
1. *A schedule, every two hours, over the backlog.* The operator's first idea, and his own better one replaced
it: a duplicate opened at 10:00 stays open and gathers notes until noon.
2. **The tracker's own `opened` event.** The tracker already says every open on the bus
([to-be 53](../03-DESIGN/01-to-be/53-the-issue-tracker.md) §4). Consuming it is the mesh's ordinary way for one
change to start work, it reaches an issue opened by any caller, and the event's redelivery is the retry.
Chosen, with a verb for the backlog that exists today.
3. *A step inside `open`.* Refused: `open` answers within the bus's 30 seconds, and a judgement can take most of
them; an open must never fail because a model was slow.
### Who judges
1. *The tracker calls the vendor's interface itself, with the node's token.* Refused: the token is the agent
module's, handed to it sealed (ADR 0206); a second holder is a second place it can leak, and a subscription's
token is for the agent.
2. *A local model through `model-access`.* Refused for now: none runs on the control-node, and the one on the
home server is weaker at telling "the same question" from "the same area", the one distinction this check
must get right.
3. **The agent module answers one question, as the node's agent with no tools.** `claude-code` gains a tool that
runs the agent in print mode, as the account agents run as, under the licence the node is bound to, with its
built-in tools off and in safe mode (no tool servers, plugins, hooks or instruction files), and answers the
structured answer. Chosen: the module that holds the licence spends it, and the session can do nothing but
answer.
4. *A seat for judgements, with kinds.* Deferred: one module asks today. When a second module asks, or a
second holder (a local model) is worth offering, the tool becomes a seat's verb; until then it is a module's
tool, called on the tracker's own node.
### What a verdict may do
1. *Close whichever of the two is newer, or merge both into a third.* Refused: a third issue loses both
numbers' history, and an agent holding the older number would find it closed.
2. **Close only the newer issue, the one just opened, and only for a clear duplicate.** The older issue stays as
it is and gains the newer one's report as a note, so nothing reported is lost and the number agents already
hold stays open. Chosen.
## Decision
### 1. A new issue is checked when it is opened
`mesh-issues` consumes its own `mesh-issues.opened` event. For each opened issue still at `open`, it judges it
against every **older** issue that is not resolved: at `open`, `diagnosing` or `located`. A resolved or wontfix
issue is never offered, never linked and never changed by the check.
The judgement has two questions, so each fits a tool call's 30 seconds however long the backlog grows; the
check as a whole, waiting behind a judgement of the backlog's pass included, stays under the two minutes the tool
runner allows a handler (issue 387):
1. **A shortlist by titles.** The new issue's title and report against every candidate's number and title; the
answer names at most five numbers that could be the same question or closely related. The titles fill at
most 48 KiB of the question, newest first; when the backlog holds more, the note on the issue says how many
were offered of how many.
2. **A verdict on the reports.** The new issue and each shortlisted issue whole, each report cut at 8 KiB; the
answer is one of `duplicate` (the same question: one answer closes both), `related` (the same area or a
cause in common, two questions) or `none`, with the issue it names, whether it is **certain**, and the reason
in one or two sentences.
An empty shortlist is a verdict of `none` without the second question.
### 2. What each verdict does
| Verdict | Done, through the tracker's own `note`, `link` and `move` |
|---|---|
| `duplicate`, certain, naming an offered issue | the new issue is linked `duplicates` to the older one; the older one gains a note holding the new one's title and report, and who found it and why; the new one gains the verdict as a note; the new one moves to `wontfix` with the reason "duplicate of N" |
| `duplicate` not certain, or `related` | the new issue is linked `relates-to` the older one; both gain a note saying so and why; nothing moves |
| `none` | the new issue gains a note: checked against so many issues, none the same |
The older issue is never moved, so the check never closes both sides. The tracker asks one judgement at a
time, and the backlog's pass (§6) waits while a new issue's check is waiting; one verdict is applied at a time. A verdict naming an issue that was not
offered is a failed judgement (§4), never acted on. The tracker's own code for the verbs does each step, so each
refuses what the verb refuses and says its event; the store is never written around them. Each note and link
records its author as `duplicate-check`, the way an import records `import` (ADR 0275 §5).
**An issue's report is data in the question, and the session has no tools.** A report written to steer the
verdict can at most close the issue that carries it, or link it: the verdict cannot move an older issue, touch a
resolved one or name an issue it was not offered.
### 3. Once per issue, whatever is delivered twice
The verdict note on the new issue is written **first**, and its first line says the verdict in a fixed form
("Duplicate check: duplicate of 412", "related to 412", "none of 85 issues is the same question"). An event
delivered again, or the backlog pass reaching an issue the event already checked, finds that note and does not
judge again: it does only the steps of that verdict not yet done (a link it holds, a note the older issue
holds, a status already `wontfix` are each done). An issue that is no longer at `open` when its event arrives is
not checked.
### 4. A judgement that fails is loud
A judgement fails when the agent module does not answer, answers late or refuses, or answers outside the shape
asked for, or names an issue it was not offered. Then the new issue gains one note saying the duplicate check
could not be made and why — once, not at every delivery — and the handler does not take the event. The bus
offers it again after five seconds; after the fifth time the controller raises `max-deliveries` on the tracker's
consumer, an open condition, and keeps the event for a person to deliver again. A tracker that cannot judge is
therefore never silent and never closes anything.
### 5. The judgement tool
`claude-code` gains `claude_code_judge`: a question in plain words and the JSON schema of the answer, optionally
a model alias. It runs `claude` in print mode, as the node's agent account, in an empty directory, with built-in
tools off, in safe mode and keeping no session, under the licence the node is bound to; and answers the
structured answer, the model and how long it took. It answers within 25 seconds or not at all (the process is
ended), so its caller hears a refusal rather than the bus's timeout. One judgement runs at a time on a node.
It refuses a question larger than 64 KiB, a schema that is not an object, and a node holding no licence, each in
words. The question is never logged or said in an event; the answer goes only to its caller.
### 6. The backlog: `groom`, a dry run first
The tracker's seat gains the verb `groom`, for the issues that were open before this check existed. It runs in
the background, because a judgement per issue takes longer than one call may:
- **Without `confirm`** it starts a dry run: every issue not resolved, oldest first, judged as in §1 against the
older ones not resolved and not already found a duplicate in this pass. It writes no note, link or move. Its
findings — each duplicate and related pair with the reason, and each judgement that failed — are kept as one
**pass** in the module's new state `grooming`, and `groom` with the pass's id answers its progress and
findings.
- **With `confirm` and a pass's id** it applies exactly that pass's duplicate and related findings, not a fresh
judgement, each checked again against the issues as they stand: a finding whose older issue has since been
resolved, or whose newer one has moved on, is skipped and said. It adds no "checked, none" note to the
backlog's issues.
One pass runs at a time. A pass's findings are kept until the next pass replaces them.
### 7. Settings
`duplicate-check` (`on` by default; `off` stops the check on new issues, said in the log) and `judge-model`
(`sonnet` by default; an alias the judgement tool accepts). Both are the module's ordinary settings.
## Consequences
- An issue opened twice is closed as a duplicate within the judgement's time of being opened, with the reason
on both issues and the duplicate's report kept on the older one; agents keep working on one number.
- Every open spends two short judgements of the control-node's licence. At the dozen or so issues a day the
mesh opens, that is noise beside one agent session.
- The duplicate check depends on the agent module and its licence on the tracker's node. When the licence is
gone, every new issue's event ends in `max-deliveries`: loud, and nothing is closed.
- Research 042's word-sharing candidate list for the duplicates pass is replaced by this judgement; to-be 53
slice 3 says so.
## How it is checked
| Rule | Checked by |
|---|---|
| A clear duplicate: linked, its report kept on the older one, the newer moved to wontfix with "duplicate of N" | the tracker's test with the judgement stubbed: the four steps, the verdict note first, each note naming `duplicate-check` |
| Related, or a duplicate not certain: linked `relates-to`, noted on both, nothing moved | the tracker's test, both cases |
| None: one note, nothing else | the tracker's test |
| A resolved or wontfix issue is never offered or changed; the older issue never moves | the tracker's test: a resolved and a wontfix issue in the backlog are absent from both questions; a verdict naming one, or naming an issue not offered, changes nothing and fails |
| Once per issue | the tracker's test: the same event twice judges once; an event after a partial apply finishes only the missing steps |
| A failed judgement is loud | the tracker's test: the handler returns an error, one note is written however often the event comes |
| The dry run changes nothing; confirm applies the pass's findings, re-checked | the tracker's test: issues byte-for-byte unchanged after a dry run; a finding whose older issue was resolved in between is skipped |
| The judgement session can do nothing but answer | the agent module's test of the command line: tools off, safe mode, no session kept, as the agent account, a 25-second bound; a question over 64 KiB and a node without a licence refused |
| Live | an agent opens an issue that repeats one not resolved; within a few minutes `get` shows it `wontfix`, "duplicate of N", linked `duplicates`, and N holds its report as a note; a second, unrelated issue gains only the note that it was checked |
## References
- [ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md) — the tracker this record extends
- [Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md) §3 — the duplicates pass
- [ADR 0206](0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md) and
[ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md) —
whose licence, and which account, a judgement runs under
- [To-be 53](../03-DESIGN/01-to-be/53-the-issue-tracker.md) — the design this record amends
@@ -237,7 +237,6 @@ withdrawn from routing and reported until it passes. A new word is a new version
| | `operator-first` | the operator can write unprompted | a simulated message yields a `message` envelope |
| trusting | `verified-sender` | the answer came from the operator's own account, through a holder no agent shares | a choice from an identity not on the list is dropped and reported; placement checked |
| | `exact-render` | the ask is shown as the router rendered it | rendered text equals the router's, byte for byte |
| | | *Amended 2026-10-10 (issue 383):* a channel may format the router's words (Telegram: the title bold, a key line and a fingerprint in monospace) and escapes its markup; it never rewords them | the words, formatting aside, equal the router's byte for byte — what a tap on the message is compared by |
| | `code-factor` | a typed code reaches the router by request and reply, never judged by the holder | a code never appears in an event; it is deleted from the conversation where the service allows |
| | `key-factor` | a security key's assertion over the router's challenge reaches the router | the challenge is the router's; the signature is checked by the router |
@@ -304,13 +303,6 @@ is not exempted.
- **A refusal is said to the sender** — the `notify` call or the ask is refused naming
the offending part, so the asker can rephrase or move the specific into a reference — and raises
`channel-refused`. The offending part is never sent to a channel.
- *Amended 2026-10-10, [ADR 0277](../../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)'s progressive insight (issue 383):* **an ask may carry its explanation
whole** (`Whole` beside `Explanation`), its paths and addresses as they are: what the operator approves must be
readable where they approve it. The router shows the whole only on an ask's own message, on a kind declaring
`verified-sender` that was given the ask's answers, and holds it to the secret rule alone; a secret's shape in
it refuses the ask as a whole. The rule above holds for everything else: every condition's words, and every
kind that proves nothing. An ask may also say what its **Details** answer shows (`Details`), held to the rule
line by line.
### References
+44 -10
View File
@@ -1,10 +1,11 @@
---
layer: to-be
status: in-progress
code: [mesh-catalog modules/mesh-issues, mesh-sdk]
code: [mesh-catalog modules/mesh-issues, mesh-catalog modules/claude-code, mesh-sdk]
updated: 2026-10-10
decisions:
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
- 02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md
---
# 53 — The issue tracker
@@ -35,10 +36,15 @@ each with what the operator sees when it is done.
├ 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,
├ verbs open, get, list, move, note, link, import, groom,
│ served by the control-node's tool runner
└ events opened, moved, noted, linked
├ events opened, moved, noted, linked, imported
└ consumes its own `opened`: the duplicate check (§ 9)
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
@@ -63,6 +69,8 @@ agent finds them with `mesh_search` ("open an issue") and calls them as `issue-t
| `move` | a number, a status, and what that status needs | 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 |
| `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.
@@ -84,10 +92,10 @@ caller; the module records that identity as who acted. No verb takes an actor.
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** keeps its status in that folder's frontmatter until the import of slice 3 has run
and been checked. Until then the tracker holds its number, title and `record`, takes its notes and links, and `move`
refuses it with the reason: its status lives in its folder. An issue without a folder is whole in the tracker from the
start. No status is written in two places for one issue.
**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
@@ -136,8 +144,8 @@ the events sees a change as it happens. No event carries a secret, and an issue
| `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](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)) |
| `wontfix` | a reason |
A refused move changes nothing and says which rule refused it. `cycle.py` keeps checking the folders that still carry
status until slice 3.
A refused move changes nothing and says which rule refused it. `cycle.py` checked the folders that carried status until
slice 3; since then it refuses a status key in a report opened after the import, and judges no issue's status.
## 6. How the operator and agents use it
@@ -156,6 +164,7 @@ in one call, from any session, and the responder on a channel answers from it on
| 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 |
| 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](../../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)'s "How it is checked" |
| 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
@@ -164,12 +173,37 @@ in one call, from any session, and the responder on a channel answers from it on
|---|---|---|
| 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 | 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; the four consolidation passes of research 042 (fixed, duplicates, owners, grouping), each a list confirmed before it is applied; an audit of `fixed-by` against the forge; then one pull request removes the status keys from every report, changes playbook 03 and `cycle.py`, and adds dated notes to ADR 0019 and 0080 | the not-closed list comes from one call and shrinks by what was already fixed or duplicated, each with its reason |
| 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 2026-10-10; the status keys stay in the reports as a dated snapshot, and one pull request changes playbook 03 and `cycle.py` and adds dated notes to ADR 0019, 0080 and 0275); 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 the controller shows the link; 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 shows 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 |
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](../../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)).
- **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` or `located`; 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`).
## What it leaves out
Priorities, claims, notifications, full-text or semantic search, the forge's own issue lists, and an index of any kind:
+10 -5
View File
@@ -22,22 +22,27 @@ both.
```
NNN-short-name/
00-report.md the symptom as observed, with the evidence; status in frontmatter
00-report.md the symptom as observed, with the evidence
01-diagnosis.md the investigation trail, dated, including what was ruled out
```
`NNN` is the number the issue tracker gave. The issue's status, owner (`located-in`), fix (`fixed-by`),
design amendment and replay live in the tracker — `issue-tracker.get NNN` — not here
([ADR 0275](../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
## Frontmatter, on `00-report.md`
```yaml
---
status: open | diagnosing | located | resolved | wontfix
opened: YYYY-MM-DD
located-in: [] # owning repo(s) or module(s), filled by diagnosis
fixed-by: # pull request or commit reference, filled at resolution
amended-design: # design doc path, when the root cause was a design gap
---
```
The report's first heading is `# NNN. The title`, the title the tracker holds. A report opened after the
import of slice 3 carries no `status:`, `located-in:`, `fixed-by:`, `amended-design:`, `replay:` or
`replay-none:`; `cycle.py` refuses them. The folders numbered 001 to 364 keep those keys as the snapshot the
import took, dated in ADR 0275; they are not kept up to date, and nobody edits them.
## Rules
- Anyone may open an issue. No localisation is required to report one.
+7 -5
View File
@@ -32,12 +32,13 @@ alongside `records.py`, `index.py` and `words.py` — run all four before any me
| How do I do X in this repo? | [`00-META/process/`](00-META/process/) — the playbook index is in `00-overview.md` |
| What was decided, and why? | [`02-DECISIONS/README.md`](02-DECISIONS/README.md) (reading order), then the record |
| What is being built / already runs? | [`03-DESIGN/01-to-be/`](03-DESIGN/01-to-be/) / [`03-DESIGN/00-as-is/`](03-DESIGN/00-as-is/) — each doc's frontmatter says its status, decisions and owning code |
| What is broken or was? | [`04-ISSUES/`](04-ISSUES/) — frontmatter carries status/owner/fix |
| What is broken or was? | the issue tracker: `issue-tracker.list` and `issue-tracker.get` through the mesh MCP server; a design or governance issue's narrative is in [`04-ISSUES/`](04-ISSUES/) under its number |
| Which repo owns what code? | [`00-META/repos.md`](00-META/repos.md) |
| Cross-cutting status view? | the `hq-status` skill (generated, never stored) |
Statuses live **only** in frontmatter; follow the pointers there (`decisions:`, `code:`,
`became:`, `fixed-by:`) instead of reconstructing history from memory.
Statuses of research, designs and decisions live **only** in frontmatter; follow the pointers there
(`decisions:`, `code:`, `became:`) instead of reconstructing history from memory. An issue's status,
owner and fix live in the issue tracker ([ADR 0275](02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
## Words
@@ -54,8 +55,9 @@ in running prose; a quotation keeps its words.
runs them on every pull request as its `mesh/repo-check` ([ADR 0238](02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)).
No new top-level folders without explicit confirmation.
- **Status lives in YAML frontmatter** — on research overviews (`status`, `became`), design
docs (`layer`, `status`, `code`, `updated`), issue reports (`status`, `located-in`,
`fixed-by`, `amended-design`) and decision records (`status`, `date`, `deciders`).
docs (`layer`, `status`, `code`, `updated`) and decision records (`status`, `date`, `deciders`).
An issue's status, owner and fix live in the issue tracker, not here: an issue report carries
`opened` and nothing of its status (playbook [03](00-META/process/03-issues.md)).
Never create a central status file; cross-cutting views are generated from frontmatter.
- **`02-DECISIONS/` records hold their meaning.** Supersede with a new record rather than rewriting
what was decided, the options weighed, or a consequence another record relies on. Fixing a broken