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
mesh-admin 1ea678c6bd Merge pull request 'ADR 0277: a trusted setting is proposed through a verb and set on the operator's warrant, and a module's own secret is asked for and typed at the desk' (#278) from decisions/0277-a-trusted-setting-is-proposed-and-set-on-the-operators-warrant into main 2026-10-10 12:50:19 +00:00
mesh-admin 76bb721cf4 Merge pull request 'Issue 362 resolved: ADR 0276 gains the own-path batch rule and a progressive insight, to-be 45 gains S18 and S19' (#277) from issues/362-resolved into main 2026-10-10 12:43:19 +00:00
jochen 3539446885 ADR 0277: the migrations are 0090 and 0091, and the code lands in two pull requests
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 14:42:47 +02:00
jochen 4d92d3872b ADR 0277: a trusted setting is proposed through a verb and set on the operator's warrant, and a module's own secret is asked for and typed at the desk
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 does not want to run manual commands on the control node. Issue 339 made every trusted
setting the terminal's alone, and a module's own secret the terminal's or the give verb's: each a line
an agent could only hand the operator to type. ADR 0274's pattern is extended: an agent proposes, the
operator approves on a channel that proves who answered, the controller sets the layer on the warrant
bound to the exact values shown. A secret is asked for by a verb and typed at the desk, bounded, the
prompt naming who asked. The designs 44, 52, 46 and 13, issue 339, ADR 0259 and 0266 and the glossary
are amended with dated notes.
2026-10-10 14:24:06 +02:00
jochen 952c984c34 Issue 362 resolved: ADR 0276 gains the own-path batch rule and a progressive insight, to-be 45 gains S18 and S19
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 14:14:56 +02:00
28 changed files with 871 additions and 193 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"))):
+11 -1
View File
@@ -279,7 +279,9 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
verb, so a change that is the terminal's alone is taken (issue 339, issue 340). In the controller's code it is a
verb, so a change that is the terminal's alone is taken (issue 339, issue 340) — or, since
[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),
proposed through a verb and set on the operator's warrant. In the controller's code it is a
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
agent can run as that account: the control-node, and a node whose agents run under an account of their own
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
@@ -501,6 +503,10 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
- **warrant** — the router's record that a person chose one option of one ask: the ask, the option,
who chose (the operator, as a channel kind's identity), through which channel, the proofs and when,
published for that asker alone. The only thing an asker acts on (ADR 0259).
- **proposal** — a change an agent, or anyone the bus admits, puts forward through a verb and that takes
effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR
0274), a trusted setting's layer ([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)).
A proposal changes nothing by itself; declined, expired or refused, it is discarded.
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
against its own list of the operator's identities: a **trusted** one is an operator message,
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
@@ -544,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
@@ -508,6 +508,13 @@ with the router not knowing its own machine, it approved. Now:
machine's launcher, carried by that machine's runtime, may answer its prompt; for any other module's
secret that remains the accepted risk of a desk where agents run as the operator.
> **The mechanism changed — 2026-10-10, by [ADR 0277](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).** The same prompt is opened by the
> controller's `secret-ask` verb from anywhere on the mesh, with the desk the module's machine unless named,
> and `give` composes its line. It is bounded (one open ask per secret, three an hour, read from the store
> before the prompt opens) and the prompt names who asked, from the bus's word on the caller alone. What
> stands: the value is typed at the desk, sealed to the one call, never in an argument, a log or an event;
> a trusted party's secret is still the terminal's.
**Where an ask can be answered is said by the router**, from the channels that qualify: "Answer on
Telegram, or from the mesh MCP server." The asker's words never name a channel.
@@ -262,6 +262,11 @@ here:
which seals to the machine that uses the secret, never to a key the caller chooses. **The `token` verb is
refused too**, because it handed the joining token to its caller. Enrolling a machine is now done at the
controller's terminal, by the operator;
> **The mechanism changed — 2026-10-10, by [ADR 0277](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).** Beside `secret rotate`, one other
> `secret` line is run through a verb: `secret ask <node> <module> <name> [--at <desk>]`, which carries no
> value and answers none — the operator types it at the desk's hidden prompt. And `settings proposals`
> joins the generic verb's reads; `settings propose` is the settings verb's. Everything else stands.
- `claude_code_add_api_key` read a caller's file as the operator, sealed it away and deleted it. It now takes
only a file under the agent's own home, reached through no symbolic link, read as the agent, at most 4 KiB,
holding an Anthropic API key. The licence manager refuses a key of another shape too;
@@ -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
@@ -164,6 +164,18 @@ either, because **delivery group** already names deliveries that share a head br
repository exist and that none names its merge, or that the controller holds none) and listed by
`stalled` at once, so probe D14 raises it for the operator.
> **Decided during the build, 2026-10-10** (on the operator's delegation). Decision 2 batches every merge of the
> window together, and decision 4 starts a walk on the controller's own path without mesh-delivery's word. A
> batch holding both a merge on that path and a catalogue merge would therefore start the catalogue's deliveries
> without their word: their turn in a delivery group, their holds, and the order of decision 5 would be skipped.
> So **a merge on the controller's own path never shares a batch with a merge that waits for the delivery's
> word.** Such a merge goes into a batch of its own, with its own window; the open catalogue batch closes and is
> cut by the normal rules, and queues behind the own-path walk like any other. Catalogue merges heard during the
> own-path batch's window join the next catalogue batch. An own-path walk being cut folds no catalogue walk that
> waits for its word: that walk's merges are batched again behind it, and the walk names that batch as the one
> that took it over, so no catalogue delivery ever skips its turn. Checked by the controller's test
> `TestAMergeOnTheControllersPathNeverSharesABatch`.
## Consequences
- **A merge waits up to 90 seconds before it is planned**, and up to 10 minutes in a busy period. A walk
@@ -181,6 +193,11 @@ either, because **delivery group** already names deliveries that share a head br
restart-safe; the walk's record names several repositories and commits instead of one, and every reader of
it (`plans`, S3, S16, `plan-moved`, mesh-delivery) changes with it. The store gains the batch and the
merges a walk answers, as a numbered migration and in the consolidated schema.
> **Progressive insight — 2026-10-10.** The controller has no consolidated schema file: a fresh install runs its
> numbered migrations from the first, embedded in the build, so the numbered migration (0089) is the whole of
> the schema change. The bullet above said "as a numbered migration and in the consolidated schema"; that second
> half describes mesh-delivery's kind of store, not the controller's. The decision stands.
- **The replay of issue 362** in the lab raises a mesh, merges two commits of one repository 18 seconds
apart, and passes when one walk delivers both without a person; it fails on the commit before the change.
@@ -0,0 +1,260 @@
---
topic: what runs on it
status: accepted
date: 2026-10-10
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md
---
# 277. A trusted setting is proposed through a verb and set on the operator's warrant, and a module's own secret is asked for and typed at the desk
## Context
[Issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md) found that any caller
of the controller's `settings` verb could have the node-engine hand a system directory to an account of the
caller's choice, and its fix made every **trusted setting** the controller's terminal's alone: `places` and
`accesses`, every key a provider serves, every setting a trusted file asks for, and the whole layer of a module
with a trusted mergeable file ([issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)).
The controller refuses them through any verb: "set at the controller's terminal only … whoever may call a verb
includes agents". A module's own secret issued outside the mesh is given the same way: `secret accept` at the
terminal, or the desk prompt the `give` verb opens ([ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)
§10, step 7).
That was right for the hole it closed and wrong for the operator's day. On 2026-10-10 the operator said: "you
should modify the settings yourself, I don't want to run manual commands on novox". The case at hand: the
`mounts` module's `sources` on the gaming machine's client, a trusted setting (a mount point on the machine,
asked for by a trusted file), and its `smb-credentials`, a secret issued outside the mesh. Each was a line the
agent could only hand the operator to type at the control node.
[ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)
established the pattern that removes the terminal from the loop without giving the decision to an agent: an
agent **proposes**, the operator **approves** on a channel that proves who answered (Telegram, placed where no
agent can become root), and the act is performed on the **warrant** — the router's record of the choice, bound
to the exact thing shown. The controller already acts on warrants for its own conditions (Release, Stop,
Restart, a rehearsal), claims each once, and holds the act to the digest the option bound.
The record was searched first ("settings propose warrant", "trusted setting approve", "secret ask desk
prompt"): nothing beside ADR 0274, issue 339's rule, ADR 0259 §10 and the operator's direction.
**Measured in the controller's code on 2026-10-10.** The `settings set` command is the one place a layer is
written; the terminal rule is keyed on the verb named in the process's environment. The `asked` bucket on the
bus is written by the controller alone, so a record there, unlike the agent module's `proposals` state (ADR
0274 §3), proves who kept it. A layer carries when it was set and not who set it. The router's content rule
refuses a path, an address or a secret's shape in any ask's words, and `sources` holds both a path and an
address. The desk's prompt (`node-launcher.secret`) is written by the launcher from the module's, the secret's
and the machine's names, says the controller asks, and takes no words of the caller's; the bus lets only the
controller ask it. Nothing bounded how often it could be asked.
### Against GENESIS
- **Failure must be loud.** A proposal nothing can carry to the operator is refused at once, in words, never
left waiting for an answer that cannot come.
- **The mesh owns the truth.** The layer is the mesh's record, set by the controller on a warrant it checked,
never a value an agent wrote; who approved it is kept beside it.
- **Effect — "when something genuinely needs a decision, you are asked, with the context".** The operator reads
the module, the machine and the exact change on their phone, and taps.
## Considered Options
### Who may set a trusted setting
1. **The controller's terminal alone** (issue 339's rule). Safe, and kept as one of two ways. Alone, every
placement and every mount is a trip to the control node, and an agent can only describe what to type.
2. **Let verbs set trusted settings outright**, trusting the caller. Rejected: an agent would then decide what
root mounts and owns on a machine, which is the hole issue 339 closed.
3. **An agent proposes through a verb; the operator approves on a channel that proves who answered; the
controller sets the layer on the warrant.** Chosen, beside 1.
### Who performs the set
1. **The verb's process, on hearing the warrant.** Rejected: a verb's process is short-lived and does not hear
the router's word; the serving controller is the asker that holds its asks and claims each once.
2. **The serving controller, in-process, as `settings set` at the terminal does.** Chosen. The same judgement
(the module's definition, a line break, a port per machine, a removal meant or refused), the same history,
the same words.
### What the operator reads
1. **The exact values, always.** Rejected: the router's content rule refuses a path, an address or a secret's
shape in any message that leaves the mesh, and a mount's source is both a path and an address.
2. **A fingerprint only.** Rejected: nobody approves what they cannot read.
3. **Every key with its exact new value where every word may leave the mesh; where not, the value in its own
shape with the part that may not leave replaced by a marker (‹address›, ‹path›, ‹withheld›), the layer's
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.
### 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
the installer's first user list in the node-engine's repository names every controller event one by one, so
it would cost a node-engine change and rollout for a fact without a reader.
2. **The hand-act log (on the bus), the layer's own record of who approved it, and the router's edit of the
ask on every channel to its outcome.** Chosen. The warrant is recorded under the verb `warrant`, which the
hand-act log counts as a person's decision; `settings` says "approved by the operator via telegram at …".
### How a module's own secret is asked for
1. **Through a verb's argument, or on Telegram.** Rejected: a verb's caller may be an agent, and a value typed
on Telegram passes a bot's server. A secret's value is typed where no agent and no outside party reads it.
2. **The desk prompt, as `give` opens it, asked for by a verb from anywhere, bounded, naming who asked.**
Chosen. The value is typed at the desk, sealed to the one call, sealed again to the module's machine, and
never in an argument, a log or an event.
## Decision
### 1. A trusted setting may be proposed through a verb, by anyone the bus admits
The controller's `settings` verb gains three arguments: `propose` (with `values`, or `clear`, and `replace` as
for a set), `proposals` and `proposal`. The command lines are `settings propose <module> <values | --clear>
[--node <node>] [--replace]` and `settings proposals [<id>]`, the operator's as `nox settings propose …`. A
proposal is for any key, the trusted ones included; `settings set` through a verb still refuses a trusted
setting (issue 339), and its refusal now names the proposal as the way.
A proposal **changes nothing**. It is kept in the controller's own asks on the bus (the `asked` bucket, which
the controller alone writes), with the module, the machine, the values whole, whether the layer is cleared or
a removal meant, the layer as it stood when proposed, who proposed it (the bus's word on the caller) and when,
and the digests of the values and of the layer they replace. It is judged as `settings set` judges before the
operator is asked, so no ask is raised for a layer the mesh would refuse, and a removal not marked `--replace`
is refused as [ADR 0217](0217-no-change-to-a-machine-takes-effect-unseen.md) refuses it at the terminal.
### 2. The ask
One ask per proposal, published under the controller's name on the `operator-channel` seat:
- **headline**: "Set mounts on shanks?" ("Clear …?", "… on the whole mesh?"), shortened when the names make
it longer than the router allows;
- **explanation**: the module and the machine; who proposed it and when; then the change against the layer as
it stands — "+ key: value" added, "~ key: value (was: old)" changed, "- key (was: old)" removed, and the
count of keys unchanged — each value exact where every word of it may leave the mesh, else in its shape
with ‹address›, ‹path› or ‹withheld› in place of what may not; the layer's **fingerprint** (the first 24
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;
- **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,
whether the removal is meant, whether it is a clear, who proposed it and when;
- **expiry**: a day less ten minutes after it was proposed; at expiry the proposal is discarded. An ask that
authorises never defaults (ADR 0259);
- **about**: the module and the machine, so a newer proposal for the same layer replaces an older one still
open, and the older is discarded.
### 3. The serving controller sets the layer on the warrant, and on nothing else
The controller acts on the warrant as on any other of its asks (ADR 0259 §6): only for an ask it holds, only
on a warrant whose ask digest is the one it kept, claimed once by compare-and-set so a warrant heard twice, or
replayed, changes nothing; only for an option the ask offered, at that option's level, before the ask expired.
Then, before anything is set:
1. the act about to be performed must be the one the chosen option bound, and **the values kept in the
record must digest to the digest in that act**: a record whose values, or whose layer-as-it-stood, changed
after the ask is refused and nothing is set;
2. the layer must still be the one the operator was shown the change against (to-be 46 §10, step 7): a layer
changed since, at the terminal or on another warrant, refuses the act, and the proposal is made again;
3. the layer is set, or cleared, exactly as `settings set` and `settings clear` do at the terminal, with the
same judgement and the same history kept, and **who approved it is kept beside the layer**: "approved by
the operator, as telegram identity …, via telegram (user id verified) at …, ask <id>, proposed by <who>".
`settings` says it back, so a layer the operator approved on their phone is told from one typed; a layer
set at the terminal says so, and one set through a verb names the verb.
On Decline nothing is set and the proposal is recorded as declined; on expiry, a cancel, a replacement or
the router's refusal nothing is set and the proposal is recorded as ended. Either way the warrant, or the
end, is recorded in the hand-act log under the verb `warrant`, a person's decision and never a repair, with
who chose, through which channel and with which proofs. **The push afterwards stays a separate act**, as it
is for a layer set at the terminal: the proposal's answer says `push <node>`.
### 4. Fail closed
A proposal is refused at once, in words, naming the terminal's line instead, when no module holding the
operator channel is assigned, when the bus does not yet hold the controller's grant to ask, when the ask could
not be published, or when the ask's words would carry what may not leave the mesh and the change could not be
shown without it. Published, the verb waits a few seconds for the router's word: a refusal (no channel can
carry an approval) is said to the caller, and the proposal is recorded as ended. The router caps open asks at
three per asker, the controller's conditions among them; a proposal that would be the fourth is refused before
it is asked, and the same change proposed twice is refused naming the first.
### 5. A module's own secret is asked for, and typed at the desk
The controller gains the verb **`secret-ask`** (`secret ask <node> <module> <name> [--at <desk>]`): the operator
is asked for the module's own secret in the desk's hidden prompt, on the module's machine or the one `at`
names, exactly as the `give` verb has it since ADR 0259 §10 — `give` now composes the same line. The value is
typed at the desk, sealed to the one call's key before it leaves that machine, opened in the controller, sealed
to the module's machine as `secret accept` seals it, and forgotten: **never in a verb's argument, never on a
channel, never in a log, an answer or an event**. It is refused, before anybody is asked to type, for a secret
the mesh issues itself (the module's bus account, one the mesh may make) and for a module that runs as an
account of its own, whose value is typed at the controller's terminal (ADR 0259 §10, N1-give).
**Bounded, and the prompt names who asked.** Every ask is recorded in the store before the prompt opens, with
who asked (the bus's word on the caller), the secret, the machine, the desk and how it ended: one open ask per
secret, and three an hour, read from that record, so an agent cannot keep a prompt in front of the operator
until they type. The prompt names the module, the secret, the machine and who asked: the caller as the bus
named it, cut to a name's characters by the controller and refused by the launcher in any other shape, never a
word the caller chose. Given, the value is recorded in the hand-act log with who asked, and announced on every
channel as every given secret is.
### 6. What this changes in the earlier records
- **Issue 339's rule** ("set at the controller's terminal only, never through a verb") gains a second way: a
trusted setting is set at the terminal, or on the operator's warrant. The refusal through a verb stands and
names the proposal.
- **ADR 0259 §10, step 7** (the desk prompt opened by `give`): the same prompt is opened by `secret-ask` from
anywhere, bounded, naming who asked; `give` composes its line.
- **ADR 0266** (every `secret` subcommand but `rotate` refused through a verb): `secret ask` is the one other
line a verb runs, carrying no value and answering none.
- **ADR 0217 §1** stands: a proposal that removes a key is refused unless `--replace` says the removal is
meant, and the removal is in the ask's words.
- **ADR 0262** stands: `settings` shows every value and where it came from, and now who set the layer.
## Consequences
- An agent sets a trusted setting with one call and one tap on the operator's phone, and asks for a secret
with one call and the operator's typing at their desk. No line is typed at the control node for either.
- No caller of a verb can set a trusted setting: what it writes is a proposal, and the controller sets the
layer only on a warrant bound to the exact values the operator was shown.
- **What got harder:**
- every trusted setting is a question to the operator, among at most three open asks of the controller's;
- a value with a path or an address is read in part on the phone and whole with `settings proposals`;
- a layer changed between the proposal and the approval refuses the act, and the proposal is made again;
- a secret is asked for at most three times an hour, and a prompt the operator did not start now names who
did start it, so they decline it;
- the store gains a column on the settings layers (migration 0090) and a table of secret asks (migration 0091).
## How it is checked
| Rule | Checked by |
|---|---|
| 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` |
| 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 |
| The warrant binds the exact values | `TestAWarrantSetsOnlyTheExactValuesTheOperatorWasShown`: the values or the layer-as-it-stood changed in the record, the action's digest changed, another ask's digest, the level acknowledge, after expiry, nobody chose, another asker's — each sets nothing |
| Set on the real store as at the terminal, judged, the history kept, who approved it said, a stale proposal refused | `TestSetOnAWarrantJudgesTheLayerAndKeepsWhoApprovedIt`: the layer set and `settings show` says who approved it; the same proposal again refused for a changed layer; a removal not meant refused; a line break refused; a clear keeps who cleared it with the history |
| A layer says who set it, at the terminal and through a verb | `TestALayerSaysWhoSetIt` |
| Fail closed | `TestAProposalFailsClosedWhenNothingCanCarryIt`: no router, no grant, a publish that fails, the router's refusal — each refused in words naming the terminal's line, nothing kept or marked unsent |
| Bounded | `TestAProposalIsBounded` (the same change twice; a fourth open ask), `TestOpenProposalsCountTowardTheAsksHeldOpen` |
| A trusted setting through the settings verb is still refused, naming the proposal | `TestATrustedSettingThroughAVerbNamesTheProposal`, and issue 339's tests unchanged |
| The verb composes propose, proposals and proposal; the generic command proposes nothing | `TestTheSettingsVerbComposesProposeAndProposals` |
| The secret's value is never in an argument, a log or an event | `TestASecretGivenAtTheDeskIsSealedAndSaidNowhere` (unchanged), `TestASecretAskIsNeverASecretRead` (a value on the line, a file, recover and export each refused) |
| The prompt names who asked from the bus's word alone, in a name's characters | `TestThePromptNamesWhoAskedFromTheBussWordAlone`; the launcher's `TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks` (a caller's name taken, a sentence refused) |
| One open ask per secret, three an hour, and every end recorded | `TestASecretAskIsBoundedAndItsEndRecorded`, and on the store `TestASecretAskIsOneAtATimeAndFewAnHour` |
| The `give` and `secret-ask` verbs run the one desk line, and no other `secret` line passes a verb | `TestTheGiveVerbRunsTheDeskPathAndTheControllerMayAskTheDesk`, `TestOnlyTheGiveLinePassesTheTerminalRuleForSecrets` |
| Live, after rollout | an agent proposes `sources` for mounts on the gaming machine's client; the operator's phone shows "Set mounts on <node>?" with the key, its value in shape, the fingerprint, Approve and Decline; after Approve `settings` shows the layer with "approved by the operator … via telegram", and `hand-acts` shows the warrant. An agent asks for `smb-credentials` there; a prompt on that desk names mounts, the secret and who asked; the operator types; the secret is sealed to that machine's mounts |
## References
- [ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)
— the pattern this record extends to settings and secrets
- [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md) — the ask,
the warrant, and the desk prompt (§10)
- [Issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md) — the rule this
record widens
- [ADR 0217](0217-no-change-to-a-machine-takes-effect-unseen.md), [ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)
— a layer replaced whole, and what `settings` says
- [ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)
— the terminal-only commands
- [to-be 44](../03-DESIGN/01-to-be/44-no-change-takes-effect-unseen.md) §1, [to-be 52](../03-DESIGN/01-to-be/52-shares-and-mounts.md)
§2, [to-be 46](../03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md) §10 — the designs amended
- mesh-controller `cmd/mesh-controller/proposals.go`, `desk_secret.go`, `internal/inventory/secret_ask.go`,
migrations 0090 and 0091; mesh-catalog `modules/rofi` (the prompt names who asked)
@@ -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
@@ -7,8 +7,9 @@ code:
- mesh-controller examples/postgres-provisioner
- mesh-controller internal/inventory/given.go
- mesh-controller cmd/mesh-controller/given.go
updated: 2026-10-06
updated: 2026-10-10
decisions:
- 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
- 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md
- 02-DECISIONS/0009-modules-and-the-graph.md
- 02-DECISIONS/0085-a-secret-is-a-provision.md
@@ -179,3 +180,10 @@ named and its age; the last is replaced when a person asks `secret rotate`. A pa
delivers is unchanged ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)).
*How it is checked:* the tests ADR 0228 names, and a live `rotate` of a given at-start secret through
the mesh MCP server.
*Amended 2026-10-10 by [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).* An outside party's value is also **asked for**: the
controller's `secret-ask` verb, from anywhere on the mesh, opens the desk's hidden prompt on the module's
machine (or the one named), naming the module, the secret and who asked; the operator types the value there,
sealed to the one call and then to the machine, never in an argument, a log or an event. One open ask per
secret, three an hour; refused for a secret the mesh makes itself and for a module running as an account of its
own, whose value stays the terminal's.
@@ -2,9 +2,10 @@
layer: to-be
status: in-progress
code:
- mesh-controller: cmd/mesh-controller/unseen.go, cmd/mesh-controller/push.go (--all, --move, the hold), cmd/mesh-controller/held_back.go (the hold in a named push's cascade), cmd/mesh-controller/plan.go (--diff), cmd/mesh-controller/modules.go (settings show, --replace), cmd/mesh-controller/seatverbs.go and internal/catalogue/verbs.go (the verbs' arguments), internal/inventory/unseen.go, internal/inventory/catalogue.go (the history kept on set and clear), internal/inventory/migrations/0078-what-a-change-replaces.sql
updated: 2026-10-08
- mesh-controller: cmd/mesh-controller/unseen.go, cmd/mesh-controller/push.go (--all, --move, the hold), cmd/mesh-controller/held_back.go (the hold in a named push's cascade), cmd/mesh-controller/plan.go (--diff), cmd/mesh-controller/modules.go (settings show, --replace), cmd/mesh-controller/seatverbs.go and internal/catalogue/verbs.go (the verbs' arguments), internal/inventory/unseen.go, internal/inventory/catalogue.go (the history kept on set and clear), internal/inventory/migrations/0078-what-a-change-replaces.sql, cmd/mesh-controller/proposals.go and internal/inventory/migrations/0090-a-layer-says-who-set-it.sql (a proposal set on a warrant, who set a layer)
updated: 2026-10-10
decisions:
- 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
- 02-DECISIONS/0217-no-change-to-a-machine-takes-effect-unseen.md
- 02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.md
---
@@ -29,6 +30,21 @@ That stays. Around it:
- **Keeping the previous layer.** Each set and each clear records the layer it replaced and when, so
the previous value is read back with `settings show --history`, not from a backup.
*Amended 2026-10-10 by [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).*
- **Who set it is kept with the layer.** A layer says who set it: at the controller's terminal by whom,
through which verb, or approved by the operator on which channel and when (ask id, proposed by whom).
`settings show` says it after the layer; the history copy keeps it.
- **A trusted setting is proposed and set on a warrant.** `settings propose <module> <values | --clear>
[--node] [--replace]` (the verb's `propose`) keeps a proposal in the controller's own asks and asks the
operator on a channel that proves who answers: the module, the machine, every key with its exact new value
(in its shape where a path or an address may not leave the mesh), the "was" of a changed key, the removed
keys, and the layer's fingerprint. Nothing changes until Approve, on which the serving controller sets the
layer as a set at the terminal would — judged the same, the removal meant or refused the same, the history
kept — only when the record's values still digest to what the option bound and the layer is still the one
shown. Decline, expiry or the router's refusal discard it. `settings proposals [<id>]` lists them and shows
one whole. The push afterwards is a separate act.
## 2. A push says what it will change
- **What was sent is kept.** Each send records a summary of the declaration it sent the machine, beside
@@ -4,6 +4,7 @@ status: in-progress
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
updated: 2026-10-10
decisions:
- 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
- 02-DECISIONS/0271-a-send-held-for-the-buss-planned-step-is-told-to-the-operator-at-once-and-a-change-that-needs-it-says-so-before-it-merges.md
- 02-DECISIONS/0265-a-verb-that-reads-is-answered-on-the-serving-controllers-own-connection-and-every-connection-says-what-it-is.md
- 02-DECISIONS/0264-a-message-a-consumer-gives-up-on-is-kept-until-a-person-delivers-it-again-or-drops-it.md
@@ -238,6 +239,8 @@ signal and asserts its condition. `doctor signals` shows, for every row, the age
| S15 | a hand act with a cause already recorded | hand-act log | each act | the second within 14 days; clears when fewer than two remain within 14 days | `healer-wanted` (names the cause, and the healer that was not enough where one answers it) | warning | — |
| S16 | a walk waiting for its delivery's word is let go | mesh-delivery, through `deliver` (ADR 0239) | each merge whose walk waits | 30 min; urgent after 4 h | `waiting` (names the walk and `plans go`) | warning | — |
| S17 | a send held for the bus's planned step is told to a person (ADR 0271) | controller's plan | each send refused because it would replace the bus outside its step | none: the first refusal; cleared once the bus's machine has been sent the build the mesh holds | `bus-step-waiting`, for the operator (names the machines, what waits, the bus build from and to, since when and the `mesh-controller.bus` call); S3 leaves such a walk out | warning | — |
| S18 | a batch of merges is cut into its walk (ADR 0276) | controller's merge window | each batch | a minute past `merge-window-at-most` while no walk is open: the controller failed to cut it | `batch-not-cut` (names the batch and what it holds) | warning | — |
| S19 | a batch waiting behind an open walk is cut when it ends (ADR 0276) | controller's merge window | each batch closed while a walk is open | that walk's bound, a tier's bound for each of its tiers, from when the batch closed | `batch-behind-walk` (names the walk) | warning | — |
The bus advisories (S9) cost one read-only subscription: the server already publishes them. The
controller translates each into a condition naming the thing in the mesh's words, as the refused reply
@@ -2,8 +2,9 @@
layer: to-be
status: designed
code: []
updated: 2026-10-08
updated: 2026-10-10
decisions:
- 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
- 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md
- 02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md
- 02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md
@@ -380,6 +381,7 @@ the factors are the router's state.
| approve or reject a waiting retirement set (`retire approve` / `retire reject`) | approve |
| confirm a kept binding moves (`pin`, while `binding-kept` names it) | approve |
| end a stuck plan; send a machine its declaration by hand; reset a bus consumer; retry a healer | approve |
| set or clear a trusted setting an agent proposed (`settings propose`, [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)) — bound to the exact values shown; the push stays a separate act | approve |
| silence a condition | acknowledge |
| delete a retired consumer's data, one or by age (`cleanup delete`) | destroy |
| change the operator's identities, the away channel, or a factor's enrolment | destroy |
@@ -130,6 +130,11 @@ controller's `plan-moved` event, never inferred from time.
- **One walk at a time.** Merges that arrive while a walk is open join the next batch. A walk that started
is never superseded. A walk still waiting for its word when the next batch is cut is folded into the new
walk, which answers every merge the folded one answered.
- **The controller's own path.** A merge that moves a module whose walk waits for nobody's word (the controller,
the node-engine, the tool runner, the bus, mesh-delivery) never shares a batch with one that waits for
mesh-delivery's: it has a batch of its own, with its own window, and the catalogue's batch queues behind its
walk like any other. An own-path walk folds no waiting catalogue walk; that walk's merges are batched again
behind it, so no catalogue delivery skips its turn (ADR 0276, decided during the build, 2026-10-10).
- **A late merge.** A merge heard after a later merge of its repository and branch was put in a walk is
answered by that walk: the assembling batch, the open walk, or, when that walk was delivered, it ends
`covered` at once.
+13 -5
View File
@@ -5,8 +5,9 @@ code:
- mesh-controller internal/catalogue (the seats node-nfs-server and node-mounts)
- mesh-catalog modules/nfs-server
- mesh-catalog modules/mounts
updated: 2026-10-08
updated: 2026-10-10
decisions:
- 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
- 02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md
- 02-DECISIONS/0051-shared-data-is-the-operators.md
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
@@ -103,8 +104,14 @@ The glossary holds the three words.
`name=disk:<uuid>@<mountpoint>`, each optionally `:ro`. `none` (the default) is none.
- **Its secret `smb-credentials`.** One section per SMB source, `[name]` with `username=` and
`password=`, issued outside the mesh and given with `secret accept <node> mounts smb-credentials
--from <file>`. The process writes one credentials file per source, readable by root only. A source
--from <file>` at the controller's terminal — or asked for by an agent with the controller's `secret-ask`
verb, on which a hidden prompt opens at that machine's desk naming the module, the secret and who asked,
and the operator types it there ([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)). The process writes one credentials file per source, readable by root only. A source
with no section is said with that remedy and is not mounted.
- **Its settings are trusted settings** (a trusted file asks for them), so a verb may not set them outright:
an agent proposes them (`settings propose mounts … --node <node>`) and the operator approves on their
phone, where the ask shows each source in its shape ("recalbox=‹address›@‹path›") and the layer's
fingerprint; the layer is set on the warrant and pushed after (ADR 0277).
- **What it refuses.** A mount point at, under or above any data the mesh keeps on this machine (the
controller's `data` verb, asked over the bus; until it is read nothing new is set up), a system path,
a mount point where something else is mounted or a folder that is not empty, and a share this machine
@@ -206,9 +213,10 @@ folders, none refused for a reason not understood.
export goes; the mesh's stays. *Undo:* `zfs set sharenfs=<the old value>` by a person, recorded as a
hand-act.
7. **The downloads**, the same way: read-only first, then read-write.
8. **The gaming device**: the operator gives `smb-credentials` with `secret accept`; the source is added
to `sources`; `node-mounts.list` says it `absent` while the device is off, and it mounts on access
when it is on. Then its fstab line is adopted.
8. **The gaming device**: an agent asks for `smb-credentials` with `secret-ask`, and the operator types it at
that machine's desk (or gives it with `secret accept` at the terminal); the agent proposes the source in
`sources`, and the operator approves it on their phone; `node-mounts.list` says it `absent` while the
device is off, and it mounts on access when it is on. Then its fstab line is adopted.
9. **The laptop**, read-only, granted read-only on the home server.
**Root.** Both modules' tools act as root through `sudo -n`, and the `sudo` module grants the operator
+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:
@@ -36,6 +36,12 @@ break adds a line of the caller's own to such a file.
## Why it is an issue
> **The mechanism changed — 2026-10-10, by [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).** The fix below made every trusted
> setting the controller's terminal's alone. That stands as one of two ways: a trusted setting is now also
> **proposed** through the `settings` verb by anyone the bus admits and **set by the controller only on the
> operator's warrant**, answered on a channel that proves who answered, bound to the exact values shown. The
> refusal through a verb stands, and names the proposal.
These settings were made for the operator. They say where an adopted machine keeps a module's data. But
the mesh has one way to write any setting, the `settings` verb, and whoever may call a verb includes every
agent session on a machine that runs the mesh MCP server. The generic `command` verb reaches the same
@@ -1,8 +1,9 @@
---
status: located
status: resolved
opened: 2026-10-10
located-in: [mesh-catalog modules/mesh-delivery (holder.go deliveryWalking), mesh-controller cmd/mesh-controller (upgrades.go SourceMoved, release_plan.go laterOnTheBranch)]
fixed-by:
fixed-by: mesh-catalog#171, mesh-controller#197
replay: R362
amended-design: 03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md
---
@@ -65,3 +66,14 @@ The operator decided the model that removes this shape, not only its bookkeeping
commit per repository, one walk at a time. The bookkeeping that tied a walk to an ended delivery is repaired as
well, and every wait of this shape has a bound and a condition. It is designed in to-be 47, *The merge window
and the batch*.
## Resolved
2026-10-10. mesh-delivery's side is mesh-catalog#171 (merged): a walk is tied to every live delivery whose
merge it answers, never to one that has ended, and a waiting walk no live delivery holds is listed by `stalled`
at once. The controller's side is mesh-controller#197: the merge window and the batch, one walk per batch with
one commit per repository, the walk naming every merge it answers, and the bounds of decision 9. The replay
R362 is registered in mesh-lab's replays register (mesh-lab#75), proved to fail on the commit before the
mesh-delivery fix and pass on it. The build raised one rule the record had not settled, decided during the
build and written into ADR 0276: a merge on the controller's own path never shares a batch with a catalogue
merge.
@@ -1,72 +0,0 @@
---
status: open
opened: 2026-10-09
located-in: []
fixed-by:
amended-design:
---
# 375. An agent cannot put a commit on the forge
## Symptom
On 2026-10-09, in an agent session on the operator's laptop, local times:
- An agent finished a change to this repository's connectivity design on a branch, committed it in a
worktree, and ran `merge-check.sh`; all four checks passed.
- `git push` was not possible: this repository's clone on the laptop has its push URL set to the literal
`DISABLED`. The laptop's clones of the mesh's code repositories push normally. Who set it, when, and
why is not recorded anywhere this session could find.
- The forge's tools have no verb that pushes a branch or makes a commit. `gitea_api` refused
`POST /repos/{owner}/{repo}/diffpatch`, the forge's endpoint that applies a patch to a branch: *not a
write gitea_api makes* ([ADR 0266](../../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)).
- The one file write `gitea_api` allows is the contents endpoint, which takes the **whole file**,
base64-encoded. The changed design document is about 90 KB, so the call would carry about 121 KB of
base64 that the agent must reproduce character for character. That is not a workable path.
The change waited for the operator to push it by hand, and so did this report.
*2026-10-10.* The push URL is no longer disabled, so `git push` over ssh works from the laptop again, with
the operator's key. Nobody recorded why it was disabled or why it was restored. The forge's tools are
unchanged: still 35, still no write that puts a commit on a branch.
## Why it is an issue
- **The mesh's own rule for a change cannot be followed by an agent.** Every change goes through a branch,
a pull request and a person's approval. An agent off the control node can make the branch locally and
open the pull request through `gitea_create_pull_request`, but it cannot put the commits between the two
on the forge.
- **Where no tool covers a need, none is worked around** ([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md)).
The only ways round are pushing to an explicit URL past the disabled one, or retyping a file as base64.
The first undoes a setting someone made on purpose. The second risks a commit that silently differs
from what was checked.
- **The control node already has an answer the laptop does not.** On the control node the agent account
holds a forge key of its own, so its pushes are its own (ADR 0266). An agent on any other machine has
no equivalent.
## Other gaps in the forge's tools
Found by listing the forge module's 35 tools on 2026-10-09. Only the first one blocks work. The others
can be done through `gitea_api`, which costs a raw call and a large answer each time.
| Gap | Today | Checked |
|---|---|---|
| **No push, commit or patch on a branch** | blocked, as above | yes |
| **No commit history or compare** | `GET …/commits` through `gitea_api`: about 100 lines of JSON for one commit, its author and committer printed in full each time. `gitea_contains` answers only whether one commit contains another. | yes |
| **No reading a pull request's reviews or line comments** | `gitea_list_comments` returns the conversation only. Reviews and comments on lines need `GET …/pulls/{n}/reviews` through `gitea_api`. An agent asked to address review feedback does not see it unless it knows to ask there. No *approve* tool is right, since approval is a person's. | yes |
| **No creating a branch** | `POST …/branches` through `gitea_api` | no |
| **No editing an issue** — title, body, labels | only close and reopen exist | no |
| **No listing a directory** | `gitea_get_file` reads one file | no |
| **No code search across repositories** | none | no |
*Checked* says whether this session called the raw endpoint and saw it answer. The rows marked *no* are
read from the tool list and the forge's API, not tried.
## Open
- Who disabled the push URL on the laptop's clone of this repository and who restored it, and whether
disabling it was meant to keep agents from pushing here or was a step in something else.
- Whether an agent off the control node should push with a forge key of its own, as on the control node,
or whether the forge module should take a commit as a patch and write it under the agent's account.
Today an agent off the control node pushes with the operator's key, so its pushes are not its own.
Either needs a decision record.
+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