Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81aad4189b | ||
|
|
d82dde3dca | ||
|
|
5f2f520018 | ||
|
|
f89c2c350d | ||
|
|
cf3065c660 | ||
|
|
894be9971f | ||
|
|
6d1310f1ea | ||
|
|
1ea678c6bd | ||
|
|
76bb721cf4 | ||
|
|
3539446885 | ||
|
|
4d92d3872b | ||
|
|
952c984c34 | ||
|
|
a5ff354eee | ||
|
|
8af56cf2ae | ||
|
|
beaeedca79 | ||
|
|
91f2a61763 | ||
|
|
cf529b567c | ||
|
|
8d9eae7224 | ||
|
|
2798306de7 | ||
|
|
d159bfa36a | ||
|
|
9395c752cc | ||
|
|
015ff1adfb | ||
|
|
835d7acc1a | ||
|
|
b18bf66ae2 | ||
|
|
a211644102 | ||
|
|
842ed805cc | ||
|
|
891608814e | ||
|
|
7c866e7356 | ||
|
|
7b096f2471 | ||
|
|
d457921486 | ||
|
|
014554ce9b | ||
|
|
591731ab54 | ||
|
|
b106518cda | ||
|
|
028f3b6ae5 | ||
|
|
f45f5f6f20 | ||
|
|
503e144ab6 | ||
|
|
2d0a6ba4bc | ||
|
|
a8e9a9afd4 | ||
|
|
c2d98bc61b | ||
|
|
851a5e9f45 | ||
|
|
997e422a32 | ||
|
|
496452fb54 | ||
|
|
511cea04f2 | ||
|
|
94719d4662 | ||
|
|
46f89cd102 | ||
|
|
288da29d94 | ||
|
|
4bc0919102 | ||
|
|
c0c7515e06 | ||
|
|
a8b9be8d7f | ||
|
|
fe970e2f00 | ||
|
|
c51aedea22 | ||
|
|
163521f32e | ||
|
|
32c0f6efb1 | ||
|
|
e68d0ffa1d | ||
|
|
fbaa8e16e6 | ||
|
|
0e7af9988a | ||
|
|
2cb779d06f | ||
|
|
d1b42b7430 | ||
|
|
19776115ef | ||
|
|
adbd57a1f9 | ||
|
|
b962e87cae | ||
|
|
189dbcef7a | ||
|
|
169200fe55 | ||
|
|
2349f4bb63 | ||
|
|
85da2fb93f | ||
|
|
1365ec17c8 | ||
|
|
a5433164c5 | ||
|
|
858e5413fa | ||
|
|
2b1edf046d | ||
|
|
2bceb5fbaa | ||
|
|
3d71532a3a | ||
|
|
7ade322bcb |
@@ -23,14 +23,17 @@ result is not "nothing to learn" — it is the reason the next person repeats th
|
|||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Read `00-report.md`. Move `status:` to `diagnosing`.
|
1. Read the issue: `mesh_call issue-tracker.get` with its number, and `00-report.md` when it has a
|
||||||
2. Investigate. Write `01-diagnosis.md` in the same folder: the trail, **dated**, including
|
folder. Move it to `diagnosing` with `issue-tracker.move`.
|
||||||
what was ruled out and how. Archaeology — which commit, which pull request, which date a
|
2. Investigate. The trail goes in `issue-tracker.note`, **dated** in its text, including what was
|
||||||
behaviour changed — is the most valuable content here.
|
ruled out and how; for an issue with a folder, in `01-diagnosis.md` there as well. Archaeology —
|
||||||
3. When the owner is known, set `status: located` and fill `located-in:` with repositories or
|
which commit, which pull request, which date a behaviour changed — is the most valuable content.
|
||||||
modules from [`00-META/repos.md`](../../../00-META/repos.md).
|
3. When the owner is known, `issue-tracker.move` to `located` with `located-in`: entries of the
|
||||||
4. On resolution: `status: resolved`, fill `fixed-by:`. If the root cause was a design gap, run
|
tracker's registered list of owners (repositories, and modules as `mesh-catalog modules/<module>`);
|
||||||
`hq-graduate` for the amendment and fill `amended-design:`.
|
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
|
## Rules
|
||||||
|
|
||||||
|
|||||||
@@ -21,27 +21,46 @@ Opens a numbered issue. **Authoritative playbook:**
|
|||||||
there, this is not an issue — it is a lookup. If the answer is a general lesson, it belongs in
|
there, this is not an issue — it is a lookup. If the answer is a general lesson, it belongs in
|
||||||
both.
|
both.
|
||||||
|
|
||||||
|
## The number comes from the issue tracker
|
||||||
|
|
||||||
|
Every issue is opened in the issue tracker (seat `issue-tracker`, module `mesh-issues`, ADR 0275). **Never take a
|
||||||
|
number by reading `main` or the open pull requests** — that is how numbers collided. If the tracker cannot be
|
||||||
|
reached, take no number and say so.
|
||||||
|
|
||||||
|
Decide the shape first:
|
||||||
|
|
||||||
|
- **Design or governance issue** — its narrative belongs in hq: it gets a folder `04-ISSUES/NNN-short-name/`.
|
||||||
|
- **Anything else** — it lives wholly in the tracker, and nothing is written in hq.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Next free number. Create `04-ISSUES/NNN-short-name/00-report.md` with:
|
1. Load `mesh_call` and `mesh_describe` (deferred tools of the `mesh` MCP server) and open the issue:
|
||||||
|
`mesh_call issue-tracker.open` with `title` (the symptom in one line), `report` (what was seen, what was expected,
|
||||||
|
how it was found — the evidence: what was run, what came back, when), optionally `located-in` when the owner is
|
||||||
|
already known, and — **only for an issue with a folder** — `record: short-name` (lower-case words joined by
|
||||||
|
hyphens, no number). The answer carries the number `NNN`.
|
||||||
|
2. **Issue wholly in the tracker:** done. Its trail goes in `issue-tracker.note`, its status changes through
|
||||||
|
`issue-tracker.move`, its links through `issue-tracker.link`.
|
||||||
|
3. **Issue with a folder:** on a branch off `origin/main`, create `04-ISSUES/NNN-short-name/00-report.md`, `NNN` being
|
||||||
|
exactly the tracker's number and `short-name` exactly the `record` passed:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
status: open
|
|
||||||
opened: YYYY-MM-DD
|
opened: YYYY-MM-DD
|
||||||
located-in: []
|
|
||||||
fixed-by:
|
|
||||||
amended-design:
|
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Write the symptom **as observed**, in plain terms, with the evidence that it happened —
|
then `# NNN. The title`, the title given to the tracker. **No status key**: the issue's status, owner and fix
|
||||||
what was run, what came back, when.
|
live in the tracker (ADR 0275), and `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`,
|
||||||
3. Say why it matters beyond the instance. An issue that is only one occurrence is a knowledge
|
`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.
|
base entry.
|
||||||
4. End with open questions rather than a proposed fix. Diagnosis is a separate step.
|
6. End with open questions rather than a proposed fix. Diagnosis is a separate step.
|
||||||
|
|
||||||
## Do not
|
## Do not
|
||||||
|
|
||||||
- Do not name nodes, domains, addresses or paths. This repository is public.
|
- Do not name nodes, domains, addresses or paths. This repository is public.
|
||||||
- Do not guess the owner — `located-in:` is filled by diagnosis, not by opening.
|
- Do not guess the owner — `located-in:` is filled by diagnosis, not by opening.
|
||||||
|
- Do not renumber a folder or reuse a number: a number never moves.
|
||||||
|
|||||||
@@ -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` |
|
| 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` |
|
| 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` |
|
| 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
|
## Steps
|
||||||
|
|
||||||
1. Read the frontmatter block from every file above — a grep across each tree is enough, no
|
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
|
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
|
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)):
|
([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
|
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
|
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.
|
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.
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
python3 00-META/checks/records.py structure: links, citations, supersession, topics
|
python3 00-META/checks/records.py structure: links, citations, supersession, topics
|
||||||
python3 00-META/checks/index.py every record's topic is in the reading order, and no list of records is stored
|
python3 00-META/checks/index.py every record's topic is in the reading order, and no list of records is stored
|
||||||
python3 00-META/checks/index.py --print the records, by topic, in reading order (generated, never written)
|
python3 00-META/checks/index.py --print the records, by topic, in reading order (generated, never written)
|
||||||
python3 00-META/checks/words.py the glossary's retired words are not used, and no word is defined twice
|
python3 00-META/checks/words.py the glossary's retired words are not used, no daemon's line is written, and no word is defined twice
|
||||||
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
|
python3 00-META/checks/words.py --list tools the words the catalogue's copy must list
|
||||||
|
python3 00-META/checks/words.py --list lines the daemons' lines the catalogue's copy must list
|
||||||
```
|
```
|
||||||
|
|
||||||
Non-zero exit on any problem, so it can be a gate rather than a report.
|
Non-zero exit on any problem, so it can be a gate rather than a report.
|
||||||
@@ -61,7 +62,20 @@ number is how a record is cited, and `main` lags every open pull request, so two
|
|||||||
allocate the same one). And a core issue — one opened from 2026-10-07 whose `located-in` names a core
|
allocate the same one). And a core issue — one opened from 2026-10-07 whose `located-in` names a core
|
||||||
repository — resolves only with `replay:` (an id in mesh-lab's replays register) or `replay-none:` saying
|
repository — resolves only with `replay:` (an id in 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));
|
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));
|
||||||
it failed on a resolved core issue carrying neither before it passed. `python3 00-META/checks/cycle.py`
|
it failed on a resolved core issue carrying neither before it passed. And an accepted record that puts work
|
||||||
|
off — "until renamed", "left for later", "not decided here", "postponed", "future work", "deferred", "a later
|
||||||
|
change" — names the issue that tracks it, as tracking it ("tracked by issue N", or "issue N tracks"), in that
|
||||||
|
paragraph or list item, or in a dated progressive insight that speaks about it: one in the same section, or one
|
||||||
|
that quotes it. An issue cited for another reason tracks nothing, and one insight does not excuse every deferral
|
||||||
|
in a record. A rejected option is not work put off and is not held, judged by its `##` section, so an option under a
|
||||||
|
`###` subheading of "Considered Options" is skipped too
|
||||||
|
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md):
|
||||||
|
ADR 0244 deferred the code renames with no issue, and two days later the operator believed them done). Held to
|
||||||
|
it: a record dated from 2026-10-09, and an older one corrected under the rule. Measured from ADR 0244's date
|
||||||
|
it found deferrals without an issue in ADR 0241, 0244 and 0270; held from 2026-10-09 it failed on 0270 and 0273
|
||||||
|
(whose "not decided here" sits beside a citation of issue 340 that tracks none of it), and on 0244's four
|
||||||
|
deferral places once its insight brought it under the rule — on three of them while the insight, in §6, named
|
||||||
|
the issue without quoting them — before each named its issue (355, 357, 354). `python3 00-META/checks/cycle.py`
|
||||||
|
|
||||||
## words.py
|
## words.py
|
||||||
|
|
||||||
@@ -75,6 +89,14 @@ never checked. It also fails when a glossary head word heads two entries or is a
|
|||||||
a graduated research effort kept as written. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
a graduated research effort kept as written. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||||
of the tools' retired words (`retired-words`) with the glossary.
|
of the tools' retired words (`retired-words`) with the glossary.
|
||||||
|
|
||||||
|
It also fails on a **daemon's line** ([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),
|
||||||
|
its insight of 2026-10-09): the operator types one command, `nox`, and a document names a `nox` line, never the
|
||||||
|
controller's or the node-engine's binary followed by one of its commands. The lines are the check's own list
|
||||||
|
(`DAEMON_LINES`); a command that is a line only with an argument after it is matched as a line is written. Looked
|
||||||
|
for in the same documents, from 2026-10-09 for research and issues, in the document's own words — code blocks and
|
||||||
|
spans included, quotations and block quotes taken out. With `MESH_CATALOG_DIR` set it compares the catalogue's copy
|
||||||
|
(`retired-lines`) with the list.
|
||||||
|
|
||||||
It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 —
|
It failed on something real before it passed: 581 uses of retired words in 63 documents on its first run, besides the glossary itself and research 034 —
|
||||||
"the host" for the node-engine in 30 designs, "control plane" in 10, the glossary's own entry for the tool runner —
|
"the host" for the node-engine in 30 designs, "control plane" in 10, the glossary's own entry for the tool runner —
|
||||||
and two glossary contradictions (the "console", the deprecated broker's seat), fixed in the change that
|
and two glossary contradictions (the "console", the deprecated broker's seat), fixed in the change that
|
||||||
|
|||||||
+129
-38
@@ -4,7 +4,7 @@
|
|||||||
The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research ->
|
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,
|
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
|
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:
|
What is enforced:
|
||||||
|
|
||||||
@@ -12,15 +12,15 @@ What is enforced:
|
|||||||
known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no
|
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
|
design without a decision -- and once `in-progress` or `implemented` it names
|
||||||
its owning code (`code:`) -- no development without a design that says where.
|
its owning code (`code:`) -- no development without a design that says where.
|
||||||
issues a known `status:`; once `located`, `located-in:` names the owner;
|
issues every report has frontmatter, and no two records share a number -- the number is
|
||||||
once `resolved`, `fixed-by:` says what fixed it (prose counts --
|
how a record is cited. An issue's status, owner, fix and replay live in the issue
|
||||||
"nothing, the capability existed" is an answer). And no two records share a
|
tracker since the import of slice 3 (ADR 0275, to-be 53), where `issue-tracker.move`
|
||||||
number -- the number is how a record is cited.
|
refuses at the write what this check refused at merge until then: a status outside
|
||||||
replays a core issue -- one opened from 2026-10-07 whose `located-in:` names a core repository --
|
the five, `located` without an owner, `resolved` without a fix, a core issue
|
||||||
resolves only with `replay:` (the replay's id in mesh-lab's replays register) or
|
resolved without its replay (ADR 0237), `wontfix` without a reason. A report opened
|
||||||
`replay-none:` (why no replay is possible): ADR 0237, to-be 45 §9. A replay is what
|
after the import carries none of those keys -- the tracker holds them, and two
|
||||||
proves a fix fails before and passes after; without one, a fixed class comes back
|
places holding one fact drift. The reports opened before keep theirs as the dated
|
||||||
through a different door (research 031: four such chains in one week).
|
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
|
research a known `status:`; a `graduated` overview says what it `became:`, and every
|
||||||
target it names exists.
|
target it names exists.
|
||||||
decisions every accepted record is REACHABLE from the cycle: cited by a design doc's
|
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
|
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.
|
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
|
Deliberately NOT enforced: as-is docs need no decisions (they describe what exists, not what
|
||||||
turned out not to be a defect has no owner), and as-is docs need no decisions (they
|
was decided).
|
||||||
describe what exists, not what was decided).
|
|
||||||
|
|
||||||
python3 00-META/checks/cycle.py
|
python3 00-META/checks/cycle.py
|
||||||
"""
|
"""
|
||||||
@@ -43,18 +42,83 @@ import sys
|
|||||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
|
DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"}
|
||||||
ISSUE_STATUSES = {"open", "diagnosing", "located", "resolved", "wontfix"}
|
|
||||||
RESEARCH_STATUSES = {"active", "graduated", "abandoned"}
|
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
|
# The keys an issue report carried while its status lived here, and the day from which a report carries
|
||||||
# tools and the console, the SDK's loops, and the catalogue's bus, forge (the announcer of merges) and
|
# none of them: the issue tracker holds them (ADR 0275, slice 3 of to-be 53), and `issue-tracker.move`
|
||||||
# build agent. A `located-in:` entry naming any of these makes an issue a core issue.
|
# enforces the cycle's rules on them at the write. A report opened on or after this day with one of these
|
||||||
CORE = re.compile(r"\b(mesh-controller|mesh-host|mesh-tools|mesh-sdk|node-tools|"
|
# keys is a status written in two places. The reports opened before keep theirs as the import's snapshot.
|
||||||
r"mesh-catalog\s+modules/(nats|gitea|build-agent))\b")
|
STATUS_IN_TRACKER_FROM = "2026-10-11"
|
||||||
|
TRACKER_KEYS = ("status", "located-in", "fixed-by", "amended-design", "replay", "replay-none")
|
||||||
|
|
||||||
# The day the rule began (ADR 0237): issues opened before it are not held to it.
|
# A decision that defers work names the issue that tracks it (issue 354). ADR 0244 left the code
|
||||||
REPLAYS_FROM = "2026-10-07"
|
# renames of the node-engine and the tool runner "for a later rename", with no issue, owner or date;
|
||||||
REPLAY_ID = re.compile(r"^R\d+\b")
|
# two days later the operator believed them done, and nothing in the record said otherwise. A deferral
|
||||||
|
# nothing tracks is indistinguishable from work that was done. Held to it: an accepted record dated
|
||||||
|
# from DEFERRALS_FROM, and an older one corrected under the rule (a progressive insight dated from then
|
||||||
|
# on): an edit made under the rule is held to it. The phrases are the ones records use to put work off;
|
||||||
|
# "for later" alone is not one, because records say "never queued for later" and "too narrow for later".
|
||||||
|
# The tracking issue is named as tracking it, beside the deferral or in a dated insight in the same section
|
||||||
|
# or quoting it (ISSUE_REF, covers).
|
||||||
|
DEFERRALS_FROM = "2026-10-09"
|
||||||
|
DEFERRAL = re.compile(r"(?i)until (?:it is |they are |the code (?:is )?)?renamed|until the code rename|"
|
||||||
|
r"\ba later rename|not renamed here|identifier to rename|"
|
||||||
|
r"left (?:for|to) (?:a )?later|not decided here|\bpostponed\b|future work|"
|
||||||
|
r"\bdeferred\b|later work|not built here|\ba later change")
|
||||||
|
# The issue that tracks a deferral is named as tracking it -- "tracked by issue N", or "issue N tracks" --
|
||||||
|
# because a record cites issues for many reasons, and an issue cited as context beside a deferral (ADR 0273
|
||||||
|
# names issue 340 in the very sentence that leaves a channel undecided) tracks nothing.
|
||||||
|
ISSUE_REF = re.compile(r"(?i)tracked (?:by|in|as)\s+\[?issue (\d{3})|\[?issue (\d{3})\]?(?:\([^)]*\))?\s+tracks\b")
|
||||||
|
INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2}-\d{2})")
|
||||||
|
|
||||||
|
|
||||||
|
def paragraphs(path):
|
||||||
|
"""A record's body as (first line number, section heading, chapter, text) paragraphs: frontmatter and fenced
|
||||||
|
code left out, a block quote's markers stripped, so a quoted insight reads as the paragraph it is. The
|
||||||
|
section is the nearest heading above the paragraph; a heading is its own paragraph and its own section.
|
||||||
|
The chapter is the nearest `#` or `##` heading, which a `###` subheading inside it does not change."""
|
||||||
|
text = open(path, encoding="utf-8").read()
|
||||||
|
m = re.match(r"^---\n.*?\n---\n?", text, re.S)
|
||||||
|
body, first = (text[m.end():], text[: m.end()].count("\n") + 1) if m else (text, 1)
|
||||||
|
body = re.sub(r"(?ms)^\s*```.*?^\s*```", lambda x: re.sub(r"[^\n]", " ", x.group(0)), body)
|
||||||
|
out, line, section, chapter = [], first, "", ""
|
||||||
|
for chunk in body.split("\n\n"):
|
||||||
|
stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip()
|
||||||
|
if stripped:
|
||||||
|
heading = re.match(r"^#{1,6}\s+(.*)", stripped)
|
||||||
|
if heading:
|
||||||
|
section = heading.group(1).strip()
|
||||||
|
if len(stripped) - len(stripped.lstrip("#")) <= 2:
|
||||||
|
chapter = section
|
||||||
|
# A list's top-level items are units of their own: a Consequences list is one paragraph, and an
|
||||||
|
# issue named in one item tracks nothing in the next.
|
||||||
|
items = re.split(r"\n(?=[-*] |\d+\. )", stripped)
|
||||||
|
at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n")
|
||||||
|
for item in items:
|
||||||
|
out.append((at, section, chapter, item))
|
||||||
|
at += item.count("\n") + 1
|
||||||
|
line += chunk.count("\n") + 2
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
QUOTE = re.compile(r"\"([^\"]{12,})\"|\u201c([^\u201d]{12,})\u201d")
|
||||||
|
|
||||||
|
|
||||||
|
def flat(text):
|
||||||
|
"""Text compared for a quotation: markup and case dropped, whitespace collapsed."""
|
||||||
|
return re.sub(r"\s+", " ", re.sub(r"[*`_]", "", text)).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def covers(insight_section, insight_text, section, text):
|
||||||
|
"""An insight speaks about a deferral paragraph when it stands in the same section, or quotes it."""
|
||||||
|
if insight_section == section:
|
||||||
|
return True
|
||||||
|
body = flat(text)
|
||||||
|
return any(flat(a or b) in body for a, b in QUOTE.findall(insight_text))
|
||||||
|
|
||||||
|
|
||||||
|
def issue_refs(text, issues):
|
||||||
|
return {n for a, b in ISSUE_REF.findall(text) for n in (a, b) if n and n in issues}
|
||||||
|
|
||||||
|
|
||||||
def rel(path):
|
def rel(path):
|
||||||
@@ -162,28 +226,23 @@ def main():
|
|||||||
else:
|
else:
|
||||||
seen_records[number] = name
|
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"))):
|
for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))):
|
||||||
front = frontmatter(path)
|
front = frontmatter(path)
|
||||||
if front is None:
|
if front is None:
|
||||||
bad(path, "no frontmatter")
|
bad(path, "no frontmatter")
|
||||||
continue
|
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 "")
|
opened = str(front.get("opened") or "")
|
||||||
if (status == "resolved" and core and opened >= REPLAYS_FROM and not replay
|
if not opened:
|
||||||
and not " ".join(listy(front, "replay-none")).strip()):
|
bad(path, "opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
|
||||||
bad(path, "a core issue resolved with no replay: name it in `replay:` (mesh-lab replays "
|
continue
|
||||||
"register.go) or say in `replay-none:` why none is possible (ADR 0237)")
|
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 ----------------------------------------------------------------------
|
# ---- research ----------------------------------------------------------------------
|
||||||
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
|
for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))):
|
||||||
@@ -233,6 +292,38 @@ def main():
|
|||||||
bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
|
bad(path, "an accepted decision nothing in the cycle cites -- give it a home in a "
|
||||||
"design doc's decisions:, a research became:, an issue, or 00-META")
|
"design doc's decisions:, a research became:, an issue, or 00-META")
|
||||||
|
|
||||||
|
# ---- deferrals -------------------------------------------------------------------
|
||||||
|
# A paragraph or list item that puts work off names the issue that tracks it, or a dated
|
||||||
|
# progressive insight that speaks about it (its section, or a quotation of it) does. An issue that has
|
||||||
|
# since resolved still counts: the deferred work landed, which is what the pointer was for.
|
||||||
|
for name, (path, status) in records.items():
|
||||||
|
front = frontmatter(path) or {}
|
||||||
|
decided = str(front.get("date") or "")
|
||||||
|
if status != "accepted":
|
||||||
|
continue
|
||||||
|
paras = paragraphs(path)
|
||||||
|
insights = [(d, sec, text) for line, sec, _, text in paras for d in INSIGHT.findall(text)]
|
||||||
|
held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _, _ in insights)
|
||||||
|
if not held:
|
||||||
|
continue
|
||||||
|
# A dated insight naming an issue covers the deferrals it speaks about -- those in its own section,
|
||||||
|
# or ones it quotes -- and no other: one insight must not excuse every deferral in the record.
|
||||||
|
tracking = [(sec, text) for d, sec, text in insights if issue_refs(text, seen)]
|
||||||
|
for line, section, chapter, text in paras:
|
||||||
|
m = DEFERRAL.search(text)
|
||||||
|
# A rejected option is a decision not to do it, not work put off -- judged by its chapter, so an
|
||||||
|
# option under a `###` subheading of "## Considered Options" is skipped as well.
|
||||||
|
if re.match(r"(?i)(options? rejected|considered options|rejected)", chapter):
|
||||||
|
continue
|
||||||
|
if not m or issue_refs(text, seen):
|
||||||
|
continue
|
||||||
|
if any(covers(isec, itext, section, text) for isec, itext in tracking):
|
||||||
|
continue
|
||||||
|
bad(path, "line %d defers work (%r) and names no issue that tracks it -- a deferral nothing "
|
||||||
|
"tracks reads as done (issue 354). Name the issue beside it, with its owner, or in a "
|
||||||
|
"dated progressive insight that says what it tracks"
|
||||||
|
% (line + text.count("\n", 0, m.start()), m.group(0)))
|
||||||
|
|
||||||
checked = (
|
checked = (
|
||||||
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
|
len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md")))
|
||||||
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
|
+ len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md")))
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ list only shrinks.
|
|||||||
| `01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md` | kept | as above: every clash names the words that clashed |
|
| `01-RESEARCH/034-the-mesh-in-domains/03-the-clashes.md` | kept | as above: every clash names the words that clashed |
|
||||||
| `01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md` | kept | as above: the check's own examples |
|
| `01-RESEARCH/034-the-mesh-in-domains/04-how-the-glossary-is-checked.md` | kept | as above: the check's own examples |
|
||||||
| `01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md` | kept | as above: the proposed glossary, with the entries of the day |
|
| `01-RESEARCH/034-the-mesh-in-domains/05-a-glossary-by-domain.md` | kept | as above: the proposed glossary, with the entries of the day |
|
||||||
|
| `01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md` | kept | the effort that settled the word: it records the operator's ask in the words it was made in |
|
||||||
|
|||||||
+114
-13
@@ -28,9 +28,23 @@ What is enforced:
|
|||||||
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
|
copy the catalogue's own check keeps (mesh-catalog `retired-words`). With
|
||||||
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
|
MESH_CATALOG_DIR set to a checkout of the catalogue it also fails when that copy differs.
|
||||||
|
|
||||||
|
lines no daemon's line in a checked document (ADR 0272, its insight of 2026-10-09). The operator
|
||||||
|
types one command, `nox` (`mesh-cli`); the node-engine (`nox-mesh-host`) and the
|
||||||
|
controller (`mesh-controller`) are daemons, and a person never types their names — so a
|
||||||
|
document names a `nox` line, never `mesh-controller push` or `mesh-host hand-over`. The
|
||||||
|
lines are DAEMON_LINES below: each daemon's commands, and which of them are a line only
|
||||||
|
with an argument after them ("the mesh-controller module" is prose; `mesh-controller
|
||||||
|
module check …` is a line). A line is looked for in the document's own words — code
|
||||||
|
blocks and code spans included, since that is where a line to type is written — with
|
||||||
|
quotations, block quotes, link targets and comments taken out. `--list lines` prints the
|
||||||
|
list, which the catalogue's own check keeps a copy of (mesh-catalog `retired-lines`); with
|
||||||
|
MESH_CATALOG_DIR set it also fails when that copy differs. The node-engine's install-time
|
||||||
|
subcommands (`nox-mesh-host enrol`, `key`, `reconcile`, …) are not lines here: they are
|
||||||
|
typed at a machine no mesh reaches yet, and the engine's usage says so.
|
||||||
|
|
||||||
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; a research
|
Checked documents: 00-META/, 03-DESIGN/ (both layers), AGENTS.md and README.md, always; a research
|
||||||
effort initiated, or an issue opened, on or after FROM. Never 02-DECISIONS/: a decision record keeps the
|
effort initiated, or an issue opened, on or after FROM (on or after LINES_FROM for the lines). Never
|
||||||
words it was written with.
|
02-DECISIONS/: a decision record keeps the words it was written with.
|
||||||
|
|
||||||
A document the check fails on that cannot be reworded in the change that found it is named, with a date
|
A document the check fails on that cannot be reworded in the change that found it is named, with a date
|
||||||
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
|
and a reason, in words-allowed.md beside this file. An entry past its date fails like the word itself, and
|
||||||
@@ -39,6 +53,7 @@ graduated research effort, which is a record of what was said, like a decision r
|
|||||||
|
|
||||||
python3 00-META/checks/words.py
|
python3 00-META/checks/words.py
|
||||||
python3 00-META/checks/words.py --list tools
|
python3 00-META/checks/words.py --list tools
|
||||||
|
python3 00-META/checks/words.py --list lines
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@@ -55,6 +70,25 @@ FROM = "2026-10-07"
|
|||||||
|
|
||||||
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
|
ALWAYS = ("00-META/", "03-DESIGN/", "AGENTS.md", "README.md")
|
||||||
|
|
||||||
|
# The daemons' lines (ADR 0272, its insight of 2026-10-09), and the day that rule began. "alone" is a
|
||||||
|
# command that is a line by itself; "with an argument" one that is a line only when written as one.
|
||||||
|
LINES_FROM = "2026-10-09"
|
||||||
|
DAEMON_LINES = {
|
||||||
|
"mesh-controller": {
|
||||||
|
"alone": ("adopt assign broker cancel cleanup collect conditions converge doctor durations hand-act "
|
||||||
|
"hand-acts healers identity kill licence migrate operator pause pin plan prepare push rebuild "
|
||||||
|
"rehearse replay resume retire rollout rotate secret settings status token unassign unpin "
|
||||||
|
"upgrade").split(),
|
||||||
|
"with an argument": ("artifacts ask builds bus collection data delivery images mirrors module node "
|
||||||
|
"overlay plans queue seat seats take version").split(),
|
||||||
|
},
|
||||||
|
"mesh-host": {
|
||||||
|
"alone": "apply bundle enrol enroll hand-over help inventory key overlay owned profile reconcile run version".split(),
|
||||||
|
"with an argument": [],
|
||||||
|
},
|
||||||
|
"nox-mesh-host": {"alone": ["hand-over"], "with an argument": []},
|
||||||
|
}
|
||||||
|
|
||||||
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
|
NOT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Not:\*(.*)$", re.M)
|
||||||
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
|
STRUCK = re.compile(r"~~([^~]+)~~(?:\s*\((hq|tools)\))?")
|
||||||
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
|
IDENT_LINE = re.compile(r"^\s*(?:[-*]\s+)?\*Identifier until renamed:\*(.*)$", re.M)
|
||||||
@@ -134,8 +168,8 @@ def prose(text, glossary_file=False):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def research_and_issues():
|
def research_and_issues(since=FROM):
|
||||||
"""The research efforts and issue reports dated on or after FROM, file by file."""
|
"""The research efforts and issue reports dated on or after `since`, file by file."""
|
||||||
out = []
|
out = []
|
||||||
for folder, key, first in (("01-RESEARCH", "initiated", "00-overview.md"), ("04-ISSUES", "opened", "00-report.md")):
|
for folder, key, first in (("01-RESEARCH", "initiated", "00-overview.md"), ("04-ISSUES", "opened", "00-report.md")):
|
||||||
base = os.path.join(ROOT, folder)
|
base = os.path.join(ROOT, folder)
|
||||||
@@ -144,7 +178,7 @@ def research_and_issues():
|
|||||||
if not os.path.isfile(head):
|
if not os.path.isfile(head):
|
||||||
continue
|
continue
|
||||||
date = frontmatter_field(read(rel(head)), key)
|
date = frontmatter_field(read(rel(head)), key)
|
||||||
if not date or date < FROM:
|
if not date or date < since:
|
||||||
continue
|
continue
|
||||||
for name in sorted(os.listdir(os.path.join(base, effort))):
|
for name in sorted(os.listdir(os.path.join(base, effort))):
|
||||||
if name.endswith(".md"):
|
if name.endswith(".md"):
|
||||||
@@ -152,7 +186,7 @@ def research_and_issues():
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def checked_documents():
|
def checked_documents(since=FROM):
|
||||||
docs = []
|
docs = []
|
||||||
for entry in ALWAYS:
|
for entry in ALWAYS:
|
||||||
path = os.path.join(ROOT, entry)
|
path = os.path.join(ROOT, entry)
|
||||||
@@ -164,7 +198,7 @@ def checked_documents():
|
|||||||
for name in sorted(files):
|
for name in sorted(files):
|
||||||
if name.endswith(".md"):
|
if name.endswith(".md"):
|
||||||
docs.append(rel(os.path.join(base, name)))
|
docs.append(rel(os.path.join(base, name)))
|
||||||
return docs + research_and_issues()
|
return docs + research_and_issues(since)
|
||||||
|
|
||||||
|
|
||||||
def allowed():
|
def allowed():
|
||||||
@@ -223,6 +257,60 @@ def check_retired(retired, identifiers):
|
|||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def lines_list():
|
||||||
|
"""The daemons' lines as the catalogue's copy lists them: `<daemon> <command>`, ` …` when an argument must follow."""
|
||||||
|
out = []
|
||||||
|
for daemon, kinds in DAEMON_LINES.items():
|
||||||
|
out += ["%s %s" % (daemon, c) for c in kinds["alone"]]
|
||||||
|
out += ["%s %s …" % (daemon, c) for c in kinds["with an argument"]]
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
|
||||||
|
def line_pattern(daemon, command, with_argument):
|
||||||
|
"""The line whole: the daemon, white space, the command. One that is a line only with an argument is matched
|
||||||
|
as a line is written — at the start of a line or a code span, a prompt allowed, an argument after it."""
|
||||||
|
d, c = re.escape(daemon), re.escape(command)
|
||||||
|
if with_argument:
|
||||||
|
return re.compile(r"(?m)(?:^|`)[ \t]*(?:\$[ \t]*)?(" + d + r"\s+" + c + r"\s+[^\s`])")
|
||||||
|
return re.compile(r"(?<![\w-])" + d + r"\s+" + c + r"(?![\w-])")
|
||||||
|
|
||||||
|
|
||||||
|
def own_words(text):
|
||||||
|
"""The text with what is not the document's own words blanked: as prose(), but a code block and a code span
|
||||||
|
are kept, since a line to type is written in one."""
|
||||||
|
if text.startswith("---\n"):
|
||||||
|
end = text.find("\n---", 4)
|
||||||
|
if end != -1:
|
||||||
|
text = re.sub(r"[^\n]", " ", text[: end + 4]) + text[end + 4:]
|
||||||
|
for step in (
|
||||||
|
r"(?s)<!--.*?-->", # comments
|
||||||
|
r"(?m)^\s*>.*$", # block quotes
|
||||||
|
r"\]\([^)\s]*\)", # link targets
|
||||||
|
r"~~[^~\n]+~~", # struck through
|
||||||
|
r"\"[^\"\n]*\"", # "quoted"
|
||||||
|
r"\u201c[^\u201d]*\u201d", # curly double quotes
|
||||||
|
r"\u2018[^\u2019\n]*\u2019", # curly single quotes
|
||||||
|
):
|
||||||
|
text = re.sub(step, blank, text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def check_lines():
|
||||||
|
problems = []
|
||||||
|
patterns = []
|
||||||
|
for daemon, kinds in DAEMON_LINES.items():
|
||||||
|
patterns += [(daemon, c, line_pattern(daemon, c, False)) for c in kinds["alone"]]
|
||||||
|
patterns += [(daemon, c, line_pattern(daemon, c, True)) for c in kinds["with an argument"]]
|
||||||
|
for doc in checked_documents(LINES_FROM):
|
||||||
|
text = own_words(read(doc))
|
||||||
|
for daemon, command, rx in patterns:
|
||||||
|
for m in rx.finditer(text):
|
||||||
|
line = text.count("\n", 0, m.start()) + 1
|
||||||
|
problems.append("%s:%d: `%s %s` is a daemon's line — the operator types `nox …` (ADR 0272)"
|
||||||
|
% (doc, line, daemon, command))
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def check_unique(retired, heads):
|
def check_unique(retired, heads):
|
||||||
problems = []
|
problems = []
|
||||||
seen = {}
|
seen = {}
|
||||||
@@ -253,29 +341,42 @@ def check_copy(retired):
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
|
return ["the catalogue's copy of the retired words cannot be read: %s" % err]
|
||||||
want = tools_list(retired)
|
want = tools_list(retired)
|
||||||
|
problems = []
|
||||||
if sorted(copy, key=str.lower) != want:
|
if sorted(copy, key=str.lower) != want:
|
||||||
return ["the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want)]
|
problems.append("the catalogue's retired-words differs from the glossary: it should list %s" % ", ".join(want))
|
||||||
return []
|
path = os.path.join(catalogue, "retired-lines")
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as handle:
|
||||||
|
copy = [l.strip() for l in handle if l.strip() and not l.startswith("#")]
|
||||||
|
except OSError as err:
|
||||||
|
return problems + ["the catalogue's copy of the daemons' lines cannot be read: %s" % err]
|
||||||
|
if sorted(copy) != lines_list():
|
||||||
|
problems.append("the catalogue's retired-lines differs from this check's list: regenerate it with "
|
||||||
|
"`python3 00-META/checks/words.py --list lines`")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
retired, identifiers, heads = glossary()
|
retired, identifiers, heads = glossary()
|
||||||
if argv[1:2] == ["--list"]:
|
if argv[1:2] == ["--list"]:
|
||||||
scope = argv[2] if len(argv) > 2 else "tools"
|
scope = argv[2] if len(argv) > 2 else "tools"
|
||||||
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
|
if scope == "lines":
|
||||||
|
words = lines_list()
|
||||||
|
else:
|
||||||
|
words = tools_list(retired) if scope == "tools" else sorted({w for w, s in retired if s in ("all", scope)})
|
||||||
print("\n".join(words))
|
print("\n".join(words))
|
||||||
return 0
|
return 0
|
||||||
if not retired:
|
if not retired:
|
||||||
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
print("words: the glossary names no retired word on a *Not:* line — the check would pass on anything")
|
||||||
return 1
|
return 1
|
||||||
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_copy(retired)
|
problems = check_unique(retired, heads) + check_retired(retired, identifiers) + check_lines() + check_copy(retired)
|
||||||
for p in problems:
|
for p in problems:
|
||||||
print(p)
|
print(p)
|
||||||
if problems:
|
if problems:
|
||||||
print("words: %d problem(s)" % len(problems))
|
print("words: %d problem(s)" % len(problems))
|
||||||
return 1
|
return 1
|
||||||
print("words: %d retired words and %d identifiers, none in running prose; %d head words, each once"
|
print("words: %d retired words and %d identifiers, none in running prose; %d daemons' lines, none written; "
|
||||||
% (len(retired), len(identifiers), len(heads)))
|
"%d head words, each once" % (len(retired), len(identifiers), len(lines_list()), len(heads)))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
-11
@@ -135,14 +135,16 @@ every other domain has something to run on. **Recorded by** the controller and t
|
|||||||
in the tools' descriptions it is also an SSH server's or a container runtime's own word, so a
|
in the tools' descriptions it is also an SSH server's or a container runtime's own word, so a
|
||||||
description that means the node-engine by it is found by review.
|
description that means the node-engine by it is found by review.
|
||||||
*Not:* ~~host agent~~, ~~the host~~ (hq), ~~node host~~
|
*Not:* ~~host agent~~, ~~the host~~ (hq), ~~node host~~
|
||||||
*Identifier until renamed:* `mesh-host` — the repository, its binary and its service unit
|
*Identifier until renamed:* `mesh-host` — the repository, its binary and its service unit; the rename is
|
||||||
|
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||||
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
|
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
|
||||||
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
|
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
|
||||||
supervises it as a process beside the services it runs, never a container
|
supervises it as a process beside the services it runs, never a container
|
||||||
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||||
which called it "node tools"). Its mode on loopback is the mesh MCP server.
|
which called it "node tools"). Its mode on loopback is the mesh MCP server.
|
||||||
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
|
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
|
||||||
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account
|
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account; the rename is
|
||||||
|
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||||
- **foundation** — the store and the bus, raised at genesis before any module system exists. The
|
- **foundation** — the store and the bus, raised at genesis before any module system exists. The
|
||||||
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
|
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
|
||||||
*Not:* ~~substrate~~
|
*Not:* ~~substrate~~
|
||||||
@@ -277,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
|
- **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**.
|
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
|
- **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
|
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
|
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)).
|
([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)).
|
||||||
@@ -287,7 +291,7 @@ and the operator act. **Recorded by** the controller's `identity` and `licences`
|
|||||||
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
|
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
|
||||||
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
|
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
|
||||||
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270.
|
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276.
|
||||||
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
|
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
|
||||||
|
|
||||||
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
|
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
|
||||||
@@ -324,6 +328,8 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
|||||||
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
|
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
|
||||||
being announced to one of its final states: one pull request, one status, one note
|
being announced to one of its final states: one pull request, one status, one note
|
||||||
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
||||||
|
From its merge on it is delivered by the walk of the batch that answers it, and ends with that walk
|
||||||
|
([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||||
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
|
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
|
||||||
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
|
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
|
||||||
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
||||||
@@ -343,10 +349,17 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
|||||||
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
|
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
|
||||||
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place.
|
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place.
|
||||||
*Not:* ~~change plan~~, ~~release plan~~
|
*Not:* ~~change plan~~, ~~release plan~~
|
||||||
|
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each
|
||||||
|
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
|
||||||
|
10 minutes after its first merge ([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||||
|
- **batch** — the merges one merge window collected, planned once as one walk. For a repository merged more
|
||||||
|
than once in it, the walk takes the latest commit, and the earlier merges are answered by it. Not *group*,
|
||||||
|
which is a delivery group.
|
||||||
- **mesh-delivery** — the module that owns deliveries and delivery groups, holding the mesh-scoped seat
|
- **mesh-delivery** — the module that owns deliveries and delivery groups, holding the mesh-scoped seat
|
||||||
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
|
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
|
||||||
- **walk** — the controller's sending of one trunk commit's builds across nodes, tier by tier, one node
|
- **walk** — the controller's sending of one batch's builds across nodes, at most one trunk commit per
|
||||||
first and judged at the first-node gate, then the rest (ADR 0236). A primitive the delivering stage
|
repository, tier by tier, one node first and judged at the first-node gate, then the rest (ADR 0236,
|
||||||
|
[ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
|
||||||
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
|
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
|
||||||
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
|
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
|
||||||
a module *rolls out* by its `roll` policy.
|
a module *rolls out* by its `roll` policy.
|
||||||
@@ -466,7 +479,11 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
|||||||
([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)).
|
([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)).
|
||||||
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
|
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
|
||||||
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
|
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
|
||||||
alias alone.
|
alias alone. It is the one command the operator types: every line the mesh addresses to the operator — a
|
||||||
|
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
|
||||||
|
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse
|
||||||
|
([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),
|
||||||
|
its insight of 2026-10-09).
|
||||||
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
|
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
|
||||||
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
|
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
|
||||||
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
|
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
|
||||||
@@ -486,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,
|
- **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,
|
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).
|
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
|
- **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,
|
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
|
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
|
||||||
@@ -495,11 +516,24 @@ asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
|||||||
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
|
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
|
||||||
- **agent** — a coding agent: the operator's, or one the mesh runs. Not the node-engine, and not the
|
- **agent** — a coding agent: the operator's, or one the mesh runs. Not the node-engine, and not the
|
||||||
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
|
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
|
||||||
|
- **instruction section** — one named section of the managed instruction file, which every agent session on the
|
||||||
|
nodes it is for reads first, as the mesh's word: set at the controller's terminal in the agent module's
|
||||||
|
settings, or **proposed** by an agent and made active on each node only by the operator's warrant, which that node
|
||||||
|
proves itself ([ADR 0274](../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)). A hand-over between sessions is one.
|
||||||
|
- **personal rule** — one short text in the operator's own words about how the operator wants their agent
|
||||||
|
to work with them, registered for the operator (the **operator scope** of the agent module) and rendered
|
||||||
|
into the agent's home on every node that names an operator account. Not a mesh-wide instruction section,
|
||||||
|
which is the mesh's word about the mesh, set at the controller's terminal
|
||||||
|
([ADR 0273](../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
|
||||||
|
- **lesson** — an agent's proposal, recorded when the operator expresses frustration or corrects a way of
|
||||||
|
working: the trigger (the operator's words), the cause (what the agent did) and the rule that would
|
||||||
|
prevent it. A lesson is `proposed`, `accepted` or `declined`; an accepted lesson is a personal rule, and a
|
||||||
|
proposed one is never read as one (ADR 0273).
|
||||||
|
|
||||||
## The record — how this repository works
|
## The record — how this repository works
|
||||||
|
|
||||||
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
|
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
|
||||||
**Recorded in** this repository. **Decided by** ADR 0019, 0080, 0244.
|
**Recorded in** this repository and the issue tracker. **Decided by** ADR 0019, 0080, 0244, 0275.
|
||||||
|
|
||||||
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation in `01-RESEARCH/`;
|
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation in `01-RESEARCH/`;
|
||||||
its closing into a decision and a design; a design given to a code repository; a documented workflow
|
its closing into a decision and a design; a design given to a code repository; a documented workflow
|
||||||
@@ -508,8 +542,18 @@ Not a domain of the mesh but of the way it is built, listed because its words me
|
|||||||
record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
|
record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
|
||||||
*record* has other meanings (see [Homonyms](#homonyms)).
|
*record* has other meanings (see [Homonyms](#homonyms)).
|
||||||
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
|
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
|
||||||
- **issue** — a report in `04-ISSUES/` of something wrong with the mesh at the level of design or
|
- **issue** — a question about the mesh that someone must answer, numbered and tracked by the issue tracker;
|
||||||
governance; an **incident** is a past event that taught a rule.
|
one at the level of design or governance also has a report in `04-ISSUES/` under the same number
|
||||||
|
([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)).
|
||||||
|
An **incident** is a past event that taught a rule.
|
||||||
|
- **issue tracker** — the role that holds the mesh's issues and hands out their numbers: the seat `issue-tracker`,
|
||||||
|
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.
|
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
|
||||||
|
|
||||||
## Homonyms
|
## Homonyms
|
||||||
@@ -523,7 +567,7 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement |
|
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement |
|
||||||
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
|
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
|
||||||
| | the sending of one commit across nodes | **walk** (verb `plans`) | Change and delivery |
|
| | the sending of one batch of commits across nodes | **walk** (verb `plans`) | Change and delivery |
|
||||||
| | a step a person starts, like the bus's | **planned step** | Core |
|
| | a step a person starts, like the bus's | **planned step** | Core |
|
||||||
| membership | the subjects a bus account is issued | **membership** | Provisioning |
|
| membership | the subjects a bus account is issued | **membership** | Provisioning |
|
||||||
| | an account in a group of the machine | **account group** | Placement |
|
| | an account in a group of the machine | **account group** | Placement |
|
||||||
@@ -556,6 +600,8 @@ renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
|||||||
| | a human | **operator** or **person** | words every domain uses |
|
| | a human | **operator** or **person** | words every domain uses |
|
||||||
| firewall | what the mesh enforces | **packet filter** | Connectivity |
|
| firewall | what the mesh enforces | **packet filter** | Connectivity |
|
||||||
| | what was found on a machine | **found firewall** | Connectivity |
|
| | what was found on a machine | **found firewall** | Connectivity |
|
||||||
|
| ticket | an answer's one-time token (ADR 0258) | **answer ticket** | Operator and conversation |
|
||||||
|
| | a question someone must answer | **issue** | The record |
|
||||||
| deploy | the per-node part of a delivery plan | **deploy plan** | Change and delivery |
|
| deploy | the per-node part of a delivery plan | **deploy plan** | Change and delivery |
|
||||||
| | a wrapped program's own deploy | its own word | — |
|
| | a wrapped program's own deploy | its own word | — |
|
||||||
|
|
||||||
|
|||||||
@@ -60,17 +60,22 @@ the expensive half.
|
|||||||
|
|
||||||
The flow above is a rule, and a rule states how it is checked:
|
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
|
[`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
|
decision, an `in-progress`/`implemented` design that names no owning code, two issue folders
|
||||||
`located`/`fixed` with no owner or `fixed`/`resolved` with no fix, and a `graduated`
|
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
|
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):
|
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.
|
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
|
Research overviews, design docs and decision records each carry their status as
|
||||||
YAML frontmatter (schemas in the section READMEs and playbooks). There are **no central status
|
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
|
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
|
[`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,
|
it is not worth a record it is not recorded. Cross-cutting views, the decision index included,
|
||||||
|
|||||||
@@ -13,6 +13,13 @@
|
|||||||
2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free
|
2. **Record the decision.** Write a record in [`02-DECISIONS/`](../../02-DECISIONS/) taking the next free
|
||||||
number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence,
|
number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence,
|
||||||
not assertion, and record the options that were rejected — that is the half worth keeping.
|
not assertion, and record the options that were rejected — that is the half worth keeping.
|
||||||
|
**A record that puts work off** — a rename, a check, a case it does not handle — names the issue that
|
||||||
|
tracks it and the owner who will do it, in the sentence that defers it ("tracked by issue N"), or the
|
||||||
|
deferral is not accepted:
|
||||||
|
a deferral nothing tracks reads as done
|
||||||
|
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
|
||||||
|
ADR 0244's code renames). [`cycle.py`](../checks/cycle.py) holds every accepted record dated from
|
||||||
|
2026-10-09, and an older one corrected under the rule, to it.
|
||||||
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
|
3. **Write the design.** Create the document under `03-DESIGN/01-to-be/` with frontmatter:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -19,46 +19,83 @@ 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
|
folder is not a second copy of it. An issue here is a question HQ must **answer**, not an
|
||||||
incident someone must **clear**.
|
incident someone must **clear**.
|
||||||
|
|
||||||
|
## 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),
|
||||||
|
[to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md)). Its number comes from one counter advanced by
|
||||||
|
compare-and-set, so two authors opening at the same moment never get the same one. **A number is never taken by
|
||||||
|
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 folder holds the report and the diagnosis; the tracker holds everything else.
|
||||||
|
- **Wholly in the tracker** — every other issue. Nothing is written here.
|
||||||
|
|
||||||
|
**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
|
## Steps
|
||||||
|
|
||||||
1. Take the next free number — **across `main` and every open pull request**, not `main` alone.
|
1. Open the issue in the tracker, through the mesh MCP server:
|
||||||
Work sits on unmerged branches for days, so two people both reading `main` allocate the same
|
`mesh_call issue-tracker.open` with `title`, `report` (the symptom **as observed**, in plain terms, with the
|
||||||
number; it happened twice in one hour between two machines, and the second collision reached
|
evidence that it happened), optionally `located-in` (entries of the tracker's registered list of owners), and, for
|
||||||
`main` with every check passing (issue 155). `cycle.py` now refuses two records sharing a number,
|
an issue that gets a folder here, `record` — the folder's name without its number, `short-name`. The tracker
|
||||||
which catches a collision but does not prevent one. Create
|
answers with the number `NNN`; a refused open spends none.
|
||||||
`04-ISSUES/NNN-short-name/00-report.md`:
|
|
||||||
|
**An issue with a folder** then creates `04-ISSUES/NNN-short-name/00-report.md` under exactly that number, on a
|
||||||
|
branch:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
status: open
|
|
||||||
opened: YYYY-MM-DD
|
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**, in plain terms, with the evidence that it happened.
|
Then `# NNN. The title`, the title as the tracker holds it, and the symptom as observed, as in the tracker's
|
||||||
2. Investigate in `01-diagnosis.md` in the same folder — the trail, dated, including what was
|
report. **No status key**: `cycle.py` refuses `status:`, `located-in:`, `fixed-by:`, `amended-design:`,
|
||||||
ruled out. Move `status:` to `diagnosing`, then `located` once the owner is known.
|
`replay:` or `replay-none:` in a report opened after the import, because the tracker holds them and two places
|
||||||
3. Resolve. Set `status: resolved`, fill `fixed-by:`, and if the root cause was a design gap,
|
holding one fact drift.
|
||||||
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
|
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
|
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
|
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).
|
([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
|
## Rules
|
||||||
|
|
||||||
- Closed issues are never deleted — they are the mesh's symptom-to-component memory.
|
- 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
|
branch is deleted when it merges, so a branch name there is a pointer that resolves to nothing by
|
||||||
the time anybody follows it.
|
the time anybody follows it.
|
||||||
- A fix that turns out to have broken something else is written back into the record that asked for
|
- A fix that turns out to have broken something else is written back into the record that asked for
|
||||||
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
|
it, pointing at the new issue. Somebody arriving at a record to learn why the code is the way it
|
||||||
is must not have to already know there was a sequel.
|
is must not have to already know there was a sequel.
|
||||||
- Renumbering a collision happens once, in the branch that lands last. Renumbering a branch whose
|
- A number never moves. A folder carries the number the tracker gave; a folder whose number collides with
|
||||||
author is still pushing only moves the race.
|
another (one numbered before the tracker, or by hand) is a fault, and `cycle.py` refuses two records
|
||||||
|
sharing a number.
|
||||||
- An issue whose answer is a general lesson should also be written to the knowledge base, so
|
- 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.
|
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.
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- Every merge is a human checkpoint, without exception.
|
- Every merge is a human checkpoint, without exception.
|
||||||
|
- What the handoff leaves for later is an issue with an owner, named in the design or the decision that
|
||||||
|
leaves it — never a sentence saying "later". The design's `code:` names who owns what is built; the issue
|
||||||
|
names who owns what is not. [`cycle.py`](../checks/cycle.py) checks the decision record
|
||||||
|
([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)).
|
||||||
- Never open a pull request unprompted. A permissions list saying it is allowed is not a
|
- Never open a pull request unprompted. A permissions list saying it is allowed is not a
|
||||||
request.
|
request.
|
||||||
- Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout
|
- Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
initiated: 2026-10-09
|
||||||
|
touches:
|
||||||
|
- 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
|
||||||
|
- 03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md
|
||||||
|
- 03-DESIGN/00-as-is/15-the-agent-and-its-licences.md
|
||||||
|
- 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
|
||||||
|
- 02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md
|
||||||
|
- 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md
|
||||||
|
- 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
|
||||||
|
- 02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md
|
||||||
|
- 04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md
|
||||||
|
became:
|
||||||
|
- 02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md
|
||||||
|
- 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 040 — The agent's home is the module's, and the operator's rules and lessons are registered through it
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
Three things about the coding agent's own directory under a person's home (`~/.claude`), investigated
|
||||||
|
together because the operator raised them as one on 2026-10-09:
|
||||||
|
|
||||||
|
1. **Who owns the configuration files in that directory.** [ADR 0216](../../02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)
|
||||||
|
gave the agent module a home scope that "owns only what it placed"; everything else there is held as
|
||||||
|
found ([ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)).
|
||||||
|
The operator's direction reverses that for the configuration: *"the home-scope does own ~/.claude, so
|
||||||
|
it should be completely empty and manual edits should be forbidden."*
|
||||||
|
2. **Whose the rules in that directory are.** The rule files a person writes there today are the
|
||||||
|
person's own, on one machine. The operator's direction: *"Rules in my home directory are my 'personal
|
||||||
|
rules' which should also end up under control of the claude-code module and all other changes to
|
||||||
|
~/.claude should go through the claude-code module's mcp tools."*
|
||||||
|
3. **How an agent learns from a correction.** The operator's direction: *"register the decision, but make
|
||||||
|
sure it's clear and that you can/should be pro-active about it. For example, if I'm frustrated, try to
|
||||||
|
see why and try to prevent it in the future by warning yourself about stuff not to do to frustrate me
|
||||||
|
— kind of like a self-learning process."*
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The home is the one place the agent's configuration still drifts. [01](01-what-the-home-holds-and-the-options.md)
|
||||||
|
measured it on 2026-10-09 across the four nodes: a skill to-be 36 retired in its first version is still on
|
||||||
|
all four; a person's instruction file is on three; the operator's two hand-written rules are on one; and
|
||||||
|
nothing in the mesh places, watches or raises anything about any of it. The corrections the operator has
|
||||||
|
given the agent live in the agent's own memory for one repository on one machine, 45 notes of which at least
|
||||||
|
9 are a correction of a way of working, shown to nobody and reaching no other node.
|
||||||
|
|
||||||
|
[Issue 340](../../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)
|
||||||
|
closed the tools' route into the operator's home for the right reason: a tool cannot tell the operator from
|
||||||
|
an agent, so an agent could plant something every session obeys. Whatever owns the home must keep that
|
||||||
|
protection. The two routes the mesh now has for the operator's own word, the controller's terminal
|
||||||
|
([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))
|
||||||
|
and a warrant on an ask ([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)),
|
||||||
|
make it possible to write into the operator's home without trusting a tool's caller.
|
||||||
|
|
||||||
|
The record was searched first: `records_search` for "personal rules lessons frustration self-learning" and
|
||||||
|
for "hand edit home .claude drift condition" found nothing on 2026-10-09.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **The agent module's design** ([to-be 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md)
|
||||||
|
§1 and §8): what under the home is the module's, and the scopes.
|
||||||
|
- **ADR 0216 §5 and §6** (the home scope owns only what it placed; what it did not place is reported) and
|
||||||
|
**ADR 0243** (removal on the person's word): the mechanism changes, the decisions stand.
|
||||||
|
- **ADR 0182**: the class table holds; what changes is which class the configuration files are in.
|
||||||
|
- **ADR 0245**: the guard gains a refusal, and the session gains a reminder.
|
||||||
|
- **ADR 0259**: the module becomes an asker, for the operator's yes.
|
||||||
|
- **Issue 340**: the protection stands; the terminal and the warrant are the two ways in.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [01 — What the home holds, and the options](01-what-the-home-holds-and-the-options.md): the measurement
|
||||||
|
on four nodes, the agent's own memory as it stands, and the options weighed.
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
# 040 — What the home holds, and the options
|
||||||
|
|
||||||
|
## 1. The agent's directory on the laptop, 2026-10-09
|
||||||
|
|
||||||
|
Read by name on the laptop's own disk, contents not read. The directory holds 33 entries.
|
||||||
|
|
||||||
|
**Configuration, written by a person or by the program on a person's choice:**
|
||||||
|
|
||||||
|
| path | what | how it got there |
|
||||||
|
|---|---|---|
|
||||||
|
| `rules/` | 2 rule files (1779 and 755 bytes): how text meant to be pasted elsewhere is handed over; how git is used in a second forge's repositories | written by hand |
|
||||||
|
| `skills/` | 5 skills of the operator's own, for the operator's other work; and `skills/synced/`, 2 folders the vendor's own sync placed | by hand; by the program |
|
||||||
|
| `commands/` | empty | — |
|
||||||
|
| `settings.json` | 6 keys: the attribution, auto mode, feedback drafts, the model, the spinner tips, the model switch on a flag | by hand, and by the program on a `/model` or `/config` choice |
|
||||||
|
| `settings.local.json` | 1 key, `permissions`: the rules the program writes when a person answers "always allow" in a session | by the program, on a person's answer |
|
||||||
|
| `plugins/` | the program's record of its marketplaces (the vendor's and the mesh's `nox-mesh`) and installed plugins, and its cache | by the program |
|
||||||
|
| no `CLAUDE.md`, no `agents/`, no `hooks/` | the instruction file was removed on 2026-10-07 through the module's removal tool, with a copy kept (ADR 0243) | — |
|
||||||
|
|
||||||
|
**Working state, written by the program for itself** (24 entries): the credentials file (the module's own
|
||||||
|
code writes it, ADR 0183), `history.jsonl` (5 MB), `projects/` (30 directories, 1.5 GB, holding every
|
||||||
|
project's sessions and the agent's own memory), `sessions/`, `session-env/`, `shell-snapshots/`,
|
||||||
|
`file-history/`, `debug/`, `cache/`, `paste-cache/`, `telemetry/`, `stats-cache.json`, the daemon's lock,
|
||||||
|
log and status files, `state/`, `ide/`, `jobs/`, `feedback/`, `backups/`, `worktrees/`, and a few markers.
|
||||||
|
|
||||||
|
**The managed directory under `/etc`**, by contrast, holds exactly what the module renders: the instruction
|
||||||
|
file, the managed settings, the managed tool-server file, and the marketplace with the `nox-mesh` plugin.
|
||||||
|
|
||||||
|
## 2. The homes on four nodes, 2026-10-09
|
||||||
|
|
||||||
|
Read through each node's `claude_code_config_status` (the module's own status tool), the field listing the
|
||||||
|
home's own items beside what the mesh placed:
|
||||||
|
|
||||||
|
| node | home items the mesh did not place | placed by the mesh at home |
|
||||||
|
|---|---|---|
|
||||||
|
| the laptop | 2 rule files, 5 skills | none |
|
||||||
|
| the workstation | an instruction file, a `cleanup` skill, a subagent (a module reviewer) | none |
|
||||||
|
| the home server | an instruction file, a `cleanup` skill | none |
|
||||||
|
| the control-node | an instruction file, a `cleanup` skill | none |
|
||||||
|
|
||||||
|
Three things follow:
|
||||||
|
|
||||||
|
- **The retired skill is still everywhere.** To-be 36 §1 said on 2026-10-02 that the `cleanup` skill
|
||||||
|
becomes "nothing; it named the predecessor's forge". It is on all four nodes on 2026-10-09. Nothing
|
||||||
|
owns it, so nothing removed it.
|
||||||
|
- **The operator's rules exist on one node.** A rule about how the operator wants text handed over is as
|
||||||
|
true on the workstation as on the laptop. It is on the laptop alone, because the only way to put it
|
||||||
|
anywhere is to write the file there.
|
||||||
|
- **Nothing is raised about any of it.** The status tool lists these items when asked. No condition says
|
||||||
|
the laptop's rules are missing elsewhere, or that a skill the design retired is still loaded in every
|
||||||
|
session on four machines. The three mesh-wide instruction sections and the `nox-mesh` plugin (holding
|
||||||
|
the guard alone) are the only things the mesh renders for a session, and both are in the managed
|
||||||
|
directory, not the home.
|
||||||
|
|
||||||
|
## 3. The agent's own memory, 2026-10-09
|
||||||
|
|
||||||
|
The agent keeps notes for itself in the program's memory directory under `projects/` for one repository
|
||||||
|
on the laptop: 45 notes on 2026-10-09. At least 9 of them are a correction of a way of working that the
|
||||||
|
operator gave in a session, for example: finish a module before pushing it; use the mesh's tools, not
|
||||||
|
ssh; read the merge gate's verdict before merging; what is done is what the operator sees working;
|
||||||
|
no work-arounds when a tool is missing; a language choice the operator insists on. Each was learned once,
|
||||||
|
after the operator said it, sometimes with irritation.
|
||||||
|
|
||||||
|
Those notes:
|
||||||
|
|
||||||
|
- are on one machine, for one repository: a session on the workstation, or in another repository on the
|
||||||
|
laptop, starts without them;
|
||||||
|
- are written by the agent for itself, in its own words, and shown to nobody: the operator cannot see
|
||||||
|
what the agent taught itself, or correct it;
|
||||||
|
- are not read before a tool runs: a note saying "read the merge gate first" is read at session start,
|
||||||
|
if the agent opens it, and nothing says it again at the moment a merge is about to be called.
|
||||||
|
|
||||||
|
The mesh keeps one related record: the guard's record of every refusal and override
|
||||||
|
([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md) §5),
|
||||||
|
which is how a habit that found a way round is seen. It records what the guard refused, not what the
|
||||||
|
operator corrected.
|
||||||
|
|
||||||
|
## 4. What the vendor loads from the home
|
||||||
|
|
||||||
|
As documented by the vendor and confirmed on the laptop on 2026-10-09 (a session's own context names the
|
||||||
|
two rule files as "the user's private global instructions for all projects"): the home's `CLAUDE.md` and
|
||||||
|
every file under `rules/` are read into every session the account starts, in every project; `skills/`,
|
||||||
|
`commands/` and `agents/` are offered under their own names; `settings.json` is the account's settings,
|
||||||
|
below the managed settings; hooks come from `settings.json` and from a plugin. None of it is read for
|
||||||
|
another account, and none of it reaches another machine.
|
||||||
|
|
||||||
|
## 5. The options
|
||||||
|
|
||||||
|
### Who owns the configuration under the home
|
||||||
|
|
||||||
|
1. **As today: the module owns what it placed, the rest is found** (ADR 0216 §5, ADR 0182). Rejected by
|
||||||
|
the operator's direction, and by §2: a rule on one node reaches no other, a retired skill stays on
|
||||||
|
four, and nothing is raised. There is a second reason: the guard refuses an agent that reaches a
|
||||||
|
machine by ssh, and refuses nothing that writes `~/.claude/rules/x.md`. On a node where the agent
|
||||||
|
runs as the operator, that is a rule planted in every later session, by hand, with no record.
|
||||||
|
2. **The module owns the whole directory and regenerates it.** Rejected in ADR 0182 (option 1), and still:
|
||||||
|
the working state is the program's, and a render that touched `projects/` would erase a season of
|
||||||
|
memory.
|
||||||
|
3. **The module owns every configuration file by name, excludes the program's working state by name,
|
||||||
|
renders the configuration from registrations, and reports a hand edit as a condition.** Chosen.
|
||||||
|
|
||||||
|
### Whose the rules are
|
||||||
|
|
||||||
|
1. **Per node** (the home scope as ADR 0216 defines it: one account's directory on one machine). Rejected
|
||||||
|
as the only scope: the operator is one person on four nodes, and §2 shows what one-node rules give.
|
||||||
|
2. **Mesh-wide instruction sections**, which already reach every node. Rejected as the place for them:
|
||||||
|
those are the mesh's word to every session, written at the controller's terminal, about the mesh.
|
||||||
|
A personal rule is the operator's word about the operator: how to hand over text, which tools their
|
||||||
|
other employer's repositories need. It belongs to the person and follows the person's agent, into the
|
||||||
|
home, under the person's own names.
|
||||||
|
3. **A new kind, the personal rule, registered for the operator and rendered into the agent's home on
|
||||||
|
every node.** Chosen. The same scope serves the operator's own skills, commands, subagents and settings.
|
||||||
|
|
||||||
|
### How a lesson becomes a rule
|
||||||
|
|
||||||
|
1. **The agent writes the rule itself.** Rejected: an agent that writes its own rules can loosen them, the
|
||||||
|
operator never sees what it taught itself, and issue 340's reasoning holds — text that every session
|
||||||
|
reads is written on the operator's word, never on an agent's.
|
||||||
|
2. **Lessons stay in the agent's memory.** Rejected by §3.
|
||||||
|
3. **A lessons register in the module. An agent proposes a lesson; the operator says yes or no; a yes
|
||||||
|
makes it a personal rule, rendered everywhere; the agent is reminded of the rules that bear on what it
|
||||||
|
is about to do.** Chosen.
|
||||||
|
|
||||||
|
### How the operator's yes reaches the operator's home
|
||||||
|
|
||||||
|
1. **Trust the tool's caller.** Rejected in issue 340.
|
||||||
|
2. **The controller's terminal only.** Safe; kept as one of two ways. It makes a yes in a session a trip
|
||||||
|
to the control-node, which is what the operator asked to avoid.
|
||||||
|
3. **A warrant on an authorising ask** (ADR 0259): the module asks the operator, with the exact text
|
||||||
|
bound in the ask, and acts on the warrant alone. Chosen beside 2. The approve option needs a channel
|
||||||
|
that proves its sender; until one answers, the terminal is the way.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
status: active
|
||||||
|
initiated: 2026-10-10
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.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/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
|
||||||
|
- 02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md
|
||||||
|
- 02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md
|
||||||
|
- 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
|
||||||
|
- 04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md
|
||||||
|
- 01-RESEARCH/039-a-tool-that-needs-a-persons-approval/00-overview.md
|
||||||
|
became: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# 041 — The agent's session runs confined, instead of being proven by a disk-wide search
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
Whether the coding agent's session on a node with an **agent account** should be **confined when it
|
||||||
|
starts**, so that what it cannot do is enforced by the kernel, instead of the node-engine proving the
|
||||||
|
account harmless by searching every local filesystem for setuid programs. Three parts, from the
|
||||||
|
operator's idea of 2026-10-10:
|
||||||
|
|
||||||
|
1. **No new privileges.** The session and everything it starts run with the kernel's `no_new_privs` flag,
|
||||||
|
so a setuid or setgid program, packaged or not, and a program with file capabilities, grant it nothing.
|
||||||
|
2. **A confined view of the files.** The session sees `/usr` read-only, its own home, a private `/tmp` and
|
||||||
|
the few system files a program needs to run, and nothing else.
|
||||||
|
3. **Everything else through the mesh's tools.** What an agent reads outside that view (a file, a
|
||||||
|
directory, a log) is a tool of a seat, not a path.
|
||||||
|
|
||||||
|
And then: what must be **proven** for a node to be root-free under such a start (every way into the
|
||||||
|
account goes through it), how the node-engine checks that cheaply, and what becomes of the setuid search.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
[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)
|
||||||
|
§3 item 10 judges the agent account by a search of every local filesystem mounted without `nosuid` for
|
||||||
|
setuid or setgid root programs no package owns, bounded at 15 minutes. Until a search finishes, the verdict
|
||||||
|
is *not judged*, and [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)
|
||||||
|
§8 rule 2 counts a machine without a recent verdict as not root-free, so the router approves nothing proven
|
||||||
|
there and a channel's `verified-sender` is stripped.
|
||||||
|
|
||||||
|
Measured on 2026-10-10 (the controller's `root-free` verb and `node` verb): on the control-node the search
|
||||||
|
"did not finish within 15m0s", the agent account is *not judged*, and two urgent conditions are open
|
||||||
|
(`agent-root` and `root-not-free`). Linking the operator's phone channel is blocked by it.
|
||||||
|
[Issue 344](../../04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md) gap 9 had
|
||||||
|
foreseen it ("on a large data pool the root search may never finish"). A resumable search is the short-term
|
||||||
|
fix, tracked separately. This effort asks whether the search is the right instrument at all: it answers
|
||||||
|
"is there a program that could make this account root?", while the question ADR 0259 needs is "can an
|
||||||
|
agent's session become root?". A confined start answers the second question directly.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- **ADR 0266 §3 and §4**: which ways to root the node-engine judges, and what a root-free verdict rests on.
|
||||||
|
Under `no_new_privs` item 10 (unpackaged setuid) and the *not judged* file capabilities no longer
|
||||||
|
apply to the session; new things must be proven instead (§3 of the findings).
|
||||||
|
- **ADR 0266 §7 and to-be 36 §9**: `claude-agent` becomes the confined start, not only an account switch.
|
||||||
|
- **ADR 0259 §8**: `root-free` would read the new proof.
|
||||||
|
- **ADR 0245**: the guard stays as the session's own refusal; the confinement backs it in the kernel.
|
||||||
|
- **ADR 0144, ADR 0152 and ADR 0266 §7**: the mesh MCP server has no login of its own, so a confined agent
|
||||||
|
still reaches every tool the operator reaches. Not closed here; recorded as the boundary of this effort.
|
||||||
|
- **Issue 344** gaps 1, 3, 4 and 9, and **research 039** (a tool that needs a person's approval).
|
||||||
|
- **A node filesystem seat** that does not exist today.
|
||||||
|
|
||||||
|
## Documents
|
||||||
|
|
||||||
|
- [01 — The options, what must be proven, and the open questions](01-options-and-open-questions.md)
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# 041 — The options, what must be proven, and the open questions
|
||||||
|
|
||||||
|
Sources: [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)
|
||||||
|
§3, §4, §6 and §7; [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)
|
||||||
|
§8; [to-be 36](../../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) §9;
|
||||||
|
[issue 344](../../04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md);
|
||||||
|
[ADR 0144](../../02-DECISIONS/0144-anything-on-a-machine-may-call-anything-on-it.md) and
|
||||||
|
[ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md) (the machine's login is
|
||||||
|
the authority on loopback). The records module was searched on 2026-10-10 for `setuid`, `not judged yet`,
|
||||||
|
`loopback` and `cannot tell the operator from an agent`; the hits are the records above.
|
||||||
|
`no_new_privs`, `NoNewPrivileges`, `Landlock`, `bubblewrap` and `ProtectSystem` appear in no record.
|
||||||
|
|
||||||
|
What the kernel and the tools do is stated from their documentation and has **not been measured on any node**
|
||||||
|
yet. Each such claim is marked *to measure* where the decision would rest on it.
|
||||||
|
|
||||||
|
## 1. What the setuid search answers, and what it does not
|
||||||
|
|
||||||
|
ADR 0266 §3 judges eleven ways to root by reading the machine. Item 10, the setuid search, is the only one
|
||||||
|
whose cost grows with the data on the machine. On the control-node it did not finish within its 15-minute
|
||||||
|
bound (measured 2026-10-10, `mesh-controller.root-free`), so the verdict is *not judged* and ADR 0259 §8
|
||||||
|
blocks every approval proven there.
|
||||||
|
|
||||||
|
The search asks whether a program exists that could make the account root. It does not ask whether the
|
||||||
|
agent's session can run one. Even when it finishes, ADR 0266 lists what it leaves unseen: a packaged setuid
|
||||||
|
program with a flaw of its own, file capabilities on a program, setuid programs on filesystems not searched.
|
||||||
|
|
||||||
|
## 2. The three parts, compared
|
||||||
|
|
||||||
|
### 2.1 No new privileges
|
||||||
|
|
||||||
|
`no_new_privs` is a per-process flag (Linux 3.5 and later). It is set by `prctl(PR_SET_NO_NEW_PRIVS)`, by
|
||||||
|
`setpriv --no-new-privs`, or by the service manager's `NoNewPrivileges=yes`. Any process may set it, without
|
||||||
|
privilege. Once set it is inherited by every child and cannot be cleared. Under it, `execve` grants no
|
||||||
|
setuid, no setgid and no file capability.
|
||||||
|
|
||||||
|
| Way to root (ADR 0266 §3) | Under `no_new_privs` |
|
||||||
|
|---|---|
|
||||||
|
| 3, 4: a sudo or doas rule | inert: `sudo` and `doas` are setuid programs |
|
||||||
|
| 5, 6: polkit through `pkexec` | inert (setuid); polkit **through D-Bus** to a root daemon is not touched |
|
||||||
|
| 7, 8: a container runtime's socket or TCP port | not touched: the request goes to a root daemon |
|
||||||
|
| 10: unpackaged setuid program | inert, on every filesystem, searched or not |
|
||||||
|
| *not judged*: file capabilities, a flawed packaged setuid program | inert |
|
||||||
|
| *not judged*: a root-run file the account can write; root's keys | not touched |
|
||||||
|
|
||||||
|
So it removes the whole class "a program that gains privilege when executed", and leaves the class
|
||||||
|
"**a root daemon that does something when asked**": the container runtime, the service manager over D-Bus
|
||||||
|
(polkit), the ssh daemon (a key into another account), cron and at, and any file root later reads.
|
||||||
|
|
||||||
|
**It has an escape that must be closed: a process the session asks to start something.** The flag passes
|
||||||
|
only from parent to child. If the agent account has a running user manager, `systemd-run --user` starts a
|
||||||
|
command as a child of that manager, which root started without the flag. The same holds for a cron or at
|
||||||
|
daemon, for D-Bus activation on a session bus, and for a terminal multiplexer server started before the
|
||||||
|
confined session. Either the account has none of these, or each is itself started with the flag (a drop-in
|
||||||
|
on the account's `user@` unit can set `NoNewPrivileges=yes`; *to measure*).
|
||||||
|
|
||||||
|
### 2.2 A confined view of the files
|
||||||
|
|
||||||
|
| | Service manager unit (`ProtectSystem=strict`, `ProtectHome=tmpfs`, `BindPaths=`, `PrivateTmp=`, `TemporaryFileSystem=`, `InaccessiblePaths=`) | Landlock | bubblewrap |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Kernel | mount namespaces, present on every kernel the mesh runs | an LSM, 5.13 or later, and enabled in the kernel's LSM list; features grow by ABI (5.19, 6.2, 6.7, 6.10, 6.12) — *to measure on each node* | user namespaces; refused where unprivileged user namespaces are switched off |
|
||||||
|
| Who sets it up | root (a system unit, or `systemd-run` as root with `User=`); `claude-agent` already goes through the operator's sudo | the process itself, unprivileged, and it requires `no_new_privs` first | the process itself, unprivileged where user namespaces are allowed; sets `no_new_privs` itself |
|
||||||
|
| Shape | hides or makes read-only whole trees; can show only a list of paths | an allow list of path rights, checked on access; nothing is hidden from a directory listing above an allowed path | a new mount tree built from bind mounts: only what is listed exists |
|
||||||
|
| What the node-engine can read | the unit's properties from the service manager, and the session's processes' cgroup | `/proc/<pid>/status` shows `NoNewPrivs`; whether a Landlock domain is applied is not readable from outside on older kernels — *to measure* | `NoNewPrivs` and the mount namespace in `/proc`; the bind list is not declared anywhere the engine reads |
|
||||||
|
| Also gives | `RestrictSUIDSGID=`, `SystemCallFilter=`, `RestrictNamespaces=`, resource limits, one cgroup for every process of the session | per-process, stackable; can be added inside any of the other two | Claude Code's own sandbox on Linux is built on it |
|
||||||
|
| Interactive terminal | `systemd-run --pty`; *to measure* with `use_pty` from the `sudo` module | unchanged | unchanged |
|
||||||
|
|
||||||
|
**Claude Code's own sandbox** (from its documentation, *to measure* on the installed version): on Linux it
|
||||||
|
runs the shell tool's commands, and their children, under bubblewrap with a write allow list and a network
|
||||||
|
proxy. It does **not** cover the session's file tools (read, edit, write), which permission rules govern,
|
||||||
|
nor the session's MCP servers. The shell tool also takes a per-call override that runs a command outside the
|
||||||
|
sandbox, which a setting can forbid. So it confines the commands an agent types, not the session, and it is
|
||||||
|
configured by the session's own settings. It is a useful inner layer; it is not the boundary ADR 0259 can
|
||||||
|
rest on.
|
||||||
|
|
||||||
|
**Composability.** The three nest: a unit started by root gives the outer view and `no_new_privs`; Landlock
|
||||||
|
can narrow further inside it; Claude Code's bubblewrap runs inside both if the unit allows user namespaces
|
||||||
|
(`RestrictNamespaces=` would forbid it, and with it a class of kernel exploits; a trade-off, see §5). The
|
||||||
|
guard ([ADR 0245](../../02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md))
|
||||||
|
runs inside the session as a hook and keeps refusing; the confinement makes a refusal it misses fail in the
|
||||||
|
kernel. The tool runner runs outside, as the operator account, and the session reaches it over loopback, so
|
||||||
|
the confinement must leave the loopback network open.
|
||||||
|
|
||||||
|
**"Nothing else" is a short list, not nothing.** A program needs the dynamic linker's cache, the account
|
||||||
|
database, the resolver file, the certificate store (the agent talks to its API over TLS), `/proc`, `/dev/null`
|
||||||
|
and its terminal. Which `/etc` files the agent needs, and whether `/etc` is read-only or hidden but for them,
|
||||||
|
is to be listed by trying it.
|
||||||
|
|
||||||
|
### 2.3 Everything else through the mesh's tools
|
||||||
|
|
||||||
|
Measured on 2026-10-10 with `mesh_overview` and `mesh_search`: every node holds `node-service-manager`, whose
|
||||||
|
`journal` verb reads one unit's journal, and `node-hostname`, whose `entries` verb reads the machine's `/etc/hosts`.
|
||||||
|
**No seat reads an arbitrary file, lists a directory or reads a log file that is not a unit's journal.**
|
||||||
|
Today that is done through `node-login-shell.execute`, which runs any command as the operator account, and
|
||||||
|
which the control-node withholds ([ADR 0268](../../02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md)).
|
||||||
|
|
||||||
|
A node filesystem seat cannot simply read a caller's path: the tool runner acts for any caller, and ADR 0266
|
||||||
|
§7 calls a tool that reads a caller's path as the operator or as root a defect. A shape that would hold
|
||||||
|
follows the store's reader after issue 337: it reads only what a module **declares readable** (its
|
||||||
|
configuration, its logs), never a secret, through no symbolic link. Verbs it needs, at least: read a declared
|
||||||
|
file, list a declared directory, read a declared log's tail with a filter.
|
||||||
|
|
||||||
|
On the operator's machines the agent works in the operator's desktop and files (ADR 0266 §1). Moving it to an
|
||||||
|
agent account there is only possible once such tools, and the desk's own tools, cover that work, and issue
|
||||||
|
344 gaps 3 and 4 (the display, the chat client) need the agent account to have no access to the operator's
|
||||||
|
display at all. Confining the agent while it **stays** the operator account does not hold: it can write a
|
||||||
|
file the operator later runs with sudo (a shell start file), and the operator's own user manager starts
|
||||||
|
anything without the flag.
|
||||||
|
|
||||||
|
## 3. What must then be proven for root-free
|
||||||
|
|
||||||
|
A confined start proves nothing if another way into the account exists. The proof becomes two halves.
|
||||||
|
|
||||||
|
**The doors (read from the machine's configuration, cheap).**
|
||||||
|
|
||||||
|
1. The account has no password: a locked entry in the shadow database.
|
||||||
|
2. No ssh key logs into it: no `authorized_keys` under its home, or the ssh daemon refuses the account.
|
||||||
|
3. No cron table and no at job for it, or no cron or at daemon runs.
|
||||||
|
4. No lingering user manager, and no user units under its home; or its `user@` unit carries
|
||||||
|
`NoNewPrivileges=yes` and the same confinement.
|
||||||
|
5. Sudo rules only **toward** it (the operator to the agent account, through `claude-agent`), none from it.
|
||||||
|
ADR 0266 §3 item 3 already judges the second half.
|
||||||
|
6. `claude-agent` starts the session as the confined unit, and the unit's declaration is the mesh's.
|
||||||
|
|
||||||
|
**The session itself (read from `/proc`, cheap, and closer to the truth than the doors).**
|
||||||
|
|
||||||
|
7. Every process running as the agent account has `NoNewPrivs: 1` in its status and sits in the cgroup of
|
||||||
|
a confined session unit. A process that does not is a door nobody listed, found by its effect.
|
||||||
|
|
||||||
|
**The daemons that still act on request** (ADR 0266 §3 items 5 to 9 and part of the *not judged* list) stay
|
||||||
|
judged as today, because `no_new_privs` does not reach them. The confined view can also hide their entry
|
||||||
|
points: the container runtime's socket and the system bus can be made inaccessible to the session, which a
|
||||||
|
unit property states and the engine reads.
|
||||||
|
|
||||||
|
Every one of these reads is bounded by the number of processes and a handful of files, not by the size of the
|
||||||
|
disks. Item 7 is checked at a moment; a process started between two looks is seen at the next one, within the
|
||||||
|
engine's interval.
|
||||||
|
|
||||||
|
**What becomes of the setuid search.** Three choices, for the operator (Q2):
|
||||||
|
|
||||||
|
- **Retired for agent accounts** once 1 to 7 hold: it judges nothing a confined session can use.
|
||||||
|
- **Kept as defence in depth**, resumable and unbounded, with a finding raising a warning but no longer
|
||||||
|
gating `root-free`.
|
||||||
|
- **Kept only for accounts that are not confined**: the operator account where agents still run as it, and
|
||||||
|
any account whose processes fail item 7.
|
||||||
|
|
||||||
|
## 4. What this does not cover
|
||||||
|
|
||||||
|
- **The mesh MCP server has no login of its own** (ADR 0144, ADR 0152: the machine's login is the authority
|
||||||
|
on loopback; ADR 0266 §7: the tool runner acts for any caller). A confined agent reaches the same tools as
|
||||||
|
the operator. Every tool that runs a caller's command or reads a caller's path as another account remains a
|
||||||
|
way round, as ADR 0266 §7 and issue 344 gaps 2, 5 and 11 already record. Research
|
||||||
|
[039](../039-a-tool-that-needs-a-persons-approval/00-overview.md) is the work that narrows it.
|
||||||
|
- **A kernel exploit.** `no_new_privs` and a confined view do not stop a flaw in the kernel. A system call
|
||||||
|
filter and no new user namespaces shrink the surface; the second conflicts with Claude Code's bubblewrap
|
||||||
|
sandbox (§2.2). The residual risk is the kernel's, as for every other process on the machine.
|
||||||
|
- **Network reach.** The session keeps the network it needs for its API and the loopback MCP server; a
|
||||||
|
network allow list is a separate question.
|
||||||
|
|
||||||
|
## 5. Open questions for the operator
|
||||||
|
|
||||||
|
1. **Is the confined start the boundary ADR 0259 rests on**, replacing the setuid search as the gate for
|
||||||
|
`root-free` on a node with an agent account? Recommendation: yes, because it answers "can the session
|
||||||
|
become root" instead of "does a program exist", and its cost does not grow with the data on the machine.
|
||||||
|
2. **What becomes of the setuid search** (§3)? Recommendation: keep it resumable as defence in depth for a
|
||||||
|
warning, gate `root-free` only on unconfined accounts.
|
||||||
|
3. **Which mechanism carries the confinement?** Recommendation: a service manager unit started by root
|
||||||
|
through `claude-agent`, because it needs no new package, gives `no_new_privs` and the view in one
|
||||||
|
declaration the node-engine can read; Landlock as an optional inner layer where the kernel has it; Claude
|
||||||
|
Code's bubblewrap sandbox kept for the shell tool, with its per-call override forbidden.
|
||||||
|
4. **May the session keep user namespaces**, so that Claude Code's sandbox works inside it, at the price of
|
||||||
|
a larger kernel surface? Or is the outer unit enough and the inner sandbox dropped?
|
||||||
|
5. **Is a node filesystem seat that reads only declared paths enough** for what agents do on the
|
||||||
|
control-node, and is it the precondition for moving the operator's machines to an agent account?
|
||||||
|
|
||||||
|
## 6. Next measurements
|
||||||
|
|
||||||
|
- On each node: the kernel's LSM list and Landlock ABI, whether unprivileged user namespaces are allowed, and
|
||||||
|
whether `sudo -iu` there starts a user manager for the target account. No mesh tool reads the first two
|
||||||
|
today; that is itself a gap for the node-engine's facts.
|
||||||
|
- A trial `claude-agent` as a transient unit on a non-control node: which `/etc` files the session needs, that
|
||||||
|
`sudo -n true` fails with the flag set, that `systemd-run --user` is refused or inherits the flag, and that
|
||||||
|
the mesh MCP server and the guard still answer.
|
||||||
|
- The `/proc` sweep of item 7 on the control-node: how many processes run as the agent account, and how long
|
||||||
|
the read takes.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
status: graduated
|
||||||
|
initiated: 2026-10-10
|
||||||
|
touches:
|
||||||
|
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||||
|
- 02-DECISIONS/0080-the-development-cycle-is-checked.md
|
||||||
|
- 00-META/process/03-issues.md
|
||||||
|
- 00-META/glossary.md
|
||||||
|
- the runtime's state verbs and the SDK's state interface
|
||||||
|
became:
|
||||||
|
- 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
|
||||||
|
---
|
||||||
|
|
||||||
|
# 042 — The mesh keeps its own issues
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
Whether the mesh should keep its issues in a module of its own, on the bus, instead of only as
|
||||||
|
numbered folders in this repository's `04-ISSUES/`, and what the smallest useful first version of
|
||||||
|
that module is. The operator's ask of 2026-10-10: work on a ticketing system on the bus, consolidate
|
||||||
|
every current issue into it, start small and extend the module along the way.
|
||||||
|
|
||||||
|
The effort answers six questions, in [01-findings.md](01-findings.md):
|
||||||
|
|
||||||
|
1. The smallest useful first slice: what the module holds, which tools it serves, which events it
|
||||||
|
emits, and what it explicitly leaves out.
|
||||||
|
2. Where the truth lives during and after the move: the folders here, the module, or a split
|
||||||
|
between them, and what each choice does to the public-repository rule, playbook 03 and the hq
|
||||||
|
checks.
|
||||||
|
3. How the 357 issue folders are imported and consolidated (duplicates, still-open ones, grouping).
|
||||||
|
4. How the controller's conditions, the router's asks and deliveries relate to issues.
|
||||||
|
5. How the operator sees issues: through the mesh's tools first, through a view onto the bus later.
|
||||||
|
6. The open questions for the operator, and the slices after the first.
|
||||||
|
|
||||||
|
The operator also named an existing project as inspiration: *hits*, a headless issue tracker built
|
||||||
|
as NATS micro services on JetStream, with an MCP server for agents. Its model is studied in the
|
||||||
|
findings, along with whether to reuse its code or build on its model.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Numbers collide. Playbook 03 tells an author to take the next free number across `main` and every
|
||||||
|
open pull request, and [issue 155](../../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md)
|
||||||
|
records the second collision reaching `main` with every check passing. `cycle.py` now refuses two
|
||||||
|
records sharing a number, which catches a collision after it happens and does not prevent one. On
|
||||||
|
`main` at the time of writing, 31 commits mention a renumbering. On 2026-10-07 three decision numbers
|
||||||
|
collided in one evening. On 2026-10-10 an open pull request still carries a branch named for issue
|
||||||
|
349 and a title naming issue 350, because 349 was taken on `main` first. The operator's answer on
|
||||||
|
2026-10-07 was that numbering belongs to the mesh's own issue tracker.
|
||||||
|
|
||||||
|
Status is also hard to see. Of 357 issue folders on `main`, 73 are not closed (27 `open`, 45
|
||||||
|
`located`, 1 `diagnosing`). Finding them means reading frontmatter across the repository.
|
||||||
|
Nothing connects a condition the controller raises to the issue that would fix it: on 2026-10-10 a
|
||||||
|
condition was silenced with the words "hq issue to follow", and nothing records whether one followed.
|
||||||
|
|
||||||
|
## What it touches
|
||||||
|
|
||||||
|
- [ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md) (statuses live in frontmatter) and
|
||||||
|
[ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md) (the cycle is checked), with
|
||||||
|
playbook [03](../../00-META/process/03-issues.md) and `00-META/checks/cycle.py`.
|
||||||
|
- [ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||||
|
(a module keeps its state in key-value buckets it declares),
|
||||||
|
[ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
||||||
|
(tools announce themselves on the bus) and
|
||||||
|
[ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) (events are declared).
|
||||||
|
- The conditions of [ADR 0231](../../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)
|
||||||
|
and the asks of [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
|
||||||
|
- The records module, which indexes this repository for agents.
|
||||||
|
|
||||||
|
**Graduated 2026-10-10** into [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)
|
||||||
|
and [to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md). The operator delegated the five open questions; each
|
||||||
|
was decided as recommended. `touches:` names what the decision changes.
|
||||||
|
|
||||||
|
## Words
|
||||||
|
|
||||||
|
The glossary's word for one of these is **issue** (*The record*: "a report in `04-ISSUES/`"). This
|
||||||
|
effort keeps that word for the thing the module holds, and proposes **issue tracker** for the module
|
||||||
|
that holds them, with the module named `issues`. It does not use *ticket*, the operator's word in
|
||||||
|
the ask, or *item*, the word hits uses. If the glossary entry is widened (an issue is then held by
|
||||||
|
the issue tracker, and its narrative may live in `04-ISSUES/`), *ticket* would go on its *Not:* line.
|
||||||
|
That change belongs to the decision this effort graduates into, not to the effort itself.
|
||||||
|
|
||||||
|
## Open questions for the operator
|
||||||
|
|
||||||
|
Listed in [01-findings.md § 7](01-findings.md#7-open-questions-for-the-operator), each with a
|
||||||
|
recommendation.
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# 01 — Findings
|
||||||
|
|
||||||
|
Measured on 2026-10-10 against `main` of this repository, the open pull requests on the forge, the
|
||||||
|
controller's open conditions, the module SDK's source, and a clone of the hits repository at its
|
||||||
|
release v0.7.0. Each section ends with what this effort recommends; section 7 lists what needs the
|
||||||
|
operator.
|
||||||
|
|
||||||
|
## 0. What exists today, measured
|
||||||
|
|
||||||
|
| Fact | Measured |
|
||||||
|
|---|---|
|
||||||
|
| Issue folders on `main` | 357, numbered up to 362; numbers 283 and 284 were never used, and 338, 341 and 350 sit on open pull requests (363 too) |
|
||||||
|
| Status in frontmatter | 283 `resolved`, 1 `wontfix`, 45 `located`, 27 `open`, 1 `diagnosing` |
|
||||||
|
| Frontmatter keys | `status`, `opened`, `located-in` on all 357; `fixed-by` 356; `amended-design` 344; `replay` 22; `replay-none` 44 |
|
||||||
|
| Size | 1.9 MiB of markdown in all; median folder 4.6 KiB, largest 25 KiB; 244 folders hold only a report, 111 also a diagnosis |
|
||||||
|
| Links between issues | 168 folders link another issue's folder (265 links); 193 name another issue in prose |
|
||||||
|
| `located-in` | free text: the controller is written `mesh-controller`, `cmd/mesh-controller`, `mesh-control` and as file paths; 17 issues name nothing |
|
||||||
|
| Renumberings | 31 commits on `main` mention one; one open pull request still carries the old number in its branch name |
|
||||||
|
| The forge's own issues | none in this repository or the controller's; [issue 178](../../04-ISSUES/178-a-routed-name-resolves-to-a-provider-merely-told-it/00-report.md) was filed there once and moved here, because "its numbers collide with hq's pull request numbers in conversation" |
|
||||||
|
| Conditions | 10 open; 5 of kind `healer-wanted` (a hand repair repeated, "an automatic repair is wanted"); one silenced with "hq issue to follow" |
|
||||||
|
|
||||||
|
Two things follow. The collisions come from allocating a number by reading a repository that many
|
||||||
|
branches write at once; nothing short of one allocator removes them. And the frontmatter is a small,
|
||||||
|
regular data set (six keys, 1.9 MiB) that fits easily in the bus's key-value store under the limits
|
||||||
|
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||||
|
§6 sets (256 KiB a value, 64 MiB a bucket).
|
||||||
|
|
||||||
|
## 1. The first slice
|
||||||
|
|
||||||
|
**A Go module `issues`, one instance on the control-node, serving one seat held once for the mesh.**
|
||||||
|
Its state is in buckets it declares (ADR 0201), its tools announce themselves on the bus
|
||||||
|
([ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)),
|
||||||
|
and every change is an event it declares under `emits`
|
||||||
|
([ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md)). The module that delivers pull
|
||||||
|
requests already has this shape: Go, one mesh seat, two state buckets, declared events.
|
||||||
|
|
||||||
|
**What one issue holds in v1**, the frontmatter of today plus what the folders carry in prose:
|
||||||
|
|
||||||
|
- `number` (the key), `title`, `status` (`open`, `diagnosing`, `located`, `resolved`, `wontfix`),
|
||||||
|
`opened`, `closed`;
|
||||||
|
- `located-in`, from a registered list of repositories and modules rather than free text;
|
||||||
|
- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`;
|
||||||
|
- `notes`, appended and never edited: who wrote, when, and the text;
|
||||||
|
- `links`, typed: to another issue (`duplicates`, `relates-to`), to a pull request, a decision
|
||||||
|
record, a design, a condition key or a delivery;
|
||||||
|
- `record`: the path of the issue's folder in this repository, when it has one (section 2).
|
||||||
|
|
||||||
|
**Storage.** One bucket `issues` with one key per issue, keeping a short history per key, and a
|
||||||
|
`counter` key for the next number. The events (`opened`, `changed`, `noted`, `linked`) carry what
|
||||||
|
changed and who changed it, so they are the audit trail, and a view can watch the bucket. Section 6
|
||||||
|
compares this with a stream as the source of record.
|
||||||
|
|
||||||
|
**Tools** (verbs of the seat, found with `mesh_search`):
|
||||||
|
|
||||||
|
| Tool | Does |
|
||||||
|
|---|---|
|
||||||
|
| `open` | takes the next number atomically and writes the issue; refuses a title or report that is empty |
|
||||||
|
| `get` | one issue, whole |
|
||||||
|
| `list` | by status, by `located-in`, by a phrase in title or notes; compact rows, newest first |
|
||||||
|
| `move` | changes status, enforcing today's cycle rules at the write: `located` needs `located-in`; `resolved` needs `fixed-by`; a core issue needs `replay` or `replay-none`; `wontfix` needs a reason |
|
||||||
|
| `note` | appends to the trail |
|
||||||
|
| `link` / `unlink` | adds or removes a typed link |
|
||||||
|
| `import` | reads this repository's issue folders and writes them as issues with their own numbers (section 3); idempotent, reporting what it would change before it does |
|
||||||
|
|
||||||
|
**The caller is the bus credential, not an argument.** The module records who acted from the
|
||||||
|
identity the runtime gives it with each call, so an agent cannot write as someone else.
|
||||||
|
|
||||||
|
**Atomic numbering needs compare-and-set.** The SDK's state interface (v0.1.9) offers `get`, `put`,
|
||||||
|
`delete`, `keys` and `watch`, and no write that is refused when the key changed since it was read.
|
||||||
|
Whether the runtime's `mesh/state.put` takes an expected revision was not checked; the tool runner's
|
||||||
|
code was not read for this effort. Two ways out: add `create` and an `update` that takes the revision
|
||||||
|
read to the state interface (small, and every module with a counter gains it), or run exactly one
|
||||||
|
instance and number inside it. **Recommended: the first.** One instance is also right for v1, but the
|
||||||
|
counter should not depend on it.
|
||||||
|
|
||||||
|
> **Correction — 2026-10-10.** The finding above is wrong about the SDK. Version v0.1.9, the one read, had no
|
||||||
|
> compare-and-set, but the SDK's Go state interface already had it from go/v0.1.10: `Create` (writes a key only if it
|
||||||
|
> does not exist) and `Update` (writes only at the revision the caller read), with the runtime's matching state
|
||||||
|
> verbs, both from [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
|
||||||
|
> What the tracker's counter needed and slice 1 added is `Increment`, in go/v0.1.12, which reads a counter and writes
|
||||||
|
> the next number with `Create` or `Update` at the revision it read, retrying a bounded number of times. The
|
||||||
|
> recommendation stands; the work it named was smaller than stated. ADR 0275 carries the same correction.
|
||||||
|
|
||||||
|
**Explicitly left out of v1:** decision and research numbers (slice 2), any change to playbook 03 or
|
||||||
|
the hq checks (slice 2), duplicate finding beyond links (slice 3), opening an issue from a condition
|
||||||
|
(slice 4), a web view (slice 5), full-text or semantic search, priorities, claims, notifications, and
|
||||||
|
the forge's own issues.
|
||||||
|
|
||||||
|
## 2. Where the truth lives
|
||||||
|
|
||||||
|
| | (a) Split: the tracker holds the issue, hq holds the narrative | (b) Everything moves | (c) The tracker mirrors hq |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Number | tracker | tracker | hq, so collisions stay |
|
||||||
|
| Status, `located-in`, `fixed-by`, links | tracker | tracker | hq frontmatter, copied |
|
||||||
|
| Report and diagnosis | hq folder for a design or governance question; notes in the tracker for the rest | tracker notes | hq |
|
||||||
|
| Public-repository rule | holds: hq keeps only scrubbed narrative, the tracker is private and may name nodes | the public record of design questions is lost | holds |
|
||||||
|
| Playbook 03 | changes: open in the tracker first, write a folder only for a design or governance question | replaced | unchanged |
|
||||||
|
| `cycle.py` | its issue rules move into the tracker's `move`, refused at the write instead of at merge | same | unchanged, and a second copy to keep in step |
|
||||||
|
| Records module | still finds the narrative; the tracker's `list` and `get` serve status | finds no issues | unchanged |
|
||||||
|
|
||||||
|
Option (c) keeps the collisions and adds a second copy that can drift, so it answers neither of the
|
||||||
|
operator's problems. Option (b) loses the public record that hq exists to be
|
||||||
|
([ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md)), and with it the
|
||||||
|
symptom-to-component memory that playbook 03 says is never deleted.
|
||||||
|
|
||||||
|
**Recommended: (a).** Every issue gets a number from the tracker, including ones that never need a
|
||||||
|
document here (a node's quirk, an operational follow-up, which playbook 03's table already sends
|
||||||
|
elsewhere). A design or governance issue also gets a folder `04-ISSUES/NNN-slug/` with the tracker's
|
||||||
|
number, whose frontmatter shrinks to a pointer (`issue: NNN`) and whose prose stays reviewed in pull
|
||||||
|
requests. Status lives in one place, the tracker. This reverses ADR 0019's "statuses live only in
|
||||||
|
frontmatter" for issues and moves part of ADR 0080's check, so it needs a decision record that
|
||||||
|
supersedes those parts, written when this effort graduates.
|
||||||
|
|
||||||
|
**Numbering continues.** The tracker's counter starts above the highest number on `main` and on open
|
||||||
|
branches at the moment of the switch (today 363, so the first new issue is 364). Numbers 283 and 284
|
||||||
|
stay unused, and a number never moves again.
|
||||||
|
|
||||||
|
**During the move**, hq frontmatter stays authoritative until the import has run and been checked
|
||||||
|
against it, and one pull request then removes the status keys from every report and changes playbook
|
||||||
|
03 and `cycle.py` together. There is no period in which both are written.
|
||||||
|
|
||||||
|
## 3. Importing and consolidating the 357 issues
|
||||||
|
|
||||||
|
**Import.** `import` reads the folders through the records module (`records_list`, `records_read`),
|
||||||
|
over the bus, rather than from a checkout. Each folder becomes an issue with its own number, its
|
||||||
|
frontmatter as fields, its title from the heading, the commit it was read at, and its path as
|
||||||
|
`record`. Folder-to-folder links (265) become `relates-to` links. The four open pull requests'
|
||||||
|
issues (338, 341, 350, 363) are reserved as numbers and imported when they merge.
|
||||||
|
|
||||||
|
**Consolidating** means four concrete passes, each producing a list for the operator or an agent to
|
||||||
|
confirm, never applying itself:
|
||||||
|
|
||||||
|
1. **Close what is fixed.** A `located` or `open` issue whose `located-in` repository has a merged
|
||||||
|
pull request naming its number is a candidate for `resolved`. hits's auditor (section 6) checks
|
||||||
|
exactly this against git and is the model.
|
||||||
|
2. **Merge duplicates.** Candidates are pairs that link each other and share a `located-in`, and
|
||||||
|
pairs whose titles share most words. A confirmed duplicate is closed `wontfix` with a
|
||||||
|
`duplicates` link and the reason, keeping its number and folder.
|
||||||
|
3. **Normalise `located-in`** onto the registered list (`mesh-controller`, `mesh-catalog`, the
|
||||||
|
node-engine's repository, a module's name), keeping the file paths as a note.
|
||||||
|
4. **Group the 73 not closed** by domain (the glossary's domains, ADR 0244) and by `located-in`, so
|
||||||
|
the operator sees, for example, how many open issues the delivery domain carries.
|
||||||
|
|
||||||
|
## 4. Conditions, asks and deliveries
|
||||||
|
|
||||||
|
A condition is one open fact, raised and cleared only by observation (ADR 0231); an issue is a
|
||||||
|
question someone must answer. They are different things, and a condition must not be closed by
|
||||||
|
closing an issue. But a condition can say that a fix is owed: five of today's ten are `healer-wanted`,
|
||||||
|
and one silence says "hq issue to follow".
|
||||||
|
|
||||||
|
Recommended for a later slice (slice 4): an issue links a condition by its key; the controller shows
|
||||||
|
the link on the condition; and for `healer-wanted` the controller opens one issue per condition key
|
||||||
|
when it first raises it, linked both ways, instead of an agent remembering to. A silence whose reason
|
||||||
|
names an owed issue should carry its number. Asks
|
||||||
|
([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md))
|
||||||
|
stay with the router: the tracker asks nothing in v1. Deliveries relate through pull requests: when
|
||||||
|
the forge says a pull request merged, the tracker notes it on every issue the pull request names, and
|
||||||
|
offers `resolved` (slice 4). Nothing in v1 depends on these.
|
||||||
|
|
||||||
|
## 5. How the operator sees issues
|
||||||
|
|
||||||
|
First through the mesh's tools: `list` returns rows short enough to read in a chat, `get` one issue
|
||||||
|
whole, and every agent session reaches them through the mesh MCP server. The responder (`@mesh`,
|
||||||
|
ADR 0234) answers from read verbs, so "what is open in delivery" works on a channel once the seat
|
||||||
|
exists. Later a view onto the bus: the `issues` bucket watched over the bus (a browser reaches it through a WebSocket listener, which this effort did not check for),
|
||||||
|
read-only, which the operator named as a future direction for every module's state. Nothing in v1
|
||||||
|
blocks it, because the state is already a bucket and every change an event.
|
||||||
|
|
||||||
|
## 6. hits: what it is, and what to take from it
|
||||||
|
|
||||||
|
**What it is.** A headless issue tracker in Go (module `github.com/impire-io/hits`), with no web
|
||||||
|
interface and no HTTP interface: NATS micro services on JetStream, a command line for people and a
|
||||||
|
stdio MCP server for agents. One author, 81 commits between 2026-09-02 and 2026-09-14, release
|
||||||
|
v0.7.0, about 9,700 lines of Go outside tests and 21 test files, tested against an embedded NATS
|
||||||
|
server. Its model descends from a method like this repository's: a sibling `hits-hq` with numbered
|
||||||
|
playbooks and decisions.
|
||||||
|
|
||||||
|
**Data model.** An *item* has a type (`bug`, `task`, `improvement`), a status (`open`, `diagnosing`,
|
||||||
|
`located`, `blocked`, `resolved`, `wontfix`), a priority, `located-in` (from a registered list of
|
||||||
|
*projects*), `fixed-by` (pull request, commit or action), `amended-design`, append-only notes, typed
|
||||||
|
links (`duplicates`, `relates-to`), a claim, a block, and a tombstone for a filing mistake. Its
|
||||||
|
statuses and fields are this repository's frontmatter almost key for key. Projects, *initiatives*
|
||||||
|
(groups of projects that also scope the numbering) and *releases* are registered lists that are
|
||||||
|
never reused once retired.
|
||||||
|
|
||||||
|
**How it stores.** The source of record is a stream `hits-ops` with subjects
|
||||||
|
`hits.ops.item.<id>` (and `project`, `initiative`, `release`). Every write appends an operation,
|
||||||
|
published with the per-subject expected last sequence, so two writers to one item cannot both win.
|
||||||
|
A bucket `hits-state` holds the folded snapshot of each item, the registered lists and one counter
|
||||||
|
per initiative. The counter is advanced by compare-and-set and an item is numbered only after it is
|
||||||
|
validated, so a refused create spends no number. On start it rebuilds the bucket from the stream.
|
||||||
|
|
||||||
|
**Services and subjects.** One micro service `hits` with 23 endpoints under `hits.api.*` (`create`,
|
||||||
|
`get`, `edit`, `transition`, `claim`, `release`, `block`, `unblock`, `link`, `unlink`, `note`,
|
||||||
|
`tombstone`, and register, list and retire for projects, initiatives and releases); three index
|
||||||
|
services, each a projection of the stream: full text (`hits.search.query`, a local index on disk),
|
||||||
|
a graph (`hits.graph.neighbors`, `hits.graph.walk`) and an optional semantic search that needs an
|
||||||
|
outside embedding provider.
|
||||||
|
|
||||||
|
**MCP tools.** 27, one per endpoint: `create_item`, `get_item`, `edit_item`, `transition_item`,
|
||||||
|
`claim_item`, `release_item`, `block_item`, `unblock_item`, `link_items`, `unlink_items`,
|
||||||
|
`note_item`, `tombstone_item`, the project, initiative and release tools, and `search_items`,
|
||||||
|
`semantic_search`, `graph_neighbors`, `graph_walk`. One server serves one agent under one actor.
|
||||||
|
Its command line also has an auditor that checks every item's `fixed-by` against git and finds
|
||||||
|
merged pull requests whose items are still open.
|
||||||
|
|
||||||
|
| | Adopt as it is | Adapt to the mesh |
|
||||||
|
|---|---|---|
|
||||||
|
| Statuses, fields, append-only notes, typed links, tombstone | yes: they are already ours | — |
|
||||||
|
| Stream of operations as the record, bucket as its projection | the pattern | the stream and bucket declared in the manifest and created by the controller (ADR 0201 §3), not created by the service on start |
|
||||||
|
| Expected-sequence writes; compare-and-set counter; number only after validation | yes | needs compare-and-set in the state interface (section 1) |
|
||||||
|
| The auditor | the idea, as a tool | reads the forge through the gitea module, not a local clone |
|
||||||
|
| Subjects `hits.api.*`, its own MCP server | no | the seat's verbs, served by the tool runner and found with `mesh_search` |
|
||||||
|
| Actor named by the caller | no | the caller is the bus credential the mesh issued |
|
||||||
|
| Contexts, OAuth device flow, semantic search | no | the mesh issues credentials; no outside provider |
|
||||||
|
| *item*, *project*, *initiative*, *release* | no | *issue*; a repository or module; no initiatives in v1; *release* is already a homonym here (glossary) |
|
||||||
|
|
||||||
|
**Reuse its code, or build on its model?** Its licence is the Sustainable Use License 1.0: use and
|
||||||
|
modification are allowed for internal business, non-commercial or personal use, and distribution only
|
||||||
|
free of charge and for non-commercial purposes. It is not an open-source licence. Running it on a
|
||||||
|
personal mesh is allowed; vendoring it into the catalogue binds the catalogue to that licence for as
|
||||||
|
long as the code is there. Its services create their own stream and bucket on start, which ADR 0201
|
||||||
|
§3 forbids a module to do; its subjects carry no prefix and assume one tracker per bus account; its
|
||||||
|
words clash with the glossary. Changing those means a fork of a two-week-old, single-author project.
|
||||||
|
v1 here is the item store without the indexes: roughly the size of the module that delivers pull
|
||||||
|
requests.
|
||||||
|
|
||||||
|
**Recommended: build our own module on hits's model,** and say so in the module's README. Do not
|
||||||
|
depend on or vendor its code. Revisit only if its licence changes, or if its author agrees to a
|
||||||
|
licence for this use.
|
||||||
|
|
||||||
|
## 7. Open questions for the operator
|
||||||
|
|
||||||
|
1. **Should the tracker hold every issue's number and status, while hq keeps a folder only for design
|
||||||
|
and governance issues, pointing at the tracker by number (option (a) in section 2)?**
|
||||||
|
Recommended: yes. It ends the collisions, keeps the public record, and moves the cycle rules into
|
||||||
|
the write, where they refuse instead of report.
|
||||||
|
2. **Should we build our own `issues` module on hits's model rather than run or vendor hits?**
|
||||||
|
Recommended: build our own. hits's licence limits distribution, its services create their own bus
|
||||||
|
resources (against ADR 0201), and its words clash with ours. Its model, its compare-and-set
|
||||||
|
numbering and its auditor are worth copying.
|
||||||
|
3. **Should the tracker also hand out decision and research numbers?** Recommended: yes, in slice 2.
|
||||||
|
Decision numbers collided three times on 2026-10-07; the same counter mechanism serves any
|
||||||
|
sequence, and the tracker then reserves the number when a branch starts.
|
||||||
|
4. **Should the word stay *issue*, with the module named `issues` and *ticket* retired?**
|
||||||
|
Recommended: yes. *Issue* is the glossary's word for this, and one word per thing (ADR 0244).
|
||||||
|
5. **Should the controller open an issue by itself for a `healer-wanted` condition, or only link
|
||||||
|
one an agent or the operator opened?** Recommended: link only, until slice 4 has run for a week;
|
||||||
|
then open by itself for `healer-wanted`, one issue per condition key.
|
||||||
|
|
||||||
|
## 8. Slices after the first
|
||||||
|
|
||||||
|
| Slice | Delivers | Done when the operator sees |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | The `issues` module: open, get, list, move, note, link, import; compare-and-set in the state interface | the next issue number comes from the tracker and the 73 not closed are listed by one call |
|
||||||
|
| 2 | Decision and research numbers from the tracker; playbook 03 and `cycle.py` changed; status keys removed from every report | two agents opening at once get two numbers, and no hq pull request is renumbered |
|
||||||
|
| 3 | Consolidation: the four passes of section 3, and an audit tool for `fixed-by` against the forge | the not-closed list shrinks by what was already fixed or duplicated, each with its reason |
|
||||||
|
| 4 | Conditions and pull requests linked; `healer-wanted` opens an issue | a condition shows its issue, and a merge notes the issues it names |
|
||||||
|
| 5 | A read-only view onto the `issues` bucket | the operator reads issues in a browser without an agent |
|
||||||
@@ -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,
|
**Every document carries its state in YAML frontmatter** — research overviews, design documents,
|
||||||
decision records, issue reports.
|
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
|
**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.
|
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.
|
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
|
> 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.
|
> 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`
|
[`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
|
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
|
actually started from a handoff — remains held by playbooks 04 and 07: a feature branch exists
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
|||||||
|
|
||||||
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
||||||
|
|
||||||
|
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
|
||||||
|
> "A merge produces a tiered plan" no longer holds: merges are collected in a rolling merge window, and each
|
||||||
|
> batch is planned once, as one walk with at most one commit per repository (issue 362). The plan the mesh
|
||||||
|
> keeps, its tiers and the one dependency relation stand as decided here.
|
||||||
|
|
||||||
> **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged.
|
> **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged.
|
||||||
|
|
||||||
> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md).**
|
> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md).**
|
||||||
|
|||||||
+11
@@ -118,6 +118,17 @@ ADR 0213's setting, then the mesh scope, then the node scope, then the mesh's ow
|
|||||||
state. It writes, changes and removes only those. It refuses to register a name the person already
|
state. It writes, changes and removes only those. It refuses to register a name the person already
|
||||||
uses there, rather than overwrite it.
|
uses there, rather than overwrite it.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-09, by [ADR 0273](0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md).**
|
||||||
|
> What stands: one plugin, three scopes, registration through the module's tools and state on the bus (§1
|
||||||
|
> to §4). What moved: §5 and §6. The module now owns every configuration file under the agent's home,
|
||||||
|
> rendered from registrations, with the program's working state excluded by name; a file it did not render
|
||||||
|
> is a condition, `agent-home-drift`, rather than an item listed on request, and a hand edit of a rendered
|
||||||
|
> file is written back with the hand version kept. A fourth scope, the operator's, carries the operator's
|
||||||
|
> personal rules, skills, commands, subagents and settings into the agent's home on every node. Why: the
|
||||||
|
> homes measured on 2026-10-09 held a skill retired a week before on all four nodes and the operator's rules
|
||||||
|
> on one, and nothing was raised. Who may register into the operator's home is issue 340's rule, kept: the
|
||||||
|
> controller's terminal, or a warrant on an ask.
|
||||||
|
|
||||||
**6. What the module did not place, it reports and can import.**
|
**6. What the module did not place, it reports and can import.**
|
||||||
|
|
||||||
- A status tool lists the home's items and says which the mesh placed. It also names any that
|
- A status tool lists the home's items and says which the mesh placed. It also names any that
|
||||||
|
|||||||
+6
@@ -9,6 +9,12 @@ extends: 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
|
|||||||
|
|
||||||
# 218. A plan sends grants before code, rolls a module out one machine first, and a newer merge takes over an older plan
|
# 218. A plan sends grants before code, rolls a module out one machine first, and a newer merge takes over an older plan
|
||||||
|
|
||||||
|
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
|
||||||
|
> Decision 3 no longer holds: a newer merge does not take over an older plan. Merges are collected in a
|
||||||
|
> rolling merge window and walked one batch at a time; a walk that started is never superseded, and only a
|
||||||
|
> walk still waiting for its word is folded into the next batch's walk (issue 362). Grants before code and
|
||||||
|
> one machine first stand as decided here.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
On 2026-10-05 the delivery path was watched through a day of merges, by several sessions at once. Three
|
On 2026-10-05 the delivery path was watched through a day of merges, by several sessions at once. Three
|
||||||
|
|||||||
+5
@@ -11,6 +11,11 @@ extends: 02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-
|
|||||||
|
|
||||||
# 238. A commit is the build at hand: one commit, one change plan, checked off the trunk and published only on it
|
# 238. A commit is the build at hand: one commit, one change plan, checked off the trunk and published only on it
|
||||||
|
|
||||||
|
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
|
||||||
|
> Decision 2's "one commit, one change plan" no longer holds for the release after the merge: a batch of
|
||||||
|
> merges collected in a rolling merge window is planned once, as one walk with at most one commit per
|
||||||
|
> repository (issue 362). A pull request's head is still checked on its own, with its own delivery plan.
|
||||||
|
|
||||||
> **Superseded in part — 2026-10-06, by [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md).** Decision 6's state machine is
|
> **Superseded in part — 2026-10-06, by [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md).** Decision 6's state machine is
|
||||||
> built as a **delivery**'s, owned by the module `mesh-delivery` rather than by the controller. Its states are
|
> built as a **delivery**'s, owned by the module `mesh-delivery` rather than by the controller. Its states are
|
||||||
> renamed (`releasing` → `delivering`, `done` → `delivered`) and a delivery group sits above it. Decision 7's
|
> renamed (`releasing` → `delivering`, `done` → `delivered`) and a delivery group sits above it. Decision 7's
|
||||||
|
|||||||
+8
@@ -11,6 +11,14 @@ extends: 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-p
|
|||||||
|
|
||||||
# 239. A delivery is owned by the mesh-delivery module and runs from commit to delivered
|
# 239. A delivery is owned by the mesh-delivery module and runs from commit to delivered
|
||||||
|
|
||||||
|
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
|
||||||
|
> A delivery is no longer walked by a walk of its own trunk commit. From its merge on it is delivered by the
|
||||||
|
> walk of the batch that answers it, one walk per rolling merge window with at most one commit per
|
||||||
|
> repository, and it ends with that walk (`covered` for an earlier commit of a repository the walk carried
|
||||||
|
> later). The superseded row (published, delivering → superseded) is taken only for a walk folded before it
|
||||||
|
> started whose successor does not answer the delivery's merge; a delivery follows the walk that took its
|
||||||
|
> own over. The owner, the table, the group and its order stand as decided here (issue 362).
|
||||||
|
|
||||||
> **Superseded in part — 2026-10-08, by [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md).**
|
> **Superseded in part — 2026-10-08, by [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md).**
|
||||||
> Decision 4's sentence "a declared order that contradicts an inferred one is a cycle" no longer holds.
|
> Decision 4's sentence "a declared order that contradicts an inferred one is a cycle" no longer holds.
|
||||||
> Two rules ordering one pair both ways were always a cycle, and a group of the controller, the node-engine
|
> Two rules ordering one pair both ways were always a cycle, and a group of the controller, the node-engine
|
||||||
|
|||||||
+7
@@ -53,6 +53,13 @@ Option 3.
|
|||||||
This extends rule 5 of ADR 0216: the module removes what it placed, and also, on the person's
|
This extends rule 5 of ADR 0216: the module removes what it placed, and also, on the person's
|
||||||
word, what it did not place. Rule 6 stands as written.
|
word, what it did not place. Rule 6 stands as written.
|
||||||
|
|
||||||
|
> **The mechanism changed — 2026-10-09, by [ADR 0273](0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md).**
|
||||||
|
> What stands: a found item leaves the home only on the person's word, with a reason, a copy kept, and a
|
||||||
|
> way back. What moved: a found item is now a condition the module raises (`agent-home-drift`), and this
|
||||||
|
> removal and the import are the operator's two answers to it; and for the operator's own home "the
|
||||||
|
> person's word" is no longer the caller's say-so (the consequence below) but one of two proofs, the
|
||||||
|
> controller's terminal or a warrant on an ask, as issue 340 requires.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- An item the mesh did not place can now leave a machine with its reason recorded and its content
|
- An item the mesh did not place can now leave a machine with its reason recorded and its content
|
||||||
|
|||||||
@@ -182,6 +182,18 @@ work:
|
|||||||
descriptions call a build request an *ask*;
|
descriptions call a build request an *ask*;
|
||||||
- the nftables module's tool `firewall_rules`, which serves the packet filter.
|
- the nftables module's tool `firewall_rules`, which serves the packet filter.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-09.** This record deferred the code renames in four places and named no
|
||||||
|
> issue, owner or date for them: this section, §2 ("listed in §6 for a later rename and is not renamed here"),
|
||||||
|
> §3 ("Code that still carries an old name is named on an *Identifier until renamed* line") and §4's table
|
||||||
|
> ("`mesh-host` an identifier until the code rename"). Two days later the operator believed them done, and
|
||||||
|
> nothing in this record said otherwise. They are tracked by
|
||||||
|
> [issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md),
|
||||||
|
> which holds the inventory of what carries the old names, the risk of renaming the paths of a self-updating
|
||||||
|
> daemon on live nodes, and the staged plan; each stage is owned by the repository it names, and the
|
||||||
|
> glossary's *Identifier until renamed* lines point at the issue. The decision stands. From this date a
|
||||||
|
> record that defers work names the issue that tracks it, with an owner, or the deferral is not accepted
|
||||||
|
> (playbooks 02 and 04; `cycle.py` checks it).
|
||||||
|
|
||||||
## Options rejected
|
## Options rejected
|
||||||
|
|
||||||
- **Machine as the one word, node only as an identifier** — the research's proposal, and what the newest
|
- **Machine as the one word, node only as an identifier** — the research's proposal, and what the newest
|
||||||
|
|||||||
+7
@@ -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
|
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.
|
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
|
**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.
|
Telegram, or from the mesh MCP server." The asker's words never name a channel.
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -120,10 +120,15 @@ The controller resolves three machine facts from it:
|
|||||||
|
|
||||||
The tool runner is given `MESH_AGENT_ACCOUNT` and `MESH_AGENT_HOME` beside the operator's.
|
The tool runner is given `MESH_AGENT_ACCOUNT` and `MESH_AGENT_HOME` beside the operator's.
|
||||||
|
|
||||||
**On the control node the agent account is `agent`.** The other nodes keep their agents as the operator for
|
**On the control node the agent account is `nox`.** The other nodes keep their agents as the operator for
|
||||||
now. The operator's machines do so because the agent works in the operator's desktop and files there, under
|
now. The operator's machines do so because the agent works in the operator's desktop and files there, under
|
||||||
the risk ADR 0234 accepts. A server may follow: it is moved by stating the fact.
|
the risk ADR 0234 accepts. A server may follow: it is moved by stating the fact.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-10.** This sentence named the control node's agent account `agent`. The
|
||||||
|
> operator chose `nox` instead. The name is the account argument of `mesh-controller node agent-account`,
|
||||||
|
> a node fact stated at the controller's terminal, so no code and no other part of this record changes:
|
||||||
|
> the control node still names an agent account of its own, which cannot become root without a person.
|
||||||
|
|
||||||
### 2. The account is declared and has no way to root
|
### 2. The account is declared and has no way to root
|
||||||
|
|
||||||
The `claude-code` module declares the account as a `user` resource named `${machine:agent-account}`, with
|
The `claude-code` module declares the account as a `user` resource named `${machine:agent-account}`, with
|
||||||
@@ -257,6 +262,11 @@ here:
|
|||||||
which seals to the machine that uses the secret, never to a key the caller chooses. **The `token` verb is
|
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
|
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;
|
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
|
- `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,
|
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;
|
holding an Anthropic API key. The licence manager refuses a key of another shape too;
|
||||||
@@ -379,6 +389,10 @@ works on branches and pull requests; what it may merge is said under *What still
|
|||||||
runs as it can still become root: the tool runner and the operator's own shells. No agent runs as it.
|
runs as it can still become root: the tool runner and the operator's own shells. No agent runs as it.
|
||||||
- **Not judged, said plainly:** the ways §3 lists under *not judged*. Each is a way to root this record does not
|
- **Not judged, said plainly:** the ways §3 lists under *not judged*. Each is a way to root this record does not
|
||||||
see. None is known on the control node, and each is left for a later record.
|
see. None is known on the control node, and each is left for a later record.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-10.** That later record had no issue tracking it. The ways §3 lists under
|
||||||
|
> *not judged* are tracked by [issue 344](../04-ISSUES/344-known-gaps-in-the-agents-boundary-accepted-for-now/00-report.md)
|
||||||
|
> (gap 14), beside the other gaps in the agent's boundary accepted for now.
|
||||||
- The tool runner's credential still carries every co-hosted module's grants. The agent cannot read it.
|
- The tool runner's credential still carries every co-hosted module's grants. The agent cannot read it.
|
||||||
- ADR 0181 decided one account per node. This record adds a second, of a different kind: not a person's
|
- ADR 0181 decided one account per node. This record adds a second, of a different kind: not a person's
|
||||||
login, but the one agents run as.
|
login, but the one agents run as.
|
||||||
|
|||||||
+5
@@ -154,6 +154,11 @@ The table of ADR 0239 decision 2 gains these rows:
|
|||||||
held merge of it stays held for a person, as before this record. Reading "every machine running it" from
|
held merge of it stays held for a person, as before this record. Reading "every machine running it" from
|
||||||
the first machines when they are all the machines would close it, and is left for a later change.
|
the first machines when they are all the machines would close it, and is left for a later change.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-09.** The one-machine case above was left for a later change with no
|
||||||
|
> issue, owner or date, which is how ADR 0244's deferred renames went unnoticed (issue 354). It is tracked by
|
||||||
|
> [issue 355](../04-ISSUES/355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md),
|
||||||
|
> owned by mesh-delivery in the controller's repository. The decision stands.
|
||||||
|
|
||||||
> **Progressive insight — 2026-10-09.** *The gap was wider than one machine.* A build the mesh held back for
|
> **Progressive insight — 2026-10-09.** *The gap was wider than one machine.* A build the mesh held back for
|
||||||
> a person and sent on the operator's word travels by a backlog walk, whose record gives one step per
|
> a person and sent on the operator's word travels by a backlog walk, whose record gives one step per
|
||||||
> machine and no "every machine running it" or "the rest running it"; and that walk's last record keeps only
|
> machine and no "every machine running it" or "the rest running it"; and that walk's last record keeps only
|
||||||
|
|||||||
+21
@@ -210,6 +210,27 @@ fails if one changes alone: if the operator's account lost its passwordless `sud
|
|||||||
> 3. **An ordinary line is reads and settings.** §4 is exact as it stands: an ordinary line is the generic verb's
|
> 3. **An ordinary line is reads and settings.** §4 is exact as it stands: an ordinary line is the generic verb's
|
||||||
> reading forms, or a `settings` line composed as the settings verb composes it — nothing else runs.
|
> reading forms, or a `settings` line composed as the settings verb composes it — nothing else runs.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-09, the operator's rule.** The operator types **one** command, `mesh-cli`
|
||||||
|
> (`nox`). `nox-mesh-host` (the node-engine) and `mesh-controller` (the controller) are daemons, and a person
|
||||||
|
> never types their names. The consequence above — a record that says to type `mesh-controller <command>` means
|
||||||
|
> `mesh-cli <command>` — is from this day a rule on every line the mesh addresses to the operator: a condition's
|
||||||
|
> words, a verb's answer, a module's README, a design document name a `nox` line, never a daemon's. Found on
|
||||||
|
> 2026-10-09 at 18:01 by the operator typing the node-engine's `hand-over` of
|
||||||
|
> [issue 339](../04-ISSUES/339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md), which a condition
|
||||||
|
> had told them to run "at the machine": the command on the machine's `PATH` was a copy of an engine from before
|
||||||
|
> that subcommand existed, and the act had no channel
|
||||||
|
> ([issue 356](../04-ISSUES/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)).
|
||||||
|
> Now `nox node hand-over <node> <path>` on the control-node is that act, terminal-only, carried by the controller
|
||||||
|
> to the node's engine over the bus. **How it is checked:** hq's `words.py` refuses a daemon's line —
|
||||||
|
> `mesh-controller` or `mesh-host` followed by one of its commands, and `nox-mesh-host hand-over` — in the
|
||||||
|
> documents it checks from this day, quotations excepted, code blocks and spans included; the catalogue's
|
||||||
|
> `checks/words` refuses the same lines in every string a module's tools can show and every manifest description,
|
||||||
|
> from its copy `retired-lines`, which `words.py --list lines` prints and compares. **What is not a line here:** the
|
||||||
|
> node-engine's install-time subcommands (`nox-mesh-host key`, `enrol`, `overlay take`, `reconcile`, `apply`,
|
||||||
|
> `profile`, `inventory`, `owned`, `bundle`, `version`) are typed as root at a machine no mesh reaches yet, or at
|
||||||
|
> one no mesh can reach any more; the engine's usage says so. The daemons' binaries keep their names. The decision
|
||||||
|
> stands.
|
||||||
|
|
||||||
### 5. What comes back
|
### 5. What comes back
|
||||||
|
|
||||||
**Every line is a call**, run, recorded and answered as a seat's verb is: the bus lets a request be answered for a
|
**Every line is a call**, run, recorded and answered as a seat's verb is: the bus lets a request be answered for a
|
||||||
|
|||||||
+425
@@ -0,0 +1,425 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-09
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 273. The agent's home is the module's, and the operator's personal rules and lessons are registered through it
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0216](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)
|
||||||
|
put the coding agent's configuration under its module at three scopes, and gave the home scope the lightest
|
||||||
|
touch: the module "owns only what it placed" (§5), reports what it did not place and can import it (§6), and
|
||||||
|
"the stale files already in the homes stay until the person removes them". That followed
|
||||||
|
[ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md): inside a home, the
|
||||||
|
person's own rules and skills are *found*, never read, rewritten or removed.
|
||||||
|
[ADR 0243](0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md)
|
||||||
|
added the one channel for removing a found item, on the person's word, with a copy kept.
|
||||||
|
|
||||||
|
[Issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md) then
|
||||||
|
found that the module's tools could not tell the operator from an agent, so an agent confined on the
|
||||||
|
control-node could register a hook, or a rule, that the operator's next session on another node would run or
|
||||||
|
obey. The operator closed it on 2026-10-09: shared text and everything that runs is set at the controller's
|
||||||
|
terminal alone, and the home scope is written only into the home of an agent 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)),
|
||||||
|
never into the operator's. The same day
|
||||||
|
[ADR 0272](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)
|
||||||
|
gave the operator a way to that terminal, and
|
||||||
|
[ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md) gave
|
||||||
|
any module a way to ask the operator a question and act on the answer with a warrant.
|
||||||
|
|
||||||
|
**What the homes hold**, measured on 2026-10-09 through each node's own status tool
|
||||||
|
([research 040](../01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md) §2):
|
||||||
|
|
||||||
|
- a skill [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) §1 retired on 2026-10-02
|
||||||
|
("nothing; it named the predecessor's forge") is still in the home on **all four nodes**;
|
||||||
|
- a person's instruction file is on three; a subagent on one;
|
||||||
|
- the operator's two hand-written rule files, about how the operator wants text handed over and how a second
|
||||||
|
forge's repositories are worked in, are on **the laptop alone**;
|
||||||
|
- the mesh has placed nothing in any home, and raises no condition about any of it.
|
||||||
|
|
||||||
|
On the laptop the directory holds 33 entries: 7 are configuration (the two rules, five skills and a synced
|
||||||
|
folder, an empty commands folder, a settings file with 6 keys, a local settings file the program writes with
|
||||||
|
the permission answers a person gives, the program's plugin records), and the rest is the program's own
|
||||||
|
working state — history, sessions, the memory of 30 projects (1.5 GB), caches, the credentials file
|
||||||
|
([research 040](../01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md) §1).
|
||||||
|
|
||||||
|
**What the agent learns, and where it keeps it.** The agent keeps notes for itself in the program's memory
|
||||||
|
for one repository on the laptop: 45 on 2026-10-09, at least 9 of them a correction the operator gave of how
|
||||||
|
the agent works — finish a module before pushing, use the mesh's tools and not ssh, read the merge gate before
|
||||||
|
merging, no work-arounds for a missing tool. Each was said once, sometimes with irritation, written by the
|
||||||
|
agent in its own words, shown to nobody, read on no other node and in no other repository, and not said again
|
||||||
|
at the moment it matters ([research 040](../01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md) §3).
|
||||||
|
|
||||||
|
**The operator's direction, 2026-10-09**, in their words:
|
||||||
|
|
||||||
|
> "the home-scope does own ~/.claude, so it should be completely empty and manual edits should be forbidden.
|
||||||
|
> Rules in my home directory are my 'personal rules' which should also end up under control of the
|
||||||
|
> claude-code module and all other changes to ~/.claude should go through the claude-code module's mcp tools"
|
||||||
|
|
||||||
|
> "register the decision, but make sure it's clear and that you can/should be pro-active about it. For
|
||||||
|
> example, if I'm frustrated, try to see why and try to prevent it in the future by warning yourself about
|
||||||
|
> stuff not to do to frustrate me — kind of like a self-learning process."
|
||||||
|
|
||||||
|
The record was searched first ("personal rules lessons frustration self-learning"; "hand edit home .claude
|
||||||
|
drift condition"): nothing.
|
||||||
|
|
||||||
|
### Against GENESIS
|
||||||
|
|
||||||
|
- **The mesh owns the truth.** "State lives in the mesh and is derived onto nodes. A file edited on a node
|
||||||
|
is a bug with a delay on it." A rule file written by hand into the agent's home is that file.
|
||||||
|
- **The mesh remembers.** "Nothing has to be explained twice." A correction the operator gives once reaches
|
||||||
|
every session of theirs, and is said again where it applies.
|
||||||
|
- **Failure must be loud.** A hand edit is a condition, not a silent difference the next render erases.
|
||||||
|
- **Nodes are personal; the mesh is a guest.** The program's own working state — memory, history,
|
||||||
|
sessions — stays the program's, excluded by name, exactly as ADR 0182 drew the line.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
The options are weighed in full in
|
||||||
|
[research 040 §5](../01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md);
|
||||||
|
in short:
|
||||||
|
|
||||||
|
### Who owns the configuration under the home
|
||||||
|
|
||||||
|
1. **As today: the module owns what it placed, the rest is found** (ADR 0216 §5). Rejected by the operator,
|
||||||
|
and by the measurement: a rule on one node reaches no other, a retired skill stays on four, nothing is
|
||||||
|
raised. And the guard refuses ssh to a machine but not a write to `~/.claude/rules/`: on a node where the
|
||||||
|
agent runs as the operator, that is a rule planted in every later session, by hand, with no record.
|
||||||
|
2. **The module owns the whole directory and regenerates it.** Rejected, as ADR 0182 rejected it: the
|
||||||
|
working state is the program's, and a render touching the projects' memory erases a season of it.
|
||||||
|
3. **The module owns every configuration file by name, excludes the working state by name, renders the
|
||||||
|
configuration from registrations, and reports a hand edit as a condition.** Chosen.
|
||||||
|
|
||||||
|
### Whose the rules are
|
||||||
|
|
||||||
|
1. **Per node only**, the home scope as ADR 0216 defines it. Rejected as the only scope: the operator is
|
||||||
|
one person on four nodes.
|
||||||
|
2. **Mesh-wide instruction sections.** Rejected as their place: those are the mesh's word about the mesh,
|
||||||
|
written at the terminal. A personal rule is the operator's word about the operator, and follows the
|
||||||
|
person's agent into the home under the person's own names.
|
||||||
|
3. **A new kind, the personal rule, registered for the operator, rendered into the agent's home on every
|
||||||
|
node.** Chosen; the same scope carries the operator's own skills, commands, subagents and settings.
|
||||||
|
|
||||||
|
### How a lesson becomes a rule
|
||||||
|
|
||||||
|
1. **The agent writes the rule itself.** Rejected: an agent that writes its own rules can loosen them, the
|
||||||
|
operator never sees what it taught itself, and text every session reads is written on the operator's
|
||||||
|
word, never an agent's (issue 340).
|
||||||
|
2. **Lessons stay in the agent's memory.** Rejected by the measurement above.
|
||||||
|
3. **A lessons register in the module; a lesson is proposed by an agent, accepted or declined by the
|
||||||
|
operator, rendered as a personal rule on a yes, and said again before the act it bears on.** Chosen.
|
||||||
|
|
||||||
|
### How the operator's yes reaches the operator's home
|
||||||
|
|
||||||
|
1. **Trust the tool's caller.** Rejected in issue 340.
|
||||||
|
2. **The controller's terminal only.** Safe, and kept as one of two ways; alone, it makes a yes in a
|
||||||
|
session a trip to the control-node.
|
||||||
|
3. **A warrant on an authorising ask** (ADR 0259), the exact text bound in the ask. Chosen beside 2.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. The configuration under the agent's home is the module's, whole
|
||||||
|
|
||||||
|
**What "completely empty" means.** The operator's words, "it should be completely empty", are read as: the
|
||||||
|
agent's home holds **nothing a person or an agent put there by hand**. Every configuration file in it is the
|
||||||
|
module's rendering, and the only other entries are the program's own working state, named below.
|
||||||
|
|
||||||
|
**The `claude-code` module owns every configuration file under the agent's home directory** (`~/.claude` of
|
||||||
|
the account the agent runs as, ADR 0266 §5): the home instruction file, `rules/`, `skills/`, `commands/`,
|
||||||
|
`agents/`, `hooks/`, `output-styles/`, `settings.json` and `keybindings.json`. **Nothing there is written by
|
||||||
|
hand.** Each is rendered by the module's code from registrations, on every node, whenever a registration
|
||||||
|
changes, and nothing else writes them. A configuration file the vendor adds later joins the list by name, in
|
||||||
|
the module, before the module renders anything into it.
|
||||||
|
|
||||||
|
**The program's working state is excluded by name**, and stays the program's, in ADR 0182's *found* class
|
||||||
|
or *written by the module's own process*: the credentials file `.credentials.json` (ADR 0183), the history,
|
||||||
|
the sessions and their environments, the projects directory with every project's memory, `todos/`,
|
||||||
|
`plans/`, `statsig/`, the shell snapshots, the file history, the debug, cache, paste and telemetry
|
||||||
|
directories, the statistics, the daemon's files (`daemon/`, its lock, log and status files,
|
||||||
|
`daemon-auth-status.json`, `daemon-auth-cooldown`), the program's dot-markers (`.last-cleanup`), the
|
||||||
|
vendor's synced skills (`skills/synced/`), and the backups, worktrees, jobs, feedback, state and IDE
|
||||||
|
directories. The module never writes or removes any of these, and names the list in its manifest's
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
**Two entries are the program's to write, and watched**, because each decides what a session runs and
|
||||||
|
nothing else would see a change to it: `settings.local.json`, where the program records the permission
|
||||||
|
answers a person gives in a session, and `plugins/`, the program's record of its marketplaces and installed
|
||||||
|
plugins, and their cache. The module never writes them; §4 says what it raises and what the guard refuses.
|
||||||
|
|
||||||
|
**What is neither, by name.** Measured on the laptop on 2026-10-09: `.hal-binding.json`, a leftover of the
|
||||||
|
retired predecessor, and `.credentials.json.testbak`, a copy of the credentials file left by a test. **No
|
||||||
|
copy of the credentials file** (`.credentials.json` followed by anything) belongs in the home: each is
|
||||||
|
reported, and removed on the operator's word with ADR 0243's tool, never kept as a copy in the module's
|
||||||
|
state, because it holds a token. **Every other entry the module knows under neither list is a condition**
|
||||||
|
(§4), so a new vendor file is a question the operator answers, not a default.
|
||||||
|
|
||||||
|
**The keys the program itself writes into `settings.json` on a person's choice** — the model, the spinner,
|
||||||
|
the drafts and the other preferences the vendor's own `/model` and `/config` commands set — are not hand
|
||||||
|
edits. The module keeps a short list of them, preferences only. When the program changes one of those keys,
|
||||||
|
the module takes the new value over into the operator's registered settings (§2), reports it in the status
|
||||||
|
tool, and the change follows the operator to every node. That is ADR 0182's *written into* class, applied
|
||||||
|
the other way round: the file is the module's, and the program is allowed into a few named keys of it. This
|
||||||
|
is the mechanism that keeps a `/model` choice from being reverted, the failure ADR 0182's context records.
|
||||||
|
**A key that decides what runs is never taken over**: `hooks`, `permissions`, `mcpServers` and the other
|
||||||
|
tool-server keys, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, and every key not on the list. Changed on
|
||||||
|
disk, it is written back on the look that finds it (§4).
|
||||||
|
|
||||||
|
**`autoMode` is a permission rule.** It tells the program's unattended mode what it may do without asking,
|
||||||
|
which ADR 0213 calls policy, not preference. It is barred from the operator scope like `permissions` and
|
||||||
|
`hooks`, and belongs in the managed settings, set at the controller's terminal in the module's
|
||||||
|
`managed_settings` for the mesh or a node.
|
||||||
|
|
||||||
|
### 2. A fourth scope, the operator's, and a new kind, the personal rule
|
||||||
|
|
||||||
|
ADR 0216's three scopes stand. A fourth joins them: **operator**. An item registered at the operator scope
|
||||||
|
is rendered into the agent's home on **every node that names an operator account** (ADR 0181): into the
|
||||||
|
operator's own home where the agent runs as the operator, and into the agent account's home where the node
|
||||||
|
names one (ADR 0266). The mesh has one operator; the scope is the person, not a machine.
|
||||||
|
|
||||||
|
The kinds at the operator scope:
|
||||||
|
|
||||||
|
- **personal rule** (new): one short text in the operator's words about how the operator wants the agent
|
||||||
|
to work with them, rendered as one file under `rules/` and listed in the home instruction file, which the
|
||||||
|
module renders as the index of the personal rules, each with where it came from (the terminal, or the
|
||||||
|
lesson it was accepted from) and when. The vendor reads both into every session of that account;
|
||||||
|
- the operator's own **skills, commands and subagents**, under the person's own names, without the plugin's
|
||||||
|
prefix;
|
||||||
|
- the operator's **settings** (`settings.json`), as one registered object of the preference keys of §1,
|
||||||
|
laid under the managed settings as the vendor lays them; and the operator's **keybindings** and **output
|
||||||
|
styles**.
|
||||||
|
|
||||||
|
**Never at the operator scope: a hook, a permission rule (`autoMode` included), a tool server, an
|
||||||
|
environment or key-helper setting, or an item that runs anything.**
|
||||||
|
Those stay the terminal's, in the managed settings and the managed tool-server file (issue 340). A personal
|
||||||
|
rule, skill, command or subagent that holds a shell line or the front matter issue 340 refuses is refused at
|
||||||
|
the operator scope too.
|
||||||
|
|
||||||
|
**The operator's existing items are imported as the first registrations**, through the import tool ADR 0216
|
||||||
|
§6 named, at the operator scope: the two rule files on the laptop become the first two personal rules; the
|
||||||
|
laptop's skills and the workstation's subagent are imported or removed, each on the operator's word
|
||||||
|
(ADR 0243); the preference keys of `settings.json` on the laptop become the operator's registered settings,
|
||||||
|
and its `autoMode` key moves to the managed settings at the terminal, because it is a permission rule (§1);
|
||||||
|
the two leftovers §1 names are removed on the operator's word; the retired `cleanup` skill and the three
|
||||||
|
instruction files are removed with a copy kept. Until each item is imported or removed it
|
||||||
|
is a condition (§4).
|
||||||
|
|
||||||
|
### 3. Who may register into the operator's home, and issue 340's protection stands
|
||||||
|
|
||||||
|
A registration at the operator scope, and a registration at the home scope into an operator's own home, is
|
||||||
|
taken **only** by one of two routes, and refused through any other:
|
||||||
|
|
||||||
|
1. **At the controller's terminal** (ADR 0272), as issue 340 made shared text: the module's settings gain
|
||||||
|
`personal_rules`, `personal_skills`, `personal_commands`, `personal_agents` and `personal_settings`, set
|
||||||
|
with `nox settings set claude-code …` on the control-node. The module's whole layer is already the
|
||||||
|
terminal's, because its settings file is a mergeable file issue 340 counts as trusted.
|
||||||
|
2. **On a warrant** (ADR 0259): the module is an asker. Its register, import and remove tools, called at the
|
||||||
|
operator scope or for an operator's home, do not act; they publish an **approve** ask whose words show the
|
||||||
|
item in full and whose digest binds the exact text, name, kind and scope, and they act once on the warrant
|
||||||
|
for that ask, recording it. Until a channel that proves its sender answers approve for the module, the
|
||||||
|
ask is refused at once with the reason and names the terminal's line.
|
||||||
|
|
||||||
|
**The module's tools stay the operator's way in.** A tool called for the operator's home, while no channel
|
||||||
|
answers approve, does not only refuse: it records the request as **pending** in the module's state (what,
|
||||||
|
at which scope, asked when, from which node) and answers with the exact line to type: `nox settings set
|
||||||
|
claude-code '<json>'`, the request merged into the module's current layer, so that typing it changes nothing
|
||||||
|
else. The status tool lists pending requests; one is dropped when the terminal's layer holds it, or after
|
||||||
|
7 days. An agent asking for a rule therefore hands the operator one line to check and type, never a
|
||||||
|
description to translate.
|
||||||
|
|
||||||
|
The home scope into **an agent account's own home** stays as issue 340 left it: that node's own tools take
|
||||||
|
it, because that home is no person's. A home-scope item never shadows a personal rule: the module renders
|
||||||
|
the operator's items first and refuses a home-scope name an operator-scope item already uses.
|
||||||
|
|
||||||
|
So an agent still cannot plant anything in the operator's home: a tool call without a warrant writes
|
||||||
|
nothing there, a warrant is the router's record of the operator's own answer, and the terminal is the
|
||||||
|
operator's own account on a node where no agent runs as it.
|
||||||
|
|
||||||
|
### 4. A hand edit is a condition, and the guard refuses it
|
||||||
|
|
||||||
|
**On every look**, at least once a minute and at once on a change on disk, the module compares the
|
||||||
|
configuration paths of §1 with what it last rendered, and reads the two watched entries. Any difference is a
|
||||||
|
**finding**, said as the module's own health (ADR 0240) on that node, and the controller raises the
|
||||||
|
condition **`agent-home-drift`** for it, a warning that needs the operator, naming each path and what
|
||||||
|
differs. **The look that finds a difference acts on it**; nothing waits for the next render:
|
||||||
|
|
||||||
|
- **a rendered file changed by hand**, `settings.json` above all: written back on that look, the hand version
|
||||||
|
kept as a copy in the module's state (`home-copies`) and shown by the status tool, so the words are not
|
||||||
|
lost. The one exception is a preference key of §1 the program changed, which is taken over. A `hooks`,
|
||||||
|
`permissions`, tool-server or other key that decides what runs, added or changed in `settings.json`, is
|
||||||
|
**always written back**, never taken over;
|
||||||
|
- **a rendered file missing**: rendered again on that look, and reported once;
|
||||||
|
- **a file nobody rendered** under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/` or
|
||||||
|
`output-styles/`:
|
||||||
|
- **before the node's first adoption** (the imports of §2, done once per node by the operator, and recorded
|
||||||
|
in the module's state): left in place and reported, never removed by the module. The operator's answers
|
||||||
|
are the import tool (register it at a scope) or the removal tool (ADR 0243: a reason, a copy kept);
|
||||||
|
- **after it**: **moved** on that look into `home-copies`, kept and restorable with ADR 0243's restore tool,
|
||||||
|
and raised. A session never loads a file nobody registered for longer than one look. Importing it from
|
||||||
|
the copy is the operator's way to keep it;
|
||||||
|
- **an entry the module knows under neither list of §1**: reported and raised, never touched; the operator
|
||||||
|
removes it on their word or the module's next build names it;
|
||||||
|
- **`settings.local.json` holding any key other than `permissions`**: raised, naming the key. The program
|
||||||
|
writes only that key there, so another is somebody's hand;
|
||||||
|
- **`plugins/` naming a marketplace or an installed plugin other than the vendor's own marketplace and the
|
||||||
|
mesh's `nox-mesh`**: raised, naming it. A plugin carries hooks, so one installed by hand runs in every
|
||||||
|
session.
|
||||||
|
|
||||||
|
The condition clears on the first look that finds no difference. It is never urgent, and the first-node gate
|
||||||
|
never reads it as a fault of a build (as
|
||||||
|
[ADR 0254](0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)
|
||||||
|
treats a wait for a person).
|
||||||
|
|
||||||
|
**The guard on the agent's shell** (ADR 0245) refuses, before it runs, naming the module's tool that does the
|
||||||
|
job and saying that a rule the agent wants is a lesson (§5):
|
||||||
|
|
||||||
|
- any shell command or file edit whose target is under a configuration path of §1 — a write, an append, a
|
||||||
|
copy or move into it, an in-place edit, a removal;
|
||||||
|
- the same aimed at `settings.local.json` or at a `.json` file under `plugins/`;
|
||||||
|
- `claude plugin install`, `claude plugin marketplace add` and the in-session `/plugin install` and
|
||||||
|
`/plugin marketplace add`, whatever the plugin.
|
||||||
|
|
||||||
|
The operator's override of ADR 0245 §5 applies and is recorded. The guard does not watch the rest of the
|
||||||
|
working state.
|
||||||
|
|
||||||
|
### 5. Lessons: an agent proposes, the operator decides, and the agent is reminded
|
||||||
|
|
||||||
|
**The module keeps a lessons register** in its state (ADR 0201): one record per lesson, holding when and on
|
||||||
|
which node it was proposed, the session that proposed it where the agent gave it, the **trigger** (the
|
||||||
|
operator's words, quoted), the **cause** (what the agent did, in one or two sentences), the **rule** that
|
||||||
|
would prevent it (written as a personal rule, in the operator's second person: "do not…", "always…"), an
|
||||||
|
optional **before** (a tool name, a command prefix or a path pattern the rule bears on), and its state:
|
||||||
|
`proposed`, `accepted` or `declined`, with who decided, how and when.
|
||||||
|
|
||||||
|
**When an agent records a lesson.** The mesh's instruction section tells every agent: when the operator
|
||||||
|
expresses frustration (repeats a request, says they have asked before, says "again", "stop", "why did you";
|
||||||
|
irritation in any words), or corrects a way of working ("don't", "always", "from now on", "never"), the agent
|
||||||
|
first works out why — what it did, and what the operator wanted instead — and then records a lesson with
|
||||||
|
the trigger, the cause and the rule, in the same turn, before going on. It says in its answer that it did,
|
||||||
|
in one line. An agent that is unsure whether a correction is a lesson records it; the operator declines what
|
||||||
|
is not.
|
||||||
|
|
||||||
|
**A correction goes to the register, not to the agent's own memory.** The program's per-project memory
|
||||||
|
(§1, working state) stays the agent's notebook for facts about a project; a way of working the operator
|
||||||
|
corrected is a lesson, so that it reaches every node and the operator sees it.
|
||||||
|
|
||||||
|
**The first lessons are the agent's existing corrections.** Once the register exists on a node, the agent
|
||||||
|
there proposes, unasked, every note in its own memory that records a correction the operator gave (at least 9
|
||||||
|
on the laptop on 2026-10-09), each as a lesson with the note's trigger, cause and rule, and says so in one
|
||||||
|
line. The operator accepts or declines them like any other.
|
||||||
|
|
||||||
|
**A proposed lesson is not a rule.** It is rendered nowhere and reaches no session as an instruction. The
|
||||||
|
module shows it to the operator for a yes: an approve ask (§3 route 2) whose words carry the trigger, the
|
||||||
|
cause and the rule, with Accept and Decline as its options; or, until a channel answers approve, the list
|
||||||
|
tool names the proposed lessons and the operator accepts or declines each at the terminal (`nox settings
|
||||||
|
set claude-code …` with the lesson's id in `lessons_accepted` or `lessons_declined`). A proposed lesson
|
||||||
|
expires after 30 days, declined with that reason.
|
||||||
|
|
||||||
|
**An accepted lesson is a personal rule.** The module registers it at the operator scope with the lesson's
|
||||||
|
id as its source, so it renders into the agent's home on every node like any personal rule, and the home
|
||||||
|
instruction file names it as "accepted from lesson N on <date>". **A declined lesson stays in the register
|
||||||
|
with its reason**, and the list tool shows it, so an agent does not propose it again; a new lesson whose rule
|
||||||
|
matches a declined one's is refused with the reason, and the agent is told to say so to the operator rather
|
||||||
|
than record it again.
|
||||||
|
|
||||||
|
**The agent is reminded, before it acts.** Two things make the lessons proactive rather than a file the agent
|
||||||
|
may or may not open:
|
||||||
|
|
||||||
|
- **at session start**, the plugin's session-start hook (the module's own binary, beside the guard) prints
|
||||||
|
the personal rules accepted from lessons in the last 30 days and the ones whose `before` matches the
|
||||||
|
session's working directory or repository, each in one line, so the session begins knowing them;
|
||||||
|
- **before a tool runs**, the guard (ADR 0245 §4, asked about every tool) prints the rule of every accepted
|
||||||
|
lesson whose `before` matches the tool, the command or the path about to be used — a reminder, never a
|
||||||
|
refusal; a refusal is a guard rule and nothing else. The reminder is printed once per session per rule.
|
||||||
|
|
||||||
|
**The register is readable by any agent on any node** through the list tool, and shows `proposed`,
|
||||||
|
`accepted` and `declined` apart, each marked, so a session reading it cannot take a proposal for a rule.
|
||||||
|
Recording a proposal needs no yes: it writes the register and nothing a session obeys.
|
||||||
|
|
||||||
|
### 6. What this changes in the earlier records
|
||||||
|
|
||||||
|
- **ADR 0216 §5 and §6**: the mechanism changes (that record gains a note). The home scope owns the
|
||||||
|
configuration whole; a found item is reported as a condition rather than listed on request; import stays
|
||||||
|
the way in; the plugin's hook file holds the module's own hooks, the guard and the session-start
|
||||||
|
reminder, and nothing registered.
|
||||||
|
- **ADR 0243** stands as the removal on the person's word, now on a warrant or at the terminal for an
|
||||||
|
operator's home, and is the operator's one answer to a found item beside importing it.
|
||||||
|
- **ADR 0182** stands: the class table holds, with the configuration files in the *owned* class and the
|
||||||
|
working state *found*, each named.
|
||||||
|
- **ADR 0245**: the guard gains a refusal (§4) and a reminder (§5).
|
||||||
|
- **Issue 340**: the protection stands; the terminal and the warrant are the two ways into the operator's
|
||||||
|
home.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The operator's rules follow the operator: a rule written once reaches the agent's home on every node
|
||||||
|
within a minute, and a session on any node starts with it.
|
||||||
|
- A hand edit under `~/.claude` is undone within a minute, with the hand version kept, and is said as a
|
||||||
|
condition: no silent difference, and no lost words.
|
||||||
|
- The retired skill and the three stale instruction files, found on four nodes since 2026-10-02, are
|
||||||
|
removed on the operator's word with copies kept, and nothing like them can stay unnoticed again.
|
||||||
|
- A correction the operator gives once becomes a rule everywhere on one yes, and is said again before the
|
||||||
|
act it bears on. The operator sees every rule the agent proposes, and can decline it.
|
||||||
|
- **What got harder:**
|
||||||
|
- a new rule for the agent is no longer a file: it is a registration at the terminal or a warrant on an
|
||||||
|
ask. Until a channel answers approve for the module, every yes is typed on the control-node;
|
||||||
|
- the module watches the home on every node, and its state holds the lessons register and the copies of
|
||||||
|
hand versions. Clearing old copies is not decided here, as ADR 0243 left it;
|
||||||
|
- a vendor file the module does not know is a condition until it is classified, which is the point: an
|
||||||
|
unknown file in the agent's home is a question, not a default;
|
||||||
|
- an agent is expected to notice frustration and record a lesson in the same turn, which costs the turn a
|
||||||
|
tool call. A lesson recorded late, or not at all, is found by reading the session against the register.
|
||||||
|
- **Not decided here:** a per-project lesson (one bound to a repository rather than the operator); lessons
|
||||||
|
for an agent the mesh runs rather than the operator's; and a channel of the operator's own that can
|
||||||
|
tell the operator from an agent (issue 340's "once we build a UI"), which would become a third route.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-09.** The four things this section leaves undecided — clearing old copies
|
||||||
|
> of hand versions, a per-project lesson, lessons for an agent the mesh runs, and a channel of the operator's
|
||||||
|
> own — were left with no issue, owner or date, which is how ADR 0244's deferred renames went unnoticed
|
||||||
|
> (issue 354). They are tracked by
|
||||||
|
> [issue 357](../04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md). The decision stands.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Every configuration path of §1 is rendered from registrations and nothing else; the working state is never written or removed; an entry under neither list is a condition | the module's render test over a seeded home: the configuration files (`keybindings.json` and `output-styles/` included) match the registrations byte for byte; every excluded entry (`todos/`, `plans/`, `statsig/`, `.last-cleanup`, `daemon-auth-cooldown` among them) is byte-identical before and after; an unknown entry, `.hal-binding.json` and a `.credentials.json.*` copy each raise `agent-home-drift` and are left in place; removing a credentials copy keeps no copy of it |
|
||||||
|
| A program-written preference key of `settings.json` is taken over; a key that decides what runs is always written back; `autoMode` is never taken at the operator scope | the module's test: the model key changed on disk appears in the operator's registered settings on the next look and in the rendered file on every node; `hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine` and `autoMode` added or changed on disk are each written back on the look that finds it and never reach the registration; `autoMode` in a `personal_settings` value is refused |
|
||||||
|
| A personal rule renders into the agent's home on every node naming an operator account, under `rules/` and in the home instruction file | the module's render test for one machine of three: the operator's own home on two, the agent account's home on the one naming one; live: a rule set at the terminal is in a new session's context on each node |
|
||||||
|
| Nothing that runs is taken at the operator scope | the module's tests: a hook, a permission rule, a tool server, and a skill, command or subagent holding a shell line or the refused front matter are each refused at the operator scope with issue 340's reason |
|
||||||
|
| A registration into an operator's home is taken only at the terminal or on a warrant | the module's tests through its real tool dispatch: each register, import and remove tool at the operator scope, or for an operator's home, publishes an ask and writes nothing; the same with a warrant for that ask acts once; a warrant for another ask, or a replayed one, is refused; while no channel answers approve the ask is refused at once naming the terminal's line. The controller's test: the module's layer is the terminal's (issue 340's) |
|
||||||
|
| A hand edit is a condition, undone on the look that finds it | the module's tests with a clock: a rendered file changed by hand (`settings.json` among them) is written back on the same look, within a minute, its hand version kept and shown; a missing one is rendered again on that look; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault |
|
||||||
|
| Before a node's first adoption a file nobody rendered is left; after it, it is moved to the copies and raised | the module's tests: an unrendered file under each of `rules/`, `skills/`, `commands/`, `agents/`, `hooks/`, `output-styles/` is reported and left before the adoption is recorded; after it, the same file is gone from the home on that look, present in `home-copies`, restorable with the restore tool, and the condition names it |
|
||||||
|
| `settings.local.json` and `plugins/` are watched | the module's tests: a key other than `permissions` in `settings.local.json` raises the condition naming it; a marketplace or installed plugin other than the vendor's and `nox-mesh` in `plugins/` raises it naming it; neither file is written by the module |
|
||||||
|
| The guard refuses a write under a configuration path, to `settings.local.json` or `plugins/*.json`, and a plugin install, and lets the working state alone | the module's guard tests: a redirect, append, copy, move, in-place edit and removal under each path, and aimed at `settings.local.json` and a `.json` under `plugins/`, refused naming the tool; `claude plugin install`, `claude plugin marketplace add` and their in-session forms refused; the same write under `projects/` passes; the operator's override honoured and recorded |
|
||||||
|
| A home-scope name may not shadow a personal rule | the module's test: with a personal rule, skill, command or subagent registered at the operator scope, a home-scope registration of the same name and kind is refused naming the operator-scope item, and the render holds the operator's item |
|
||||||
|
| A tool called for the operator's home leaves a pending request and the exact terminal line | the module's tests: the call writes nothing into the home, records one pending request, and answers a `nox settings set claude-code '…'` line which, applied to the current layer, adds that item and changes no other key; the request is dropped once the layer holds it, and after 7 days |
|
||||||
|
| A lesson is recorded with its trigger, cause and rule; a proposal renders nowhere; an accepted one is a personal rule; a declined one is not proposed again | the module's tests over the register: register, list, accept, decline, expiry; the render test that only accepted lessons reach `rules/`; a second proposal matching a declined rule refused with the reason |
|
||||||
|
| The agent is reminded at session start and before a matching tool | the module's hook tests: the session-start hook prints the recent and matching rules; the guard prints a matching lesson's rule once per session and refuses nothing for it |
|
||||||
|
| An agent records a lesson when corrected, and proposes its existing corrections first | the module's render test: the rendered managed instruction file's `working-on-the-mesh` section holds the paragraph on when a lesson is recorded, that a correction goes to the register and not to the agent's memory, and that existing corrections are proposed first, and the test fails when the paragraph is taken out of the section; live: a correction given in a session is in the register within that turn, and the laptop's memory corrections are in the register as proposals after WP9 |
|
||||||
|
| Live, at acceptance (the operator's view) | on the laptop: `~/.claude` holds the rendered configuration and the program's working state and nothing else; a rule written by hand into `rules/` raises `agent-home-drift` within a minute and is listed by the status tool; `nox settings set claude-code '{"personal_rules": …}'` on the control-node puts the rule into a new session on every node; a correction given in a session appears in the lessons list, is accepted at the terminal, and the next session on another node names it at start |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 040](../01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md):
|
||||||
|
the measurement on four nodes, the agent's memory, the options.
|
||||||
|
- [ADR 0216](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md),
|
||||||
|
[ADR 0182](0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md),
|
||||||
|
[ADR 0243](0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md):
|
||||||
|
the home before this record.
|
||||||
|
- [ADR 0245](0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md): the guard.
|
||||||
|
- [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md),
|
||||||
|
[ADR 0272](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):
|
||||||
|
the two ways the operator's word reaches the mesh.
|
||||||
|
- [ADR 0266](0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md),
|
||||||
|
[ADR 0181](0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md): whose home the agent's is.
|
||||||
|
- [ADR 0240](0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md),
|
||||||
|
[ADR 0254](0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md):
|
||||||
|
the condition and the gate.
|
||||||
|
- [Issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md): the protection kept.
|
||||||
|
- [To-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) §1, §8 and §10,
|
||||||
|
[to-be 40](../03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md) WP7–WP10: the design and the build.
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
---
|
||||||
|
topic: what runs on it
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-10
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 274. An agent proposes a shared instruction section, and every node takes it only on the operator's warrant, which it proves itself
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[Issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md) found that the
|
||||||
|
agent module's tools cannot tell the operator from an agent: the tool runner acts for any caller of the mesh MCP
|
||||||
|
server and is not told who called. An instruction section registered for every node lands in the managed instruction
|
||||||
|
file, which every Claude Code session on those nodes reads first, as the mesh's own word. Anything that could write
|
||||||
|
one could ask the operator's agent on every node to act for it, at the next session there: a prompt injection that
|
||||||
|
stays. The operator decided on 2026-10-09 that shared text is set at the controller's terminal, in the module's
|
||||||
|
settings, "for now", and named the way back: "once we build a UI, we can also update the context through it". A
|
||||||
|
surface of the operator's own, which can tell the operator from an agent, was the intended way.
|
||||||
|
|
||||||
|
That surface exists now. [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)
|
||||||
|
lets any module ask the operator a question and act on the answer with a **warrant**: the router's record that a
|
||||||
|
person chose one option of one ask, published on a subject only the router may publish and kept in a record only the
|
||||||
|
router writes. An option at the level **approve** is carried only by a channel that proves its sender (Telegram,
|
||||||
|
linked to the operator's account, placed where no agent can become root), so a warrant for it is the operator's
|
||||||
|
own answer. The controller already acts on warrants, and its `rehearse` command shows the whole path live.
|
||||||
|
|
||||||
|
The operator asked on 2026-10-10: "our claude-code module should have mcp-tools to add mesh-shared
|
||||||
|
instructions/context". Agents keep shared text up as they work: a rule learned, a hand-over from one session to the
|
||||||
|
next on another node, the state of a piece of work. Today each of those is a trip to the control-node's terminal,
|
||||||
|
a line typed with the text as a list of lines, which an agent can only hand the operator to type.
|
||||||
|
|
||||||
|
The record was searched first ("propose instructions approval warrant", "shared instructions agent hand-over
|
||||||
|
context"): nothing beside ADR 0273, whose §3 route 2 already puts the operator's personal rules behind a warrant on
|
||||||
|
an authorising ask, and issue 340.
|
||||||
|
|
||||||
|
**Where it would land, measured in the module's code on 2026-10-10.** A registration is a key in the module's
|
||||||
|
`config` state on the bus. Every node's tool runner that carries the module writes that state, and every node renders
|
||||||
|
what it finds there. So a record on the bus proves nothing about who put it there: a warrant copied into the bus beside
|
||||||
|
a text is a string anyone with a tool runner's credential could write. Issue 340 lists the same fact as still open
|
||||||
|
for text already there ("Text at the mesh or node scope written straight into the bus state still renders").
|
||||||
|
|
||||||
|
### 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 approved text is the mesh's, rendered on every node it is for, not a file an
|
||||||
|
agent edits.
|
||||||
|
- **The mesh remembers.** A hand-over written once reaches the next session on every node, so nothing has to be
|
||||||
|
explained twice.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
### Who may make a shared section active
|
||||||
|
|
||||||
|
1. **The tool's caller, as before issue 340.** Rejected in issue 340: the caller may be any agent.
|
||||||
|
2. **The controller's terminal alone** (issue 340's decision). Safe, and kept as one of two ways. Alone, it makes
|
||||||
|
every shared text a trip to the control-node, and an agent can only describe what to type.
|
||||||
|
3. **The operator, on a warrant: an agent proposes, the operator approves on a channel that proves who answered.**
|
||||||
|
Chosen, beside 2.
|
||||||
|
|
||||||
|
### Who proves the warrant, and where the approved text is kept
|
||||||
|
|
||||||
|
1. **The instance that hears the warrant writes the text into the module's state on the bus**, as a registration,
|
||||||
|
and every node renders it. Rejected: the state is written by every tool runner carrying the module, so every node
|
||||||
|
would render whatever is written there with a warrant's shape beside it. That is issue 340's open item, widened.
|
||||||
|
2. **The controller verifies the warrant and writes the text into the module's settings**, the terminal's own store.
|
||||||
|
Weighed. It is durable, and a node that joins later gets it. But it makes the controller write a key it keeps for
|
||||||
|
the terminal on another module's ask, and lets any asker's warrant write terminal-only settings once that path
|
||||||
|
exists. It also takes a module's own decision (what its ask says, what its answer does) into the controller.
|
||||||
|
3. **Every node proves the warrant itself and keeps the approved text in a file of its own.** Each instance reads
|
||||||
|
the router's own record of the ask under the module's name, composes the ask again from the proposal, holds the
|
||||||
|
warrant to it, and only then writes the section into its own file, which is what it renders. Chosen. A record
|
||||||
|
written into the bus by anyone else becomes active nowhere, and a record deleted from the bus removes nothing.
|
||||||
|
|
||||||
|
### What "context" is
|
||||||
|
|
||||||
|
1. **A separate managed file of shared context** that sessions are told to read. Rejected: a second file, a second
|
||||||
|
way to reach a session, and a second thing to prove, for text the instruction file already carries.
|
||||||
|
2. **An instruction section.** Chosen. A hand-over, a rule or the state of a piece of work is a named section, read
|
||||||
|
first in every session it reaches, approved, replaced and removed like any other. One mechanism.
|
||||||
|
|
||||||
|
### Which kinds may be proposed
|
||||||
|
|
||||||
|
1. **Every text kind** (sections, output styles, skills, commands, subagents). Rejected for now: a skill is invoked
|
||||||
|
by the model on its own when its description matches, may carry scripts, and its front matter decides what runs.
|
||||||
|
Approving one on a phone is a different judgement from reading a paragraph.
|
||||||
|
2. **Instruction sections only.** Chosen. The other kinds stay the terminal's, as issue 340 left them.
|
||||||
|
|
||||||
|
### How the operator reads what they approve
|
||||||
|
|
||||||
|
1. **The whole text on the phone, always.** Rejected: the router's content rule refuses a path, an address or a
|
||||||
|
long unbroken word in any message that leaves the mesh, and a message has a length bound.
|
||||||
|
2. **A fingerprint only.** Rejected: nobody approves what they cannot read.
|
||||||
|
3. **The whole text when it is short and every line may leave the mesh; else the first lines, the size and a
|
||||||
|
fingerprint, and the line that shows all of it at the controller's terminal.** Chosen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. An agent proposes; nothing is active until the operator approves
|
||||||
|
|
||||||
|
The agent module (`claude-code`) gains two tools:
|
||||||
|
|
||||||
|
- **`claude_code_instructions_propose`** takes a scope (`mesh` for every node, or `node` and a list of nodes), a
|
||||||
|
name and the text, or `remove` for a section approved before. It refuses what issue 340 refuses and more (§5),
|
||||||
|
records the **proposal** in the module's `proposals` state (the action, the scope, the nodes, the name, the text,
|
||||||
|
the text's SHA-256 digest, the node it came from and when), and publishes one ask on the `operator-channel` seat
|
||||||
|
under the module's own name. It answers with the proposal's id, its fingerprint (the digest's first 24
|
||||||
|
hexadecimal digits, in groups of four) and the question the operator was asked. **It changes nothing on any node.**
|
||||||
|
- **`claude_code_proposals`** lists the proposals, newest first: what each does, where it reaches, its
|
||||||
|
fingerprint, and what this node made of it (waiting, approved, declined, discarded at expiry, refused). Given an
|
||||||
|
id it answers that proposal's whole text and digest. It also lists the sections approved on this node.
|
||||||
|
|
||||||
|
A proposal that would replace an approved section of the same name and scope says *Replace*. A removal is a
|
||||||
|
proposal too (§4).
|
||||||
|
|
||||||
|
### 2. The ask
|
||||||
|
|
||||||
|
One ask per proposal, composed from the proposal alone, so that every node composes the same one:
|
||||||
|
|
||||||
|
- **headline**: "Add Claude instructions '<name>'?" ("Replace", "Remove"), shortened to "Add Claude instructions?"
|
||||||
|
when the name makes it longer than the router allows, and ending "Part shown" whenever the phone does not show
|
||||||
|
the whole text;
|
||||||
|
- **explanation**, first line: "Add instructions '<name>' to every Claude session on the mesh?", or "on <the
|
||||||
|
nodes>"; then the node it was proposed from and when, the text's lines, bytes and fingerprint, and that every new
|
||||||
|
session there reads it as the mesh's own word until it is removed the same way; then **the whole text** when it is
|
||||||
|
at most 2000 bytes and 40 lines and every line may leave the mesh, or else "NOT SHOWN IN FULL here" and **its first
|
||||||
|
lines** (at most 20 lines and 1200 bytes), each line that may carry a path, an address or a long word replaced by a marker, and the line
|
||||||
|
that shows it all: `mesh-cli ask claude-code claude_code_proposals '{"id": "<id>"}'` at the controller's
|
||||||
|
terminal, whose fingerprint must be the one on the phone;
|
||||||
|
- **two options, Approve and Decline, both at the level approve**, as the controller's rehearsal asks: only a
|
||||||
|
channel that proves who answered carries either, so a click on a channel that does not prove its sender ends
|
||||||
|
nothing. Each option binds the digest of its act: the proposal's id, action, scope, nodes, name, the text's
|
||||||
|
digest, where it came from and when, and the answer;
|
||||||
|
- **expiry**: a day less ten minutes after it was proposed; at expiry the proposal is discarded and nothing
|
||||||
|
changes. An ask that authorises never defaults (ADR 0259);
|
||||||
|
- **about**: the scope, the nodes and the name, so a newer proposal for the same section replaces an older one
|
||||||
|
still open, and the older one is discarded.
|
||||||
|
|
||||||
|
The router caps open asks at three per asker. A fourth proposal is refused by the module, in words, before it is
|
||||||
|
asked.
|
||||||
|
|
||||||
|
### 3. Every node proves the warrant itself
|
||||||
|
|
||||||
|
A proposal record on the bus proves nothing, because every tool runner carrying the module writes that state. So
|
||||||
|
**each node**, for each proposal it has not decided yet, on every word from the router, on every change of the
|
||||||
|
proposals, and once a minute:
|
||||||
|
|
||||||
|
1. reads the router's **own record** of the ask, under the module's name. The bus lets only the router write that
|
||||||
|
record, and lets the module read only its own asks (ADR 0259 §3). An ask still open is left for the next look;
|
||||||
|
2. judges the proposal as the module would have made it (§5's refusals, its name, scope, nodes, where it came from,
|
||||||
|
its action, and the text against its digest), because anything holding a tool runner's credential can write a
|
||||||
|
record and ask for it under the module's name; then composes the ask again from the proposal, and holds the
|
||||||
|
warrant to it with the SDK's own check: the same asker
|
||||||
|
and ask, **the ask's digest the router took**, a person's choice, an option the ask offered, at that option's
|
||||||
|
level, given before the ask expired. Then the chosen option's bound act must be this proposal's act, and the
|
||||||
|
text's digest must be the digest the proposal names. An ask published under the module's name with other words,
|
||||||
|
a proposal changed after it was asked, a warrant for another ask, or an answer at another level is refused and
|
||||||
|
said, and nothing is applied. A refusal is judged again on every look rather than kept, so an honest approval
|
||||||
|
refused for a reason that passes (a node still on an older version of the module) is taken once it holds;
|
||||||
|
3. on **Approve**, writes the section into **its own file** in the module's state directory, if the proposal reaches
|
||||||
|
this node (the mesh scope, or a node scope naming it), and renders. On **Decline**, an expiry, a cancel, a
|
||||||
|
replacement or the router's refusal it applies nothing. Either way **it records the proposal as decided and never
|
||||||
|
decides it again**: a warrant heard twice, or replayed into the bus, changes nothing;
|
||||||
|
4. takes closed asks oldest warrant first, and keeps, for each section, when the last change applied to it was
|
||||||
|
approved: **an approval older than that is never applied**, so an earlier approval read late, or replayed into the
|
||||||
|
bus, never undoes a later replacement or removal.
|
||||||
|
|
||||||
|
A section is its scope, its nodes and its name together: a proposal of the same name for other nodes is another
|
||||||
|
section, and replaces or removes nothing of the first.
|
||||||
|
|
||||||
|
**A record that cannot be read is never taken for an empty one.** The node's own file holding the approved sections
|
||||||
|
and every decision is written whole, synced to the disk before it takes the old one's place; when it cannot be
|
||||||
|
read, the node decides nothing, proposes nothing and renders no approved section, and says why.
|
||||||
|
|
||||||
|
The managed instruction file renders the approved sections under their own heading, "Instructions the operator
|
||||||
|
approved", each with where it was proposed from and when it was approved, after the mesh's own text, the
|
||||||
|
instructions table and the terminal's sections.
|
||||||
|
|
||||||
|
**What the operator approved and when is the router's record**, kept 30 days after the ask closes. A node that was
|
||||||
|
away for longer than that, or joins later, cannot prove an approval made before, and **does not render it**: its
|
||||||
|
`claude_code_proposals` says which approvals it could not prove, and the operator proposes the section again or sets
|
||||||
|
it at the terminal. That is the cost of option 3 and is accepted: an approval the node cannot prove is the case this
|
||||||
|
decision exists to refuse. The same holds the other way: **a removal whose record is deleted from the bus before a
|
||||||
|
node read it is not seen there**, so that node keeps the section; the node's list shows what it renders, and the
|
||||||
|
operator proposes the removal again.
|
||||||
|
|
||||||
|
### 4. A removal needs the same approval
|
||||||
|
|
||||||
|
An approved section is removed only by a proposal with `remove`, approved the same way, whose ask names the section,
|
||||||
|
its scope and nodes, and the fingerprint of the text it removes. A node removes the section only when the approved
|
||||||
|
removal names the text it holds. A proposal record deleted from the bus removes nothing, because the node's own file
|
||||||
|
is what renders. A section set at the terminal is changed there, and a proposal of the same name is refused.
|
||||||
|
|
||||||
|
### 5. What is refused, before anything is asked
|
||||||
|
|
||||||
|
- **What issue 340 refuses, kept**: a shell line (`` !`…` `` or a block opened with ```` ```! ````); front matter of
|
||||||
|
any kind, naming `hooks`, `mcpServers`, `permissionMode` or `allowed-tools` when it holds one; and **any `@` at the
|
||||||
|
text's start or after anything but a letter or a digit** (any whitespace, Unicode's included, or punctuation),
|
||||||
|
which the instruction file may read in as a file. Wider than the vendor's pattern on purpose;
|
||||||
|
- **what a reader cannot see and a session reads**: a control character but a line break and a tab, a format
|
||||||
|
character (zero-width characters, bidirectional overrides, the tag block), a variation selector, a letter that
|
||||||
|
shows blank, private use, a noncharacter, and any space or separator but the plain space. The operator approves what was shown, so nothing unshown may ride along;
|
||||||
|
- a heading of level one to three, in any of its forms (`#`, a line underlined with `=` or `-`, HTML), which would
|
||||||
|
read as one of the managed file's own sections;
|
||||||
|
- anything shaped like a secret (a key block, a known token's shape, a value given to a secret's name), because the
|
||||||
|
text is read by every session and shown on a phone;
|
||||||
|
- an empty text, one that is not text, or **more than 16 KiB**: every session reads all of it, every time;
|
||||||
|
- the home scope (an account's own directory keeps its own rules, ADR 0273) and `all` at the node scope (that is the
|
||||||
|
mesh scope);
|
||||||
|
- a name the controller's terminal sets;
|
||||||
|
- **when no channel that proves its sender is on the mesh** (no channel kind promising a choice, an exact rendering
|
||||||
|
and a verified sender): refused at once, naming the terminal's line instead. The router judges again when it takes
|
||||||
|
the ask, and refuses an ask none of its ready channels can carry; that refusal reaches every node as the ask's end,
|
||||||
|
and the proposal is discarded. **Nothing waits for an answer that cannot come, and nothing is active without one.**
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-10.** This said the module refuses at once "when no channel that proves its
|
||||||
|
> sender is on the mesh", judged from the channel kinds in its own membership. The controller issues those kinds
|
||||||
|
> only to a module that uses or watches a kinded bench, and an asker uses the operator channel alone, so the module
|
||||||
|
> saw no kinds and refused every proposal on the live mesh, with Telegram linked and proven. Instead: the module
|
||||||
|
> refuses at once only when kinds are issued to it and none proves its sender; otherwise the router judges, as
|
||||||
|
> ADR 0259 §4 has it, and the module waits a few seconds for its word, says a refusal to the proposer, and
|
||||||
|
> discards the proposal on every node that hears it. A proposal the router keeps no record of after its ask's
|
||||||
|
> time is discarded too. The decision stands: nothing waits for an answer that cannot come, and nothing is active
|
||||||
|
> without one.
|
||||||
|
|
||||||
|
### 6. The warrant is recorded in the hand-act log
|
||||||
|
|
||||||
|
The controller gains the verb **`warranted`** (asker, ask, and nothing else). It reads **the router's own record**
|
||||||
|
of that asker's ask, never the caller's account of it, and records who chose, through which channel, with which proofs
|
||||||
|
and which answer, under the verb `warrant` that the hand-act log already counts as a person's decision, not a repair.
|
||||||
|
It records once per ask, under an id the ask decides, however many of the module's instances ask. It refuses an ask
|
||||||
|
still open, one that ended without a person's choice, and another asker's. Every node that decided an Approve or a
|
||||||
|
Decline asks it, again every ten minutes for a week while the controller has not taken it, so a node that is away
|
||||||
|
leaves nothing unrecorded.
|
||||||
|
|
||||||
|
### 7. Nothing proposed is logged
|
||||||
|
|
||||||
|
The module's log names a proposal by its id, action, scope, name and fingerprint. It never logs the text, and never
|
||||||
|
the ask's explanation, which carries it.
|
||||||
|
|
||||||
|
### 8. What this changes in the earlier records
|
||||||
|
|
||||||
|
- **Issue 340, decision 2** ("shared agent text is the terminal's, for now"): instruction sections at the mesh and
|
||||||
|
node scopes gain a second way, the operator's warrant, beside the terminal. The other kinds stay the terminal's.
|
||||||
|
The registering tools still refuse every shared kind and now name the proposal tool. The issue's still-open item
|
||||||
|
about text already on the bus is unchanged: approved sections never come from there.
|
||||||
|
- **ADR 0273 §3** stands. Its route 2, a warrant for the operator's home, is built the way §3 here proves a warrant:
|
||||||
|
on every node, from the router's own record, against an ask composed again from what was proposed.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- An agent hands work over to the next session on any node with one tool call and one tap on the operator's
|
||||||
|
phone. The operator sees the text, or its first lines and a fingerprint, before it reaches a session.
|
||||||
|
- No agent, and nothing holding a tool runner's credential, can make text active in another session: what it
|
||||||
|
writes to the bus is a proposal, and every node checks the router's own record against the exact text.
|
||||||
|
- **What got harder:**
|
||||||
|
- every shared section is a question to the operator, at most three open at once;
|
||||||
|
- a long text is read at the controller's terminal before it is approved;
|
||||||
|
- a node away for more than 30 days, or joining later, does not render a section approved before, and says so;
|
||||||
|
- each node keeps its own copy of what it approved, so `claude_code_proposals` on one node speaks for that node;
|
||||||
|
- the module now asks the operator, so it uses the `operator-channel` seat, and its grant on the bus grows by
|
||||||
|
the asker's subjects and its own records there.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| A proposal changes nothing until the operator answers, and asks at the level approve | the module's test: the proposal's ask is published under `ask.claude-code`, passes the SDK's `Check`, offers Approve and Decline both at the level approve, each binding an act digest; the open ask leaves the rendered instruction file without the text |
|
||||||
|
| The text becomes active on a node only on a warrant that node proved | the module's test: an approval in the router's record makes a second node render the section; a decline, an expiry, the router's refusal, a text changed after the ask (with or without its digest, and past what the phone shows), another ask's words under the module's name, an answer after expiry, at the level acknowledge, with nobody choosing, or for another ask each render nothing and are recorded as decided |
|
||||||
|
| A warrant is acted on once | the module's test: the same warrant read again changes nothing and is recorded once; an approval replayed into the bus after its section was removed brings nothing back |
|
||||||
|
| A removal needs the same approval; a deleted record removes nothing | the module's test: deleting the proposal record leaves the section rendered; proposing the removal leaves it; the approved removal takes it out |
|
||||||
|
| A node scope reaches only its nodes | the module's test: a section approved for one node renders there and not on another |
|
||||||
|
| Fail closed when no channel proves its sender, or the bus refuses the ask | the module's test: with no verified-sender channel the proposal is refused naming the terminal's line, and nothing is kept or asked; an ask the bus refuses leaves the proposal never asked and never active |
|
||||||
|
| What a reader cannot see, or the file would read in, is refused | the module's test: an `@` after a vertical tab, a no-break space, an ideographic space, a line separator or a bracket; tag characters, a zero-width space, a bidirectional override, an escape, a carriage return, private use, and headings of level one to three are each refused; an e-mail-like word and a fourth-level heading are taken |
|
||||||
|
| A record written straight into the bus is judged before its approval applies | the module's test: a record with an import, a heading in its name, its nodes out of order, or too long, asked for under the module's name and approved, is refused and renders nothing |
|
||||||
|
| An older approval never undoes a later one | the module's test: a node that reads a removal before the addition it removes leaves the addition superseded and the section absent |
|
||||||
|
| An unreadable record is never taken for an empty one | the module's test: over an unreadable record nothing is decided, proposed or written |
|
||||||
|
| A section for some nodes is its own | the module's test: the same name for other nodes is an addition, and a removal for them is refused |
|
||||||
|
| Issue 340's refusals hold | the module's test: a shell line, a shell block, front matter (with `hooks` or `allowed-tools`, or any), an `@` reference, a key block, a value given to a token's name, more than 16 KiB, a NUL, the home scope, `all` at the node scope and a name the terminal sets are each refused and nothing is asked |
|
||||||
|
| A long text is shown in part and read whole at the terminal | the module's test: a line with a path is not on the phone, the explanation names the terminal's line and the fingerprint, and `claude_code_proposals` with the id answers the whole text and digest |
|
||||||
|
| The text is never logged | the module's test: the module's log over a proposal and its approval holds the id and not a line of the text |
|
||||||
|
| The tools are served | the module's test through its real tool dispatch: propose, list, and the registering tool's refusal naming the proposal tool |
|
||||||
|
| The warrant is recorded once, from the router's record | the controller's tests: the entry carries who chose, via which channel and the proofs from the router's warrant; an ask still open, ended without a choice, another asker's or another ask's, or the controller's own is refused; the entry is written once however often it is asked, against a real bus; a word of the caller's own is refused; the verb's line is not taken for a repair and is not the terminal's alone |
|
||||||
|
| Live, after rollout | an agent proposes a hand-over for every node; the operator's phone shows "Add instructions '…' to every Claude session on the mesh?" with Approve and Decline; after Approve a new session on another node reads the text, and `hand-acts` shows the warrant |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Issue 340](../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md) — the
|
||||||
|
decision this amends
|
||||||
|
- [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
|
||||||
|
and the warrant
|
||||||
|
- [ADR 0273](0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)
|
||||||
|
— the module owns the agent's home; the operator's rules on a warrant
|
||||||
|
- [ADR 0216](0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md)
|
||||||
|
— the three scopes
|
||||||
|
- [to-be 36](../03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md) §11 — the design
|
||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
topic: how we work
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-10
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# 275. 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
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md) measured, on 2026-10-10, how this
|
||||||
|
repository keeps its issues, after the operator asked that day for an issue tracker on the bus that every current issue
|
||||||
|
is consolidated into, started small and extended along the way.
|
||||||
|
|
||||||
|
**Numbers collide.** An author takes the next free number by reading `main` and every open pull request
|
||||||
|
([playbook 03](../00-META/process/03-issues.md)), and many branches write at once. 31 commits on `main` mention a
|
||||||
|
renumbering; [issue 155](../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md) records a
|
||||||
|
collision that reached `main` with every check passing; three decision numbers collided on the evening of 2026-10-07,
|
||||||
|
and on 2026-10-10 an open pull request still carried a branch named for an issue number another branch had taken
|
||||||
|
first. `cycle.py` refuses two records sharing a number, which catches a collision after it happens and prevents none.
|
||||||
|
The operator said on 2026-10-07 that numbering belongs to the mesh's own issue tracker.
|
||||||
|
|
||||||
|
**Status is hard to see.** Of 357 issue folders on `main`, 73 were not closed (27 `open`, 45 `located`, 1
|
||||||
|
`diagnosing`), and finding them means reading frontmatter across the repository. The frontmatter is a small, regular
|
||||||
|
data set: six keys over 1.9 MiB of markdown, well inside the limits
|
||||||
|
[ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||||
|
§6 sets for a bucket. Nothing connects a condition the controller raises to the issue that would fix it: on 2026-10-10
|
||||||
|
a condition was silenced with the words "hq issue to follow", and nothing records whether one followed.
|
||||||
|
|
||||||
|
**hits**, the headless issue tracker the operator named as inspiration, was studied at release v0.7.0 (findings §6). Its
|
||||||
|
model is this repository's frontmatter almost key for key, and it numbers an issue by compare-and-set on a counter only
|
||||||
|
after the issue is validated. Its licence (Sustainable Use License 1.0) limits distribution, its services create their
|
||||||
|
own bus resources on start, and its words (*item*, *project*, *release*) clash with the glossary.
|
||||||
|
|
||||||
|
The research left five questions for the operator, each with a recommendation. **The operator delegated the choice on
|
||||||
|
2026-10-10**: the recommendations are taken, in the spirit of the original ask, "start small and extend the module
|
||||||
|
along the way".
|
||||||
|
|
||||||
|
### Against GENESIS
|
||||||
|
|
||||||
|
- **The mesh owns the truth** ([mission](../00-META/mission.md)). An issue's number and status live in the mesh, in one
|
||||||
|
place, written through one module, instead of in a file every branch may edit.
|
||||||
|
- **The mesh remembers** ([effect](../00-META/effect.md)). The public narrative of a design or governance issue stays
|
||||||
|
here, reviewed in pull requests, so the symptom-to-component memory is kept.
|
||||||
|
- **Failure must be loud.** The cycle's issue rules refuse a wrong write at the moment it is made, instead of a check
|
||||||
|
reporting it at merge.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
### Where the truth lives (open question 1)
|
||||||
|
|
||||||
|
1. **Split: the tracker holds every issue's number and status; hq keeps a folder only for a design or governance issue,
|
||||||
|
pointing at the tracker by number.** Adopted.
|
||||||
|
2. **Everything moves into the tracker.** Rejected: the public record of design questions, which hq exists to be
|
||||||
|
([ADR 0019](0019-how-this-repository-works.md)), would be lost, and with it the memory playbook 03 says is never
|
||||||
|
deleted.
|
||||||
|
3. **The tracker mirrors hq.** Rejected: hq would still hand out numbers, so the collisions stay, and a second copy of
|
||||||
|
every status can drift from the first.
|
||||||
|
|
||||||
|
### Whose code (open question 2)
|
||||||
|
|
||||||
|
1. **Run hits as it is.** Rejected: its services create their own stream and bucket, which ADR 0201 §3 forbids a module
|
||||||
|
to do; its subjects assume one tracker per bus account; its actor is named by the caller rather than proven by the
|
||||||
|
bus credential; its words clash with the glossary.
|
||||||
|
2. **Vendor or fork hits into the catalogue.** Rejected: its licence is not an open-source licence, and vendoring binds
|
||||||
|
the catalogue to it for as long as the code is there; changing the points above means owning a fork of a young,
|
||||||
|
single-author project.
|
||||||
|
3. **Build our own module in Go on hits's model.** Adopted. Its statuses, fields, append-only notes, typed links,
|
||||||
|
compare-and-set numbering and its auditor are copied as ideas, and the module's README says so.
|
||||||
|
|
||||||
|
### Which numbers (open question 3)
|
||||||
|
|
||||||
|
1. **Issue numbers only.** Rejected: decision numbers collided three times in one evening, and the same counter serves
|
||||||
|
any sequence.
|
||||||
|
2. **Issue, decision and research numbers, all from the tracker.** Adopted, with decision and research numbers in
|
||||||
|
slice 2.
|
||||||
|
|
||||||
|
### The word (open question 4)
|
||||||
|
|
||||||
|
1. **Adopt the operator's word *ticket*.** Rejected: *issue* is already the glossary's word for this thing, and one word
|
||||||
|
names one thing ([ADR 0244](0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)). *Ticket*
|
||||||
|
also already names something else: an answer's one-time token
|
||||||
|
([ADR 0258](0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)).
|
||||||
|
2. **Adopt hits's word *item*.** Rejected for the same reason.
|
||||||
|
3. **Keep *issue*; call the module that holds them the *issue tracker*, named `issues`.** Adopted.
|
||||||
|
|
||||||
|
### Conditions (open question 5)
|
||||||
|
|
||||||
|
1. **The controller opens an issue by itself for every `healer-wanted` condition, from the start.** Rejected for now:
|
||||||
|
an automatic opener with no week of linked data behind it can flood the tracker with issues nobody reads.
|
||||||
|
2. **The controller only links issues to conditions until slice 4 has run for a week; then it opens one issue per
|
||||||
|
`healer-wanted` condition key by itself.** Adopted.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **The issue tracker is a Go module, `issues`, with one instance on the control-node.** It declares and claims one
|
||||||
|
seat held once for the mesh, `issue-tracker`
|
||||||
|
([ADR 0126](0126-a-module-declares-its-own-seats.md)). It is built on hits's model and carries none of its code.
|
||||||
|
2. **The tracker is the truth for every issue's number, status and links.** Every issue is opened in the tracker and
|
||||||
|
takes its number from it, including one that needs no document here. A design or governance issue also gets a
|
||||||
|
folder `04-ISSUES/NNN-slug/` under the tracker's number, whose narrative is reviewed in pull requests as today.
|
||||||
|
Status lives in one place.
|
||||||
|
3. **A number comes from one counter, advanced by compare-and-set, and only after the issue is valid.** A refused
|
||||||
|
open spends no number, and two callers opening at the same moment never get the same one. The runtime's state
|
||||||
|
verbs gain a write that is refused when the key changed since it was read; the counter does not rely on there
|
||||||
|
being one instance. The counter starts above the highest number on `main` and on every open branch at the moment of
|
||||||
|
the switch. A number never moves again, and numbers already skipped stay unused.
|
||||||
|
4. **State is in buckets the module declares** (ADR 0201): the controller creates them, the module never does.
|
||||||
|
**Every change is an event the module declares** ([ADR 0041](0041-events-are-a-relationship.md)), carrying what
|
||||||
|
changed and who changed it, so the events are the audit trail.
|
||||||
|
5. **The caller is the bus credential, never an argument.** The module records who acted from the identity the
|
||||||
|
runtime gives it with each call.
|
||||||
|
6. **The cycle's issue rules are enforced at the write.** A status outside the five is refused; `located` and
|
||||||
|
`resolved` need `located-in`; `resolved` needs `fixed-by`; a core issue resolves with `replay` or `replay-none`
|
||||||
|
([ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md));
|
||||||
|
`wontfix` needs a reason. `located-in` names an entry of a registered list of repositories and modules.
|
||||||
|
7. **The word is *issue*.** The module is the *issue tracker*, named `issues`; *ticketing system* is retired in the
|
||||||
|
glossary, and the bare word *ticket* is a homonym that never means an issue.
|
||||||
|
8. **It is built in slices, each ending at something the operator sees.** Slice 1 is the module with `open`, `get`,
|
||||||
|
`list`, `move`, `note` and `link`, and playbook 03 takes an issue's number from it. Slice 2 hands out decision and research numbers too. Slice 3 imports the issue
|
||||||
|
folders and consolidates them, after which one pull request removes the status keys from every report and changes
|
||||||
|
playbook 03 and `cycle.py` together. Slice 4 links conditions and pull requests. Slice 5 is a read-only view onto
|
||||||
|
the tracker's bucket. The design is [to-be 53](../03-DESIGN/01-to-be/53-the-issue-tracker.md); once slice 1 runs,
|
||||||
|
each slice after it is an issue in the tracker itself.
|
||||||
|
9. **Conditions are linked, not opened, until slice 4 has run for a week.** A condition is never closed by closing an
|
||||||
|
issue: a condition is cleared only by observation
|
||||||
|
([ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)).
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-10-10.** Two facts, found as slice 1 was built. **The module's name:** §1 and §7 name
|
||||||
|
> the module `issues`. The operator named it `mesh-issues` on 2026-10-10, as the mesh's own parts carry the `mesh-`
|
||||||
|
> prefix (`mesh-delivery`, `mesh-vault`). The seat stays `issue-tracker`, the role, as `operator-channel` is held by
|
||||||
|
> `messenger`; the thing is still the *issue tracker*, and its verbs are still called `issue-tracker.<verb>`.
|
||||||
|
> Wherever this record says the module `issues`, read `mesh-issues`; the state named `issues` keeps its name.
|
||||||
|
> **Compare-and-set:** §3 says the runtime's state verbs "gain" a write refused when the key changed. They had it
|
||||||
|
> already: `create` and `update` at a revision, with a lost write told by its own error code, came with
|
||||||
|
> [ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
|
||||||
|
> 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. (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
|
||||||
|
the mechanism now lives; until then both stand unchanged.
|
||||||
|
- An agent opening an issue needs the mesh to be reachable. A node that cannot reach the control-node cannot take a
|
||||||
|
number, and says so, rather than guessing one.
|
||||||
|
- The SDK's state interface gains a compare-and-set write, which every module keeping a counter can use; it is a
|
||||||
|
contract change under [ADR 0039](0039-what-the-sdk-holds-and-refuses.md), and every module should be rebuilt
|
||||||
|
against it.
|
||||||
|
- What got harder: one more module the mesh's own process depends on. hq's checks today run from a checkout alone;
|
||||||
|
from slice 2 on, writing a decision or an issue here needs the tracker.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Research 042](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md) and its
|
||||||
|
[findings](../01-RESEARCH/042-the-mesh-keeps-its-own-issues/01-findings.md).
|
||||||
|
- [To-be 53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md).
|
||||||
|
- hits, `github.com/impire-io/hits`, release v0.7.0: the model studied, not the code used.
|
||||||
|
- [Issue 155](../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md): the collision that
|
||||||
|
reached `main`.
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-10
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
supersedes-in-part:
|
||||||
|
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
|
||||||
|
- 02-DECISIONS/0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md
|
||||||
|
- 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
|
||||||
|
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||||
|
extends: 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 276. Merges are assembled in a rolling window, and each batch is delivered by one walk with one commit per repository
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Shortly after midnight on 2026-10-10 two pull requests of the catalogue were merged 18 seconds apart, the
|
||||||
|
claude-code change at 00:20:52 and the dunst change at 00:21:09
|
||||||
|
([issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md)).
|
||||||
|
The controller opened one walk for each merge, as [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||||
|
decided ("a merge produces a tiered plan"). The second walk superseded the first, as
|
||||||
|
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
|
||||||
|
decision 3 decided, and both were planned at the dunst commit, as the fix for
|
||||||
|
[issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md)
|
||||||
|
decided. mesh-delivery then tied the surviving walk to the dunst delivery, which the supersession had already
|
||||||
|
ended, and tied no walk to the claude-code delivery. Nothing gave the walk its word. The change waited 58
|
||||||
|
minutes, until the operator started the walk by hand with `plans go`.
|
||||||
|
|
||||||
|
The diagnosis found two parts that each behave as written and disagree on what a walk's commit means. That
|
||||||
|
is the narrow cause. The wide one is that the mesh plans one walk per merge, while merges arrive in bursts:
|
||||||
|
|
||||||
|
- **A burst is the normal case.** The operator merges a batch of reviewed pull requests at once, often
|
||||||
|
across repositories (a catalogue change with the controller change it needs). Every merge opened a walk,
|
||||||
|
and every walk after the first superseded the one before. Three supersession paths now exist (ADR 0218,
|
||||||
|
issue 254, issue 349), each with its own bookkeeping, and issue 362 is a fourth way for them to disagree.
|
||||||
|
- **A walk superseded mid-gate wastes the gate and confuses its record.** A first machine judged healthy
|
||||||
|
for a build that the next walk replaces a minute later proves nothing about the build that stays. On
|
||||||
|
2026-10-09 a controller merge landing while another walk was at its gate rolled everything back; the rule
|
||||||
|
"one controller rollout at a time" was kept since then by the person merging, not by the mesh.
|
||||||
|
- **ADR 0239 made a delivery "one commit … from commit to delivered" and walked it alone**, while its own
|
||||||
|
decision 4 delivers a delivery group across repositories as one unit. The walk the commit gets is
|
||||||
|
decided by when the controller heard its merge, which is not something the person merging controls.
|
||||||
|
|
||||||
|
On 2026-10-10 the operator decided the model, in their words:
|
||||||
|
|
||||||
|
> "a delivery doesn't really 'run from commit to delivered', we've already discussed that a delivery plan
|
||||||
|
> might start with a set of commits spanning multiple repo's, however, only 1 commit per repo is allowed
|
||||||
|
> … we assemble all commits that are coming in with a 1 minute rolling window, so if a new commit comes
|
||||||
|
> in, less then 1 or 2 minutes after the last one, the builder again waits 1 or 2 minutes, until no new
|
||||||
|
> commit came in, then it looks at the 'group of commits' and decides if they all fit into 1 delivery
|
||||||
|
> plan … This is a hard problem to tackle, but it will improve our build setup I believe."
|
||||||
|
|
||||||
|
> "the 'actively assembling plan' is very interesting, the controller can say 'Still waiting for 13
|
||||||
|
> seconds for new commits, these are the currently grouped commits, plan is yet to be calculated'."
|
||||||
|
|
||||||
|
On two commits of one repository in one window, the operator chose the later commit over splitting:
|
||||||
|
"i like your idea better, I just thought it would be more complex, but if it's reasonably doable, go for
|
||||||
|
it".
|
||||||
|
|
||||||
|
**Checked against GENESIS.** *Failure must be loud*: an assembling batch and a waiting walk each have a
|
||||||
|
bound and a condition. *The mesh owns the truth*: the window, the batch and the walk are kept by the
|
||||||
|
controller and shown by its `plans`, not inferred from timing by a person. Nothing here conflicts with
|
||||||
|
GENESIS.
|
||||||
|
|
||||||
|
**Words.** The glossary had no word for the merges one window collects, nor for the window. This record
|
||||||
|
adds **merge window** and **batch** to it (Change and delivery), with this record. *Group* is not used for
|
||||||
|
either, because **delivery group** already names deliveries that share a head branch name.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**What a walk answers.**
|
||||||
|
|
||||||
|
1. **One walk per merge, as today, with the bookkeeping repaired.** The walk names the merges it answers,
|
||||||
|
and mesh-delivery ties a superseding walk to every live delivery it contains. Rejected as the model,
|
||||||
|
kept as a repair (decision 8): it makes the 362 shape deliver, but every burst still opens and
|
||||||
|
supersedes walks, a walk can still be superseded at its gate, and a cross-repository change is still
|
||||||
|
walked as separate walks in whatever order the merges were heard.
|
||||||
|
2. **A rolling merge window, and one walk per batch** (chosen). Merges are collected while they keep
|
||||||
|
arriving; the batch they form is planned once, as one walk with at most one commit per repository.
|
||||||
|
3. **Every walk started by hand.** The operator named it as the alternative ("this does mean we need to
|
||||||
|
manually start all delivery plans, or, even better, …"). Rejected: a person in every delivery is the
|
||||||
|
wait ADR 0239 exists to remove, and a merge that passed its checks must reach its machines without a
|
||||||
|
person.
|
||||||
|
|
||||||
|
**Two commits of one repository in one batch.**
|
||||||
|
|
||||||
|
1. **Split into two walks in merge order** (the operator's first proposal). The second walk waits for the
|
||||||
|
first. Rejected: every machine running the module builds, is sent and is judged on the earlier commit,
|
||||||
|
and then again on the later one minutes after; the gate judges a build that is about to be replaced;
|
||||||
|
and the batch's walk stops being one unit.
|
||||||
|
2. **One walk with the later commit** (chosen). On a linear trunk the later merge commit contains the
|
||||||
|
earlier one, so building it delivers both. One build, one gate, one rollout. The earlier merge is
|
||||||
|
answered by its successor and ends `covered` when the walk is delivered. Only when that walk fails are
|
||||||
|
the earlier merges walked on their own, to find which one brought the failure.
|
||||||
|
|
||||||
|
**Who keeps the window.**
|
||||||
|
|
||||||
|
1. **mesh-delivery**, which owns deliveries (ADR 0239). Rejected: a walk on the controller's own path (the
|
||||||
|
controller, the node-engine, the node tools, the bus, mesh-delivery itself) must not wait for
|
||||||
|
mesh-delivery, and with no holder of the seat on record the controller plans alone. The window would
|
||||||
|
then exist twice, once in each.
|
||||||
|
2. **The controller** (chosen). It hears every merge (the bus, and the catch-up that reads back what the
|
||||||
|
bus missed), it owns the planner and the walk, and it already plans alone for its own path. mesh-delivery
|
||||||
|
keeps what it owns: each commit's delivery record, the order of a delivery group, and the word that lets
|
||||||
|
a waiting walk start.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **A rolling merge window.** The controller collects every merge into a branch a module follows into the
|
||||||
|
open **batch**. The window is rolling: each merge restarts it. It closes when no merge arrived for the
|
||||||
|
window's length, 90 seconds by default (the controller's setting `merge-window`), and at the latest 10
|
||||||
|
minutes after the batch's first merge (`merge-window-at-most`), so a busy period never starves delivery.
|
||||||
|
The batch, its merges and its times are kept in the controller's store, so a restarted controller
|
||||||
|
resumes the window where it stood rather than losing or doubling a merge.
|
||||||
|
2. **A batch is planned once, as one walk with at most one commit per repository.** When the window closes,
|
||||||
|
the planner computes one delivery plan for the batch: every module its commits move, their dependents,
|
||||||
|
in tiers along the module graph, across repositories. For a repository with two or more merges in the
|
||||||
|
batch the walk takes the latest commit of its branch, which contains the earlier ones. **Every merge
|
||||||
|
belongs to exactly one walk**: the walk names every merge it answers, its own commits and the earlier
|
||||||
|
ones they contain. A merge is never dropped and never walked twice.
|
||||||
|
3. **The earlier merge of a repository is answered by its successor.** Its delivery follows the walk and
|
||||||
|
ends `covered` when the walk is delivered, naming the merge that carried it (the glossary's word,
|
||||||
|
[ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md);
|
||||||
|
here the containment is the trunk's order inside one batch, so the forge is not asked). **When that walk
|
||||||
|
fails**, the walk's own commits fail, and each earlier merge it answered is walked on its own commit, the
|
||||||
|
newest first, one walk each, in the next batches: the first that is delivered ends `delivered`, and the
|
||||||
|
search stops there. A walk that failed never marks a merge delivered.
|
||||||
|
4. **One walk at a time, and nothing supersedes a walk that started.** A batch becomes a walk only when no
|
||||||
|
walk is open. Merges that arrive while a walk runs join the next batch, whatever its window says. A walk
|
||||||
|
that has not started, because it waits for mesh-delivery's word, is folded into the next batch when that
|
||||||
|
batch is planned, and the new walk answers every merge the folded one answered. This is the only
|
||||||
|
supersession left; ADR 0218 decision 3 and the issue 349 rule of planning a late merge at a later commit
|
||||||
|
are replaced by it. A merge heard after a later merge of its repository and branch was put in a walk is
|
||||||
|
answered by that walk: by the batch still assembling, by the open walk, or, when that walk was delivered,
|
||||||
|
it ends `covered` at once.
|
||||||
|
5. **The order of a delivery group holds inside a walk** (ADR 0249 unchanged). The pairs its order rules
|
||||||
|
give (declared, built by, version skew, engine before controller, each with the rule that won) become
|
||||||
|
edges of the walk's tiers beside the module graph: the modules of a member ordered after another are in
|
||||||
|
a later tier than that member's. A contradiction still rejects the group before anything merges.
|
||||||
|
mesh-delivery gives a walk its word only when every delivery it carries is at its turn.
|
||||||
|
6. **A delivery is one commit's record, and its walk is the batch's.** A delivery keeps its pull request,
|
||||||
|
its status, its note and its check, as ADR 0239 decided. From its merge on it is delivered by the walk
|
||||||
|
that answers it, and every delivery a walk carries ends with that walk's result: `delivered` (its own
|
||||||
|
commit) or `covered` (an earlier commit of the same repository) when the walk is done, `failed` when it
|
||||||
|
fails, `stopped` when it is stopped. The walk's machine steps are recorded on each delivery it carries.
|
||||||
|
7. **The assembling state is shown.** The controller's `plans` lists the open batch first, as the operator
|
||||||
|
asked: "assembling: 13 s left (at the latest 00:31:09); grouped: novox/mesh-catalog@48bda475 (answers
|
||||||
|
553b7191), novox/mesh-controller@a6bc0931; plan not yet calculated". It says the batch on the bus as
|
||||||
|
`plan-moved` with the state `assembling`, so mesh-delivery's `deliveries` and `show` say of a merged
|
||||||
|
delivery that its batch is assembling and how long is left, and of one whose batch waits that it waits
|
||||||
|
behind which walk.
|
||||||
|
8. **The bookkeeping is repaired as well** (the narrow cause of issue 362), because decision 4 keeps one
|
||||||
|
supersession path. A walk names every merge it answers, and a superseded walk names the walk that took it
|
||||||
|
over. mesh-delivery ties a walk to every live delivery whose merge it answers, follows a walk to the one
|
||||||
|
that took it over, and **never ties a walk to a delivery that has ended**. A walk waiting for the word
|
||||||
|
that no live delivery holds is either adopted as a delivery of its own or listed at once by `stalled`.
|
||||||
|
9. **Every wait has a bound and a condition.** A batch still assembling a minute past
|
||||||
|
`merge-window-at-most` while no walk is open raises a condition: the controller failed to cut it. A
|
||||||
|
batch waiting behind an open walk longer than that walk's bound raises one naming the walk. A walk
|
||||||
|
waiting for its word past 30 minutes is S16's, as before, and its words name the merges it answers. A
|
||||||
|
merged delivery no walk answers within ten minutes is held with the true reason (which open walks of its
|
||||||
|
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
|
||||||
|
takes minutes to build and longer to judge, so the wait is small beside it, and it removes the builds,
|
||||||
|
sends and gates that a superseded walk wasted.
|
||||||
|
- **A cross-repository change merged within a window is walked as one unit**, tiered by the graph and its
|
||||||
|
group's order. A person no longer has to merge in order and wait in between.
|
||||||
|
- **One walk at a time is mechanical.** A controller merge can no longer land in the middle of another
|
||||||
|
walk's gate; it waits for the next batch.
|
||||||
|
- **A failing walk fails every delivery it carried**, including one whose own modules passed before the
|
||||||
|
failure. Each delivery's machine steps say how far it got, and decision 3's search finds the culprit only
|
||||||
|
among the merges of one repository. A failure in a walk that carried several repositories is the
|
||||||
|
operator's to read. This is the price of one unit, and the operator chose it.
|
||||||
|
- **What got harder**: the controller's merge handling holds state across merges (the open batch) and is
|
||||||
|
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.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- The controller's tests: two merges of one repository 18 seconds apart make one batch and one walk at the
|
||||||
|
later commit, answering both; merges in three repositories within the window make one walk; a merge at
|
||||||
|
second 80 restarts the window; the window closes at `merge-window-at-most` however busy; a merge arriving
|
||||||
|
while a walk runs joins the next batch; a walk that has not started is folded into the next one; `plans`
|
||||||
|
shows the assembling state; the batch survives a restart; the two conditions of decision 9 are raised past
|
||||||
|
their bounds and not before.
|
||||||
|
- mesh-delivery's tests: both deliveries of one walk end `delivered` and `covered` with its done record; a
|
||||||
|
failed walk marks none delivered and walks the earlier merge alone; a walk is never tied to an ended
|
||||||
|
delivery; a walk is given its word only when every delivery it carries is at its turn; a group's order is
|
||||||
|
unchanged; a held delivery with no walk names the true reason and is listed by `stalled`.
|
||||||
|
- Each of those tests fails with its part of the change removed.
|
||||||
|
- mesh-lab's replay R362 (decision's consequence above), proved to fail before the change and pass after it.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md),
|
||||||
|
its diagnosis, and the replay R362 in mesh-lab.
|
||||||
|
- [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) (a merge produces a plan),
|
||||||
|
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
|
||||||
|
decision 3 (a newer merge takes over an older plan), [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)
|
||||||
|
decision 2 (one commit, one change plan), [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||||
|
(a delivery walked by its own commit's walk), each superseded in the part this record names.
|
||||||
|
- [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md),
|
||||||
|
whose order rules this record keeps and uses as tier edges.
|
||||||
|
- [ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md),
|
||||||
|
whose `covered` ends an earlier merge of a repository.
|
||||||
|
- [Issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md),
|
||||||
|
whose rule decision 4 replaces.
|
||||||
|
- To-be [47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md) and
|
||||||
|
[10](../03-DESIGN/01-to-be/10-delivery.md), amended with this record.
|
||||||
+260
@@ -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)
|
||||||
+214
@@ -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
|
||||||
@@ -223,8 +223,9 @@ The same path, with the mesh built in the middle of it.
|
|||||||
# 1 — raise the foundation and the controller from the carried bundle
|
# 1 — raise the foundation and the controller from the carried bundle
|
||||||
nox-mesh-host reconcile
|
nox-mesh-host reconcile
|
||||||
|
|
||||||
# 2 — the controller now exists, and issues the first token
|
# 2 — the controller now exists, and issues the first token (nox, the operator's command,
|
||||||
mesh-controller token issue
|
# at the controller's terminal: ADR 0272)
|
||||||
|
nox token issue
|
||||||
|
|
||||||
# 3 — the machine joins the mesh it just raised
|
# 3 — the machine joins the mesh it just raised
|
||||||
nox-mesh-host enrol --token <token>
|
nox-mesh-host enrol --token <token>
|
||||||
@@ -618,8 +619,8 @@ rather than left to be worked out.
|
|||||||
**A token is issued *for* a node record**, and that is where a re-enrolment is decided.
|
**A token is issued *for* a node record**, and that is where a re-enrolment is decided.
|
||||||
|
|
||||||
```
|
```
|
||||||
mesh-controller token issue --node workstation # this machine is that node again
|
nox token issue --node workstation # this machine is that node again
|
||||||
mesh-controller token issue --new # a machine the mesh has not seen
|
nox token issue --new # a machine the mesh has not seen
|
||||||
```
|
```
|
||||||
|
|
||||||
The node-engine does not need to know which it is. It presents a token and receives an identity; what
|
The node-engine does not need to know which it is. It presents a token and receives an identity; what
|
||||||
@@ -699,7 +700,7 @@ keeps cataloguing.
|
|||||||
|
|
||||||
### Where the enrolment token comes from
|
### Where the enrolment token comes from
|
||||||
|
|
||||||
**`mesh-controller token issue` prints it once**, to the person running it. Single-use, and it
|
**`nox token issue` prints it once**, to the person running it at the controller's terminal. Single-use, and it
|
||||||
expires whether used or not ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)).
|
expires whether used or not ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)).
|
||||||
|
|
||||||
It is carried by hand — read off a screen, pasted into a terminal. That is the design rather than
|
It is carried by hand — read off a screen, pasted into a terminal. That is the design rather than
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller internal/builder
|
- mesh-controller internal/builder
|
||||||
- mesh-controller cmd/mesh-controller (build, build --behind, push, status)
|
- mesh-controller cmd/mesh-controller (build, build --behind, push, status)
|
||||||
- mesh-controller internal/inventory/builds.go
|
- mesh-controller internal/inventory/builds.go
|
||||||
updated: 2026-10-06
|
updated: 2026-10-10
|
||||||
decisions:
|
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/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||||
- 02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
|
- 02-DECISIONS/0090-a-failure-that-repeats-is-said-to-be-stuck.md
|
||||||
- 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
- 02-DECISIONS/0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md
|
||||||
@@ -282,3 +283,9 @@ everything else while that module is held. A person can always deliver by hand.
|
|||||||
|
|
||||||
The controller keeps the comparison, the planner, the gate, sending and rollback. `mesh-delivery` keeps the
|
The controller keeps the comparison, the planner, the gate, sending and rollback. `mesh-delivery` keeps the
|
||||||
record and the order, and asks.
|
record and the order, and asks.
|
||||||
|
|
||||||
|
**Merges are walked in batches** (2026-10-10, by [ADR 0276](../../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)). The controller collects
|
||||||
|
merges in a rolling merge window, 90 seconds by default and at most 10 minutes, and plans each batch once, as
|
||||||
|
one walk with at most one commit per repository: the latest of its branch, which contains the earlier ones.
|
||||||
|
One walk is open at a time, and a walk that started is never superseded. `plans` shows a batch while it
|
||||||
|
assembles: what it holds and how long is left. To-be 47 says how a delivery follows its batch's walk.
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ code:
|
|||||||
- mesh-controller examples/postgres-provisioner
|
- mesh-controller examples/postgres-provisioner
|
||||||
- mesh-controller internal/inventory/given.go
|
- mesh-controller internal/inventory/given.go
|
||||||
- mesh-controller cmd/mesh-controller/given.go
|
- mesh-controller cmd/mesh-controller/given.go
|
||||||
updated: 2026-10-06
|
updated: 2026-10-10
|
||||||
decisions:
|
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/0001-mesh-brokers-nodes-host-agents-think.md
|
||||||
- 02-DECISIONS/0009-modules-and-the-graph.md
|
- 02-DECISIONS/0009-modules-and-the-graph.md
|
||||||
- 02-DECISIONS/0085-a-secret-is-a-provision.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)).
|
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
|
*How it is checked:* the tests ADR 0228 names, and a live `rotate` of a given at-start secret through
|
||||||
the mesh MCP server.
|
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,8 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-catalog modules/claude-code]
|
code: [mesh-catalog modules/claude-code]
|
||||||
updated: 2026-10-09
|
updated: 2026-10-10
|
||||||
decisions:
|
decisions:
|
||||||
|
- 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
|
||||||
|
- 02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md
|
||||||
- 02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md
|
- 02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md
|
||||||
- 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md
|
- 02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md
|
||||||
- 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
|
- 02-DECISIONS/0245-a-verb-says-what-it-replaces-and-the-agent-is-guarded-from-working-round-the-mesh.md
|
||||||
@@ -63,12 +65,21 @@ instruction file and the manager's tools:
|
|||||||
|
|
||||||
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
**The home.** Under [ADR 0182](../../02-DECISIONS/0182-inside-a-home-the-mesh-owns-what-it-places-and-holds-the-rest-as-found.md)
|
||||||
the module owns the directory `~/.claude` — that it exists, that the operator owns it, its mode,
|
the module owns the directory `~/.claude` — that it exists, that the operator owns it, its mode,
|
||||||
`0700` — and declares it, so the mesh refuses a second module owning it. Of what is inside, it owns only
|
`0700` — and declares it, so the mesh refuses a second module owning it.
|
||||||
the agent's credentials file, which its own code writes for a subscription licence (§5); every other path
|
|
||||||
is *found*. The person's memory, history, projects, local
|
> **Revised 2026-10-09 by [ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md).**
|
||||||
settings, their own rules, skills and tool servers are never read or written by the mesh. **The six
|
> This paragraph said the module owns only the credentials file inside the directory and holds every other
|
||||||
predecessor files are the operator's to remove, once, on each workstation**; the module's documentation
|
> path as found, the person's own rules and skills included. Now **every configuration file under the
|
||||||
lists them, and until they go the agent reads stale instructions beside the mesh's.
|
> directory is the module's** — the home instruction file, `rules/`, `skills/`, `commands/`, `agents/`,
|
||||||
|
> `hooks/` and `settings.json` — rendered from registrations and never written by hand; and **the program's
|
||||||
|
> working state is excluded by name** and stays found: the credentials file (written by the module's own
|
||||||
|
> code, §5), the history, the sessions, the projects and their memory, the caches, the program's own plugin
|
||||||
|
> records, the vendor's synced skills, and the local settings file the program writes with a person's
|
||||||
|
> permission answers. §10 says how, and what a hand edit becomes. The six predecessor files are no longer
|
||||||
|
> the operator's to remove by hand: a file nobody rendered is a condition, and the operator imports or
|
||||||
|
> removes it through the module's tools.
|
||||||
|
|
||||||
|
The person's memory, history and projects are never read or written by the mesh.
|
||||||
|
|
||||||
## 2. What the module declares and what its code writes
|
## 2. What the module declares and what its code writes
|
||||||
|
|
||||||
@@ -244,28 +255,37 @@ machine. A machine that joins later takes it at its first start.
|
|||||||
The managed settings name the marketplace and enable the plugin. Those two keys are the mesh's, laid
|
The managed settings name the marketplace and enable the plugin. Those two keys are the mesh's, laid
|
||||||
last with the attribution key, and no setting replaces them.
|
last with the attribution key, and no setting replaces them.
|
||||||
|
|
||||||
**Three scopes.** Every registration names one:
|
**Three scopes, and a fourth since 2026-10-09.** Every registration names one:
|
||||||
|
|
||||||
- **mesh:** every machine running the agent;
|
- **mesh:** every machine running the agent;
|
||||||
- **node:** one machine or a list of them, rendered into the same plugin and files there only;
|
- **node:** one machine or a list of them, rendered into the same plugin and files there only;
|
||||||
- **home:** the operator account's own agent directory on one machine, where the item sits as if
|
- **home:** the agent account's own agent directory on one machine, where the item sits as if
|
||||||
written there by hand.
|
written there by hand. Since [issue 340](../../04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md)
|
||||||
|
it is taken only for the node serving the call, and only where that home is an agent account's own
|
||||||
|
(§9), never the operator's;
|
||||||
|
- **operator** (ADR 0273, §10): the operator's own items, rendered into the agent's home on every node that
|
||||||
|
names an operator account — the operator's own home where the agent runs as the operator, the agent
|
||||||
|
account's home where the node names one.
|
||||||
|
|
||||||
Settings take the first two scopes only. In the managed settings file they are laid in this order:
|
Settings take the mesh and node scopes into the managed settings file, laid in this order: the operator's
|
||||||
the operator's `managed_settings` setting (§2), then the mesh scope, then the node scope, then the
|
`managed_settings` setting (§2), then the mesh scope, then the node scope, then the mesh's own keys. The
|
||||||
mesh's own keys.
|
operator's own settings take the operator scope, into the home's `settings.json` (§10). **Since issue 340,
|
||||||
|
hooks, permission rules, tool servers and every kind of shared text for the mesh or a node are set at the
|
||||||
|
controller's terminal alone**, in the module's settings; the tools refuse them and say so.
|
||||||
|
|
||||||
**Instructions follow the scopes.** The managed instruction file holds the mesh's piece, then the node's
|
**Instructions follow the scopes.** The managed instruction file holds the mesh's piece, then the node's
|
||||||
piece: its role, and the sections registered for it. Further customisation per machine is a rule file
|
piece: its role, and the sections registered for it. Further customisation per machine is a rule file
|
||||||
placed at the home scope. The agent concatenates these and does not override, so the status tool names
|
placed at the home scope. The agent concatenates these and does not override, so the status tool names
|
||||||
a section that contradicts another, or that calls a tool the mesh no longer serves.
|
a section that contradicts another, or that calls a tool the mesh no longer serves.
|
||||||
|
|
||||||
**The home scope owns only what it placed.** The module records each home path it placed and touches
|
**The home is the module's** (ADR 0273, replacing "the home scope owns only what it placed"). The module
|
||||||
only those (ADR 0182). It refuses to register a name the person already uses there.
|
renders every configuration file under the agent's home from the registrations that apply there, at the
|
||||||
|
operator and home scopes, and records what it rendered. A name registered twice is refused: an operator-scope
|
||||||
|
item is rendered first, and a home-scope item may not take its name.
|
||||||
|
|
||||||
The one exception is a removal the person asks for ([ADR 0243](../../02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md)).
|
A removal the person asks for ([ADR 0243](../../02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md))
|
||||||
On the person's word, with a reason, the module removes an item it did not place. It keeps a checked copy in its own
|
is now the operator's answer to a file nobody rendered (§10). On the person's word, with a reason, the
|
||||||
state and logs the removal, and can put the item back.
|
module removes it, keeps a checked copy in its own state, logs the removal, and can put the item back.
|
||||||
|
|
||||||
**The tools.**
|
**The tools.**
|
||||||
|
|
||||||
@@ -281,7 +301,7 @@ An item and its files are one value in the state, refused above 256 KiB.
|
|||||||
|
|
||||||
**Changing the agent's own settings is the operator's act.** The vendor refuses an agent that loosens its
|
**Changing the agent's own settings is the operator's act.** The vendor refuses an agent that loosens its
|
||||||
own settings, and the module does not route around that refusal. A settings tool is called on the
|
own settings, and the module does not route around that refusal. A settings tool is called on the
|
||||||
operator's word.
|
operator's word — and, for the operator's own home, only at the terminal or on a warrant (§10).
|
||||||
|
|
||||||
## 9. The account the agent runs as
|
## 9. The account the agent runs as
|
||||||
|
|
||||||
@@ -363,6 +383,143 @@ terminal. The next send names the operator account again: the guard lets the ope
|
|||||||
and the licence is fetched into the operator's home. The agent account is kept (the node-engine never deletes
|
and the licence is fetched into the operator's home. The agent account is kept (the node-engine never deletes
|
||||||
a login).
|
a login).
|
||||||
|
|
||||||
|
## 10. The home is the module's: personal rules, and the lessons an agent proposes
|
||||||
|
|
||||||
|
*Added 2026-10-09 by [ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md).*
|
||||||
|
|
||||||
|
**What the module renders under the agent's home**, on every node, as the account the agent runs as (through
|
||||||
|
the helper of §9 where that is an agent account):
|
||||||
|
|
||||||
|
| path | rendered from |
|
||||||
|
|---|---|
|
||||||
|
| the home instruction file | the index of the operator's personal rules: each rule's name, where it came from (the terminal, or the lesson it was accepted from) and when; a first line saying the file is the module's and names the tools that change it |
|
||||||
|
| `rules/<name>.md` | one personal rule each |
|
||||||
|
| `skills/<name>/`, `commands/<name>.md`, `agents/<name>.md` | the operator's own skills, commands and subagents at the operator scope, under their own names; and the home-scope items where the home is an agent account's own |
|
||||||
|
| `hooks/` | nothing: a hook is the terminal's, in the managed settings. The directory is kept empty |
|
||||||
|
| `output-styles/<name>.md`, `keybindings.json` | the operator's output styles and keybindings at the operator scope |
|
||||||
|
| `settings.json` | the operator's registered settings, preference keys only, with the keys the program itself writes on a person's choice (the model, the spinner, the drafts, the other preferences the vendor's own commands set) taken over into the registration when the program changes them, so a choice made in one session follows the operator. A key that decides what runs (`hooks`, `permissions`, `mcpServers`, `env`, `apiKeyHelper`, `statusLine`, `autoMode`, any key not on the list) is never taken over: changed on disk, it is written back on the look that finds it. `autoMode` is a permission rule, so it lives in the managed settings at the terminal |
|
||||||
|
|
||||||
|
"Completely empty", the operator's words, means: nothing in the home was put there by hand. What is not the
|
||||||
|
module's rendering is the program's own working state, excluded by name in the module's documentation and
|
||||||
|
never written or removed: the credentials file (§5), the history, the sessions and their environments, the
|
||||||
|
projects and their memory, `todos/`, `plans/`, `statsig/`, the shell snapshots, the file history, the debug,
|
||||||
|
cache, paste and telemetry directories, the statistics, the daemon's files (`daemon-auth-cooldown` among
|
||||||
|
them), the dot-markers (`.last-cleanup`), `skills/synced/`, and the backups, worktrees, jobs, feedback, state
|
||||||
|
and IDE directories. **Two are the program's and watched**: `settings.local.json` (the permission answers a
|
||||||
|
person gives) and `plugins/` (marketplaces and installed plugins). **Named as neither**: `.hal-binding.json`,
|
||||||
|
a predecessor's leftover, and every copy of the credentials file (`.credentials.json` followed by anything,
|
||||||
|
such as `.credentials.json.testbak`): reported, and removed on the operator's word, no copy kept because it
|
||||||
|
holds a token. **Any other entry is a condition** until the operator removes it or the module names it.
|
||||||
|
|
||||||
|
**Who may register into the operator's home.** A registration at the operator scope, or at the home scope
|
||||||
|
into a home that is the operator's own, is taken by two routes and refused through any other:
|
||||||
|
|
||||||
|
1. **at the controller's terminal** ([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)):
|
||||||
|
the module's settings `personal_rules`, `personal_skills`, `personal_commands`, `personal_agents`,
|
||||||
|
`personal_output_styles`, `personal_keybindings`, `personal_settings`, `lessons_accepted` and
|
||||||
|
`lessons_declined`, set with `nox settings set claude-code …`. The module's whole layer is already the
|
||||||
|
terminal's (issue 340);
|
||||||
|
2. **on a warrant** ([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)):
|
||||||
|
the module uses the `operator-channel` seat as an asker. A register, import or remove tool called for the
|
||||||
|
operator's home acts on nothing: it publishes an approve ask showing the item in full, its digest binding
|
||||||
|
the text, name, kind and scope, and acts once on the warrant for that ask.
|
||||||
|
|
||||||
|
**The tools stay the operator's way in.** While no channel answers approve for the module, such a tool
|
||||||
|
records the request as **pending** in the module's state and answers with the exact line to type,
|
||||||
|
`nox settings set claude-code '<json>'`, the request merged into the current layer so that nothing else
|
||||||
|
changes. The status tool lists pending requests; one is dropped once the layer holds it, or after 7 days.
|
||||||
|
|
||||||
|
Nothing that runs — a hook, a permission rule (`autoMode` included), a tool server, an environment or
|
||||||
|
key-helper setting, a skill or command holding a shell line or the front matter issue 340 refuses — is taken
|
||||||
|
at the operator scope by either route. Issue 340's protection stands: a tool call without a warrant writes
|
||||||
|
nothing into the operator's home. A home-scope name never shadows an operator-scope item of the same kind.
|
||||||
|
|
||||||
|
**A hand edit is a condition, undone on the look that finds it.** At least once a minute, and at once on a
|
||||||
|
change on disk, the module compares the rendered paths with the disk and reads the two watched entries. Any
|
||||||
|
difference is said in its health as a finding on that node, and the controller raises **`agent-home-drift`**,
|
||||||
|
a warning that needs the operator, naming each path. That look acts:
|
||||||
|
|
||||||
|
- a rendered file changed by hand (`settings.json` above all) is written back, the hand version kept in
|
||||||
|
`home-copies` and shown by the status tool; only a preference key the program changed is taken over;
|
||||||
|
- a rendered file missing is rendered again;
|
||||||
|
- a file nobody rendered under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/` or `output-styles/` is,
|
||||||
|
**before the node's first adoption** (the imports below, recorded once per node), left and reported for the
|
||||||
|
operator to import or remove (ADR 0243); **after it**, moved into `home-copies`, restorable, and raised;
|
||||||
|
- an entry under neither list, a key other than `permissions` in `settings.local.json`, and a marketplace or
|
||||||
|
plugin in `plugins/` other than the vendor's and `nox-mesh` are raised, naming each, and left.
|
||||||
|
|
||||||
|
The condition clears on the first clean look; the first-node gate never reads it as a fault of a build
|
||||||
|
([ADR 0254](../../02-DECISIONS/0254-a-wait-for-a-persons-new-login-is-not-a-gates-failure-and-each-module-of-a-send-keeps-its-own-pass.md)'s
|
||||||
|
exemption). **The guard** (§3) refuses, naming the module's tool and saying a rule the agent wants is a
|
||||||
|
lesson: any shell command or file edit that writes, appends to, copies or moves into, edits in place or
|
||||||
|
removes a rendered path, `settings.local.json` or a `.json` file under `plugins/`; and `claude plugin
|
||||||
|
install`, `claude plugin marketplace add` and their in-session forms. It does not watch the rest of the
|
||||||
|
working state. The operator's override (ADR 0245 §5) applies.
|
||||||
|
|
||||||
|
**The first registrations** are imports, the node's first adoption: the operator's two rule files on the
|
||||||
|
laptop as the first personal rules; the laptop's skills and the workstation's subagent imported or removed on
|
||||||
|
the operator's word; the preference keys of the laptop's `settings.json` as the operator's settings, and its
|
||||||
|
`autoMode` key moved to the managed settings at the terminal; `.hal-binding.json` and the credentials copy
|
||||||
|
removed on the operator's word; the retired `cleanup` skill and the three stale instruction files found on
|
||||||
|
four nodes on 2026-10-09 removed with copies kept. Each is a condition until imported or removed.
|
||||||
|
|
||||||
|
**Lessons.** The module keeps a `lessons` register in its state: per lesson, when and on which node it was
|
||||||
|
proposed, the session where known, the **trigger** (the operator's words), the **cause** (what the agent did),
|
||||||
|
the **rule** (a personal rule in the operator's second person), an optional **before** (a tool name, command
|
||||||
|
prefix or path pattern), and its state — `proposed`, `accepted` with who, how and when, or `declined` with the
|
||||||
|
reason. The mesh's instruction section tells every agent to record a lesson, in the same turn, when the
|
||||||
|
operator expresses frustration or corrects a way of working, after first working out why, and to say in one
|
||||||
|
line that it did. A correction goes to the register, not to the agent's own per-project memory, which stays its
|
||||||
|
notebook for facts about a project. **The first lessons are the agent's existing corrections**: once the
|
||||||
|
register exists on a node, the agent there proposes, unasked, every note in its own memory that records a
|
||||||
|
correction the operator gave, and says so in one line.
|
||||||
|
|
||||||
|
- **A proposed lesson is not a rule.** It is rendered nowhere. The module shows it to the operator for a yes:
|
||||||
|
an approve ask with Accept and Decline, or, while no channel answers approve, the list tool shows it and the
|
||||||
|
operator accepts or declines at the terminal (`lessons_accepted`, `lessons_declined`). A proposal expires
|
||||||
|
after 30 days, declined with that reason.
|
||||||
|
- **An accepted lesson is a personal rule**, registered at the operator scope with the lesson as its source,
|
||||||
|
rendered into every agent's home, and named in the home instruction file as accepted from that lesson.
|
||||||
|
- **A declined lesson stays**, with its reason; a new proposal whose rule matches a declined one is refused
|
||||||
|
with that reason, and the agent is told to raise it with the operator instead.
|
||||||
|
- **The agent is reminded.** The plugin's session-start hook (the module's own binary, beside the guard)
|
||||||
|
prints at session start the rules accepted from lessons in the last 30 days and those whose `before`
|
||||||
|
matches the working directory or repository. Before a tool runs, the guard prints the rule of every
|
||||||
|
accepted lesson whose `before` matches the tool, command or path — a reminder, once per session per rule,
|
||||||
|
never a refusal.
|
||||||
|
- **The register is readable by any agent on any node** through the list tool, each state marked, so a
|
||||||
|
proposal is never read as a rule. Recording a proposal needs no yes.
|
||||||
|
|
||||||
|
**The tools of this section**: `claude_code_personal_rule_register`, `_list`, `_unregister`;
|
||||||
|
`claude_code_personal_settings_get`, `_set`; the existing register tools with `scope: operator` for skills,
|
||||||
|
commands and subagents; `claude_code_config_import` taken again, at the operator scope, by the two routes;
|
||||||
|
`claude_code_config_status` extended with the drift findings, the unknown entries, the watched entries
|
||||||
|
and the pending requests;
|
||||||
|
`claude_code_lesson_register`, `_list`, `_accept`, `_decline`. Every tool that writes into the operator's
|
||||||
|
home goes through the two routes above.
|
||||||
|
|
||||||
|
## 11. Shared instruction sections an agent proposes and the operator approves
|
||||||
|
|
||||||
|
*Added 2026-10-10 by [ADR 0274](../../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).*
|
||||||
|
|
||||||
|
An instruction section for every node, or for some, has two ways in: the controller's terminal (issue 340), and **a
|
||||||
|
proposal an agent makes and the operator approves** on a channel that proves who answered. A hand-over, a rule
|
||||||
|
learned, the state of a piece of work: each is a named section, read first in every session it reaches.
|
||||||
|
|
||||||
|
| Step | What happens |
|
||||||
|
|---|---|
|
||||||
|
| propose | `claude_code_instructions_propose` (scope `mesh`, or `node` and the nodes; a name; the text, or `remove`) refuses what issue 340 refuses (any `@` after a non-letter included), what a reader cannot see, headings of level one to three, a secret's shape, more than 16 KiB, a name the terminal sets, and at once when no channel proves its sender; keeps the proposal in the `proposals` state; publishes one ask under the module's name on `operator-channel` |
|
||||||
|
| ask | composed from the proposal alone: "Add instructions '<name>' to every Claude session on the mesh?", the text whole when short and sayable, else its first lines, its size and fingerprint and the terminal line that shows all of it; Approve and Decline both at the level approve, each binding the proposal's act; a day to answer |
|
||||||
|
| prove | every node reads the router's own record of the ask under the module's name, composes the ask again, judges the record as the module would have made it, holds the warrant to it (the ask's digest, the option's bound act, the text's digest, in time), and only then writes the section into its own `approved.json`; decided once per node, oldest warrant first, never older than the section's last applied change |
|
||||||
|
| render | the managed instruction file gains "Instructions the operator approved", after the terminal's sections, from the node's own file, never from the bus |
|
||||||
|
| remove | a proposal with `remove`, approved the same way; a deleted bus record removes nothing |
|
||||||
|
| record | each node that decided an answer asks the controller's `warranted`, which reads the router's record and writes the hand-act log once per ask |
|
||||||
|
| list | `claude_code_proposals`: every proposal, where it stands on this node, and with an id the whole text and digest |
|
||||||
|
|
||||||
|
A node away for more than the 30 days the router keeps its record, or joining later, cannot prove an earlier
|
||||||
|
approval and does not render it; the list says so. The module uses the `operator-channel` seat, which grants it the
|
||||||
|
asker's subjects and its own records there.
|
||||||
|
|
||||||
## How it is checked
|
## How it is checked
|
||||||
|
|
||||||
| Check | Defends |
|
| Check | Defends |
|
||||||
@@ -373,8 +530,21 @@ a login).
|
|||||||
| a switch asked of the seat through the mesh MCP server changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
|
| a switch asked of the seat through the mesh MCP server changes the licence and the token on the node; no tool answer and no log line holds a token | ADR 0183 |
|
||||||
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
|
| the API-key binding writes nothing under the home and the agent authenticates through the helper | ADR 0183 |
|
||||||
| the module's test: keys set in `managed_settings` (an auto-mode allow list, a permissions list) appear in the rendered managed settings file, a setting naming the attribution, the connectors key or a key-helper is overridden, and a key-helper appears only for an API-key binding | ADR 0213 |
|
| the module's test: keys set in `managed_settings` (an auto-mode allow list, a permissions list) appear in the rendered managed settings file, a setting naming the attribution, the connectors key or a key-helper is overridden, and a key-helper appears only for an API-key binding | ADR 0213 |
|
||||||
| the module's render test: a registered skill, subagent, command, hook and output style land in the `nox-mesh` plugin; a tool server, a setting and an instruction section in their managed files; for one machine of two, a mesh item on both, a node item on one, a home item only in that home; a setting naming the marketplace keys is overridden | ADR 0216 |
|
| the module's render test: a registered skill, subagent, command and output style land in the `nox-mesh` plugin (a hook and a setting are refused since issue 340, and the terminal's land from the module's settings); a tool server and an instruction section in their managed files; for one machine of two, a mesh item on both, a node item on one, a home item only in that home; a setting naming the marketplace keys is overridden | ADR 0216, issue 340 |
|
||||||
| the module's test: a home name the person already uses is refused, unregistering removes only the placed path, and an item above 256 KiB is refused | ADR 0216, ADR 0182 |
|
| the module's test: a name registered twice is refused, unregistering removes only the rendered path, and an item above 256 KiB is refused | ADR 0216, ADR 0182 |
|
||||||
|
| the module's render test over a seeded home: every configuration path of §10 matches the registrations byte for byte, every excluded entry is byte-identical before and after, an unknown entry is reported; a program-written key of `settings.json` is taken over into the operator's settings, a key outside the list is drift | ADR 0273 §1 |
|
||||||
|
| the module's render test for one machine of three: a personal rule lands under `rules/` and in the home instruction file in the operator's own home on two and in the agent account's home on the third; a hook, a permission rule, a tool server and an item holding a shell line are refused at the operator scope | ADR 0273 §2 |
|
||||||
|
| the module's tests through its real tool dispatch: a register, import or remove at the operator scope, or for an operator's home, publishes an ask and writes nothing; with a warrant for that ask it acts once; another ask's warrant, or a replayed one, is refused; while no channel answers approve the ask is refused naming the terminal's line | ADR 0273 §3, issue 340 |
|
||||||
|
| the module's tests with a clock: a rendered file changed by hand (`settings.json` among them) is written back on the look that finds it, within a minute, its hand version kept and shown; a `hooks`, `permissions`, tool-server or `autoMode` key added on disk is written back and never taken over; a missing file is rendered again on that look; before a node's first adoption an unrendered file under `rules/`, `skills/`, `commands/`, `agents/`, `hooks/`, `output-styles/` is left and reported, after it the same file is moved into `home-copies`, restorable, and raised; an entry under neither list, `.hal-binding.json` and a `.credentials.json.*` copy are raised and left; a key other than `permissions` in `settings.local.json` and a marketplace or plugin other than the vendor's and `nox-mesh` are raised; the condition clears on a clean look; the controller's test that `agent-home-drift` is a warning the first-node gate never reads as a fault | ADR 0273 §1, §4 |
|
||||||
|
| the module's guard tests: a write, append, copy, move, in-place edit and removal under each rendered path, and aimed at `settings.local.json` and a `.json` under `plugins/`, refused naming the tool; `claude plugin install`, `claude plugin marketplace add` and their in-session forms refused; a write under `projects/` passes | ADR 0273 §4 |
|
||||||
|
| the module's test: a home-scope registration of a name and kind already registered at the operator scope is refused naming it, and the render holds the operator's item | ADR 0273 §3 |
|
||||||
|
| the module's test: a tool called for the operator's home writes nothing there, records one pending request and answers a `nox settings set claude-code '…'` line that, applied to the current layer, adds that item and changes no other key; the request is dropped once the layer holds it, and after 7 days | ADR 0273 §3 |
|
||||||
|
| the module's tests over the lessons register: register, list, accept, decline, expiry; only accepted lessons reach `rules/`; a proposal matching a declined rule is refused with the reason; the session-start hook prints the recent and matching rules; the guard prints a matching rule once per session and refuses nothing for it | ADR 0273 §5 |
|
||||||
|
| the module's render test: the rendered managed instruction file's `working-on-the-mesh` section holds the paragraph on when a lesson is recorded, that a correction goes to the register and not to the agent's memory, and that existing corrections are proposed first; the test fails when the paragraph is taken out | ADR 0273 §5 |
|
||||||
|
| live: on the laptop, `~/.claude` holds the rendered configuration and the working state and nothing else; a rule written by hand into `rules/` raises `agent-home-drift` within a minute; a rule set at the terminal is in a new session on every node; a correction given in a session is in the lessons list in that turn, accepted at the terminal, and named at the start of the next session on another node | ADR 0273, the exit of WP7–WP9 |
|
||||||
|
| the module's tests through its real tool dispatch and with a router's record: a proposal asks at the level approve and changes nothing; an approval proved from the router's record renders on another node; a decline, an expiry, a refusal, a changed text, another ask's words, a late answer, a wrong level or another ask's warrant render nothing; a warrant acts once; a removal needs approval and a deleted record removes nothing; a node scope reaches its nodes only; no verified-sender channel refuses at once; issue 340's refusals hold; the text is never logged | ADR 0274 |
|
||||||
|
| the controller's tests: `warranted` records who chose from the router's record, once per ask, and refuses an open ask, one without a choice, and another asker's | ADR 0274 §6 |
|
||||||
|
| live: a proposal for every node shows on the operator's phone with Approve and Decline; after Approve a new session on another node reads it | ADR 0274 |
|
||||||
| live: a skill registered at the mesh scope is offered as `nox-mesh:<name>` in a new session on each machine | ADR 0216 |
|
| live: a skill registered at the mesh scope is offered as `nox-mesh:<name>` in a new session on each machine | ADR 0216 |
|
||||||
| the module's test: the "instead of" table is generated from the controller's answers, ordered and cut; the guard refuses ssh to a mesh machine by name, domain, internal name, address, alias and jump, and the local work-arounds for a mesh name, naming the tool — and lets git to the forge, ssh beyond the mesh and a command merely mentioning ssh through; the override is honoured only from the session's start and only when recorded | ADR 0245 |
|
| the module's test: the "instead of" table is generated from the controller's answers, ordered and cut; the guard refuses ssh to a mesh machine by name, domain, internal name, address, alias and jump, and the local work-arounds for a mesh name, naming the tool — and lets git to the forge, ssh beyond the mesh and a command merely mentioning ssh through; the override is honoured only from the session's start and only when recorded | ADR 0245 |
|
||||||
| the mesh MCP server's provision resolves by co-location; a machine without the mesh MCP server refuses the module by name | ADR 0027, ADR 0152 |
|
| the mesh MCP server's provision resolves by co-location; a machine without the mesh MCP server refuses the module by name | ADR 0027, ADR 0152 |
|
||||||
@@ -392,6 +562,9 @@ a login).
|
|||||||
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
|
directory in it, bound to its own licence through the manager (to-be 39 §5); that is for when workers
|
||||||
exist, and nothing here changes for it.
|
exist, and nothing here changes for it.
|
||||||
- **The package repository seat** (§7).
|
- **The package repository seat** (§7).
|
||||||
|
- **A per-project lesson, lessons for an agent the mesh runs, and a surface of the operator's own** that can
|
||||||
|
tell the operator from an agent, which would be a third route into the operator's home (ADR 0273). For the mesh's
|
||||||
|
shared instruction sections, the operator channel is that surface since ADR 0274 (§11).
|
||||||
- **How the module's tools are run** is decided: the node's tool runner, host-side
|
- **How the module's tools are run** is decided: the node's tool runner, host-side
|
||||||
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
([ADR 0175](../../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)).
|
||||||
The managed files and the credential write are tools of this module that runtime serves. Until the
|
The managed files and the credential write are tools of this module that runtime serves. Until the
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ resolver is asked by the second graphical module; the display server itself is g
|
|||||||
|
|
||||||
## 6. The order of the build
|
## 6. The order of the build
|
||||||
|
|
||||||
1. **The operator account on every node** — `mesh-controller node` with the login name; empty on
|
1. **The operator account on every node** — `nox node account <node> <login>` at the controller's terminal; empty on
|
||||||
all four today. Nothing home-scoped composes before it.
|
all four today. Nothing home-scoped composes before it.
|
||||||
2. **The tool runner** — `mesh-host` supervises it; mesh-tools serves bundles from memberships
|
2. **The tool runner** — `mesh-host` supervises it; mesh-tools serves bundles from memberships
|
||||||
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
|
and reloads; mesh-controller composes the bundle into the declaration and the memberships to one
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: in-progress
|
||||||
code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager]
|
code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager]
|
||||||
updated: 2026-10-04
|
updated: 2026-10-09
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md
|
||||||
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
- 02-DECISIONS/0206-a-node-reports-the-anthropic-grant-it-holds-and-the-licence-manager-adopts-a-licence-by-refreshing-it.md
|
||||||
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
- 02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md
|
||||||
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
- 02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md
|
||||||
@@ -194,6 +195,131 @@ designs 36 and 39 set to `implemented` with the as-is written
|
|||||||
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue
|
**Proof.** `claude_code_status` answers on every node; the refusal's notification arrived; the catalogue
|
||||||
has no module built on the old placement.
|
has no module built on the old placement.
|
||||||
|
|
||||||
|
## The home, personal rules and lessons — WP7 to WP10
|
||||||
|
|
||||||
|
*Added 2026-10-09 by [ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md);
|
||||||
|
design 36 §10 is the authority on what is built.* WP0–WP6 ran and stay as recorded above; these four packages
|
||||||
|
open the document again. All of it is in `mesh-catalog`, in the `claude-code` module, except where a line
|
||||||
|
says otherwise. The controller needs nothing new: the module's settings layer is already the terminal's
|
||||||
|
(issue 340), and the asker's client is the shared library's (ADR 0259 §10 step 3).
|
||||||
|
|
||||||
|
```
|
||||||
|
WP7 the home is the module's: render, drift, guard (mesh-catalog) ── two days
|
||||||
|
WP8 personal rules and the operator's items, live (the live mesh) ── half a day, the operator's
|
||||||
|
WP9 lessons: register, ask, reminders, the instruction section (mesh-catalog) ── two days
|
||||||
|
WP10 the rest of the nodes, and the as-is (the live mesh) ── half a day
|
||||||
|
```
|
||||||
|
|
||||||
|
### WP7 — The home is the module's
|
||||||
|
|
||||||
|
*mesh-catalog. Two days.*
|
||||||
|
|
||||||
|
1. **The manifest.** The module `uses` the `operator-channel` seat, as an asker. New settings, each a list
|
||||||
|
of items whose text is a list of lines (a setting holds no line break, issue 339): `personal_rules`,
|
||||||
|
`personal_skills`, `personal_commands`, `personal_agents`, `personal_output_styles`,
|
||||||
|
`personal_keybindings`, `personal_settings` (preference keys only; `autoMode`, `hooks`, `permissions`,
|
||||||
|
tool-server, `env`, `apiKeyHelper` and `statusLine` refused), `lessons_accepted`, `lessons_declined`. New
|
||||||
|
state buckets: `personal` (the operator-scope registrations and the lessons accepted into them), `lessons`
|
||||||
|
(WP9), `home-copies` (the hand versions, the files moved out after adoption, and the removal copies ADR 0243
|
||||||
|
already keeps; never a credentials copy), `pending` (requests waiting for the terminal), and a per-node
|
||||||
|
record of the first adoption. The documentation gains the lists of design 36 §10: the rendered paths, the
|
||||||
|
excluded working state, the two watched entries, and the entries named as neither.
|
||||||
|
2. **The renderer.** From the operator-scope registrations and the terminal's settings, the home instruction
|
||||||
|
file, `rules/`, `skills/`, `commands/`, `agents/`, `output-styles/`, an empty `hooks/`, `keybindings.json`
|
||||||
|
and `settings.json`, written as the
|
||||||
|
agent's account (through §9's helper where that is an agent account), only when content changed, and a
|
||||||
|
record of what was rendered. The program-written preference keys of `settings.json` taken over into
|
||||||
|
`personal_settings` when the program changes them; every other key written back.
|
||||||
|
3. **Drift.** A comparison at least once a minute and at once on a file watch, over the rendered paths, the
|
||||||
|
two watched entries and the directory's top level; findings in the module's health statement (ADR 0240)
|
||||||
|
naming each path and what differs. **The look that finds a difference acts**: a changed rendered file is
|
||||||
|
written back on that look, its hand version kept in `home-copies` first (a key that decides what runs is
|
||||||
|
never taken over); a missing one rendered again; a file nobody rendered under `rules/`, `skills/`,
|
||||||
|
`commands/`, `agents/`, `hooks/` or `output-styles/` left and reported before the node's first adoption,
|
||||||
|
and after it moved into `home-copies`, restorable, and raised; an entry under neither list, a
|
||||||
|
`settings.local.json` key other than `permissions`, and a marketplace or plugin other than the vendor's and
|
||||||
|
`nox-mesh` raised and left. The controller:
|
||||||
|
the condition kind `agent-home-drift`, a warning that needs the operator, exempt at the first-node gate as
|
||||||
|
the relogin and directory-used-as-found kinds are (one small change in `mesh-controller`).
|
||||||
|
4. **The two routes.** Every register, import and remove tool at the operator scope or for an operator's
|
||||||
|
home publishes an approve ask through the asker's client, with the item in full and a digest over text,
|
||||||
|
name, kind and scope, and acts once on the warrant; while the router carries no channel that answers
|
||||||
|
approve for this module, it records the request in `pending` and answers the exact
|
||||||
|
`nox settings set claude-code '<json>'` line, merged into the current layer so nothing else changes. A
|
||||||
|
home-scope name an operator-scope item of the same kind uses is refused. The home scope into an agent
|
||||||
|
account's own home stays as issue 340 built it.
|
||||||
|
5. **The guard.** A refusal of any shell command or file edit whose target is under a rendered path, is
|
||||||
|
`settings.local.json`, or is a `.json` file under `plugins/` (a redirect, append, copy, move, in-place
|
||||||
|
edit, removal), and of `claude plugin install`, `claude plugin marketplace add` and their in-session forms,
|
||||||
|
naming the tool; nothing for the rest of the working state; the override of ADR 0245 §5.
|
||||||
|
6. **The tools.** `claude_code_personal_rule_register`, `_list`, `_unregister`;
|
||||||
|
`claude_code_personal_settings_get`, `_set`; `scope: operator` on the skill, command and agent register
|
||||||
|
tools; `claude_code_config_import` taken again at the operator scope by the two routes;
|
||||||
|
`claude_code_config_status` extended with the drift findings, the entries the module knows under
|
||||||
|
neither list, the watched entries and the pending requests.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** The module's tests of design 36 "How it is checked" for ADR 0273 §1,
|
||||||
|
§2, §3 and §4, each failing on the commit before. The catalogue's checks pass.
|
||||||
|
|
||||||
|
### WP8 — Personal rules and the operator's items, live
|
||||||
|
|
||||||
|
*The live mesh. Half a day, and it is the operator's.* One workstation first, as every package here.
|
||||||
|
|
||||||
|
1. The module with WP7 reaches the laptop, where no adoption is recorded yet. `agent-home-drift` is raised
|
||||||
|
there, naming the two rules, the five skills, `settings.json` (its keys differ from an empty registration),
|
||||||
|
`.hal-binding.json` and `.credentials.json.testbak` as entries under neither list, and nothing else.
|
||||||
|
2. At the terminal the operator sets `personal_rules` with the two rules, `personal_settings` with the five
|
||||||
|
preference keys, `managed_settings` (for the laptop or the mesh) with the `autoMode` key, which is a
|
||||||
|
permission rule and cannot be imported at the operator scope, and `personal_skills` with the skills they
|
||||||
|
keep; removes the rest, `.hal-binding.json` and the credentials copy with the removal tool (no copy kept of
|
||||||
|
the credentials copy). The operator then records the laptop's first adoption. The condition clears.
|
||||||
|
3. The module reaches the other three nodes. On each, the condition names the retired `cleanup` skill and
|
||||||
|
the instruction file (and the workstation's subagent); the operator removes or imports each, once, and
|
||||||
|
records that node's first adoption.
|
||||||
|
|
||||||
|
**Proof.** On every node `~/.claude` holds the rendered configuration and the program's working state and
|
||||||
|
nothing else; a new session on the workstation has the laptop's two rules in its context; a rule written by
|
||||||
|
hand into `rules/` on the laptop is moved into the copies and raised within a minute; a hand edit of
|
||||||
|
`settings.json` is written back within a minute with the hand version shown by the status tool.
|
||||||
|
|
||||||
|
### WP9 — Lessons
|
||||||
|
|
||||||
|
*mesh-catalog. Two days.*
|
||||||
|
|
||||||
|
1. **The register**: the `lessons` bucket; `claude_code_lesson_register` (trigger, cause, rule, optional
|
||||||
|
before; refused when its rule matches a declined lesson's, with the reason), `_list` (the three states
|
||||||
|
marked apart), `_accept` and `_decline` (the two routes of WP7 step 4, since they write into the
|
||||||
|
operator's home); expiry of a proposal after 30 days; `lessons_accepted` and `lessons_declined` read
|
||||||
|
from the terminal's settings.
|
||||||
|
2. **Accepted lessons as personal rules**: registered at the operator scope with the lesson as their source;
|
||||||
|
the home instruction file names each as accepted from its lesson.
|
||||||
|
3. **The reminders**: a session-start hook in the `nox-mesh` plugin, the module's own binary, printing the
|
||||||
|
rules accepted in the last 30 days and those whose `before` matches the working directory or repository;
|
||||||
|
the guard printing the rule of a matching accepted lesson once per session before the tool runs,
|
||||||
|
refusing nothing for it.
|
||||||
|
4. **The instruction section**: the mesh's `working-on-the-mesh` section gains the paragraph of design 36
|
||||||
|
§10 on when an agent records a lesson, that a correction goes to the register and not to the agent's own
|
||||||
|
memory, and that its existing corrections are proposed first; a render test holds the section to it; and the `talking-to-the-operator` section one line saying the agent
|
||||||
|
says in one line that it did. Both are set at the terminal, as every mesh-wide section is since issue 340.
|
||||||
|
|
||||||
|
**Proof, before anything runs live.** The module's tests for ADR 0273 §5 in design 36 "How it is checked".
|
||||||
|
|
||||||
|
5. **The first lessons**: on the laptop, once the register exists, the agent proposes, unasked, every note in
|
||||||
|
its memory that records a correction the operator gave (at least 9 on 2026-10-09).
|
||||||
|
|
||||||
|
**Proof, live, on one workstation.** The laptop's memory corrections are in `claude_code_lesson_list` as
|
||||||
|
proposals. In a session, the operator corrects a way of working; the lesson is in
|
||||||
|
`claude_code_lesson_list` in that turn; the operator accepts it at the terminal; the next session on another
|
||||||
|
node names it at start; a tool whose name matches its `before` is preceded by the rule once.
|
||||||
|
|
||||||
|
### WP10 — The rest of the nodes, and the as-is
|
||||||
|
|
||||||
|
*The live mesh. Half a day.* WP9 on every node; the as-is of the agent
|
||||||
|
([00-as-is/15](../00-as-is/15-the-agent-and-its-licences.md)) rewritten to say what the home holds and how
|
||||||
|
a lesson travels; design 36 and this document set to `implemented` ([playbook 02](../../00-META/process/02-graduation.md)).
|
||||||
|
|
||||||
|
**Proof.** The live line of ADR 0273's "How it is checked", on every node.
|
||||||
|
|
||||||
## What is deliberately not here
|
## What is deliberately not here
|
||||||
|
|
||||||
- **The package repository seat** for a distribution that does not carry the agent's package (design 36
|
- **The package repository seat** for a distribution that does not carry the agent's package (design 36
|
||||||
@@ -213,4 +339,5 @@ Each package's proof is run when the package is finished and its line here gains
|
|||||||
commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under it
|
commit, as design 38 does. A package whose proof fails is not reworded; the failure is recorded under it
|
||||||
and the package stays open. When WP2's live proof runs, design 36 moves to `in-progress` with its owning
|
and the package stays open. When WP2's live proof runs, design 36 moves to `in-progress` with its owning
|
||||||
repository; when WP5's does, design 39 does too; and when WP6's does, both move to `implemented`, with
|
repository; when WP5's does, design 39 does too; and when WP6's does, both move to `implemented`, with
|
||||||
the as-is written.
|
the as-is written. WP6's did, on 2026-10-04. ADR 0273 opened WP7–WP10 on 2026-10-09, so design 36 and this
|
||||||
|
document are `in-progress` again until WP10's proof runs.
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code:
|
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
|
- 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-08
|
updated: 2026-10-10
|
||||||
decisions:
|
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/0217-no-change-to-a-machine-takes-effect-unseen.md
|
||||||
- 02-DECISIONS/0030-data-outlives-the-mesh-that-declared-it.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
|
- **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.
|
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
|
## 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
|
- **What was sent is kept.** Each send records a summary of the declaration it sent the machine, beside
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: in-progress
|
status: in-progress
|
||||||
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
|
code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab]
|
||||||
updated: 2026-10-09
|
updated: 2026-10-10
|
||||||
decisions:
|
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/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/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
|
- 02-DECISIONS/0264-a-message-a-consumer-gives-up-on-is-kept-until-a-person-delivers-it-again-or-drops-it.md
|
||||||
@@ -72,6 +73,8 @@ Every kind of state the core keeps, and the one component that writes it. Anyone
|
|||||||
| State | Writer | Kept in | Others |
|
| State | Writer | Kept in | Others |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| a machine's declaration | controller (lease holder) | the bus, last per subject | read |
|
| a machine's declaration | controller (lease holder) | the bus, last per subject | read |
|
||||||
|
| a hand-over asked of a machine ([issue 356](../../04-ISSUES/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)) | controller, at its terminal | the machine, beside its state | the engine verifies the mesh's signature and records it, or refuses |
|
||||||
|
| a fresh setuid search asked of a machine ([issue 361](../../04-ISSUES/361-the-control-nodes-agent-account-is-never-judged/00-report.md)) | controller, at its terminal | the machine, which throws its last search away | the engine verifies the mesh's signature and starts it, or refuses |
|
||||||
| a machine's applied state and its report | the node-engine's apply queue (§6) | the machine; the report on the bus | the reconcile and a delivery *enqueue*, never apply |
|
| a machine's applied state and its report | the node-engine's apply queue (§6) | the machine; the report on the bus | the reconcile and a delivery *enqueue*, never apply |
|
||||||
| the controller lease | the controller instance holding it | key-value `mesh-controller_lease` | a candidate waits |
|
| the controller lease | the controller instance holding it | key-value `mesh-controller_lease` | a candidate waits |
|
||||||
| plans and their tiers | controller (lease holder), compare-and-set on the plan's revision | the controller's store | read through `plans` |
|
| plans and their tiers | controller (lease holder), compare-and-set on the plan's revision | the controller's store | read through `plans` |
|
||||||
@@ -96,7 +99,8 @@ this table marks it shared. Changing a writer is a change to this table, through
|
|||||||
|
|
||||||
**How it is enforced** ([ADR 0229](../../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)):
|
**How it is enforced** ([ADR 0229](../../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)):
|
||||||
the table is compiled into the controller, each row the bus carries with the subjects a write of it
|
the table is compiled into the controller, each row the bus carries with the subjects a write of it
|
||||||
publishes to — a declaration `mesh.node.*.declare`, a report `mesh.control.*.report` (its own machine's
|
publishes to — a declaration `mesh.node.*.declare`, a hand-over asked `mesh.node.*.ask.hand-over` (signed as
|
||||||
|
well, because a responder can reach a subject through a reply, which no grant refuses), a report `mesh.control.*.report` (its own machine's
|
||||||
only), the controller's buckets, stream creation, update and deletion and durable consumer creation, a
|
only), the controller's buckets, stream creation, update and deletion and durable consumer creation, a
|
||||||
build's outcome, a merge announced, a provider's standing — and its writer among the principals. A
|
build's outcome, a merge announced, a provider's standing — and its writer among the principals. A
|
||||||
grant that overlaps another writer's subject is refused at composition, naming the state and its
|
grant that overlaps another writer's subject is refused at composition, naming the state and its
|
||||||
@@ -235,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 | — |
|
| 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 | — |
|
| 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 | — |
|
| 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
|
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
|
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
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-08
|
updated: 2026-10-10
|
||||||
decisions:
|
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/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/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
|
- 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 |
|
| approve or reject a waiting retirement set (`retire approve` / `retire reject`) | approve |
|
||||||
| confirm a kept binding moves (`pin`, while `binding-kept` names it) | 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 |
|
| 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 |
|
| silence a condition | acknowledge |
|
||||||
| delete a retired consumer's data, one or by age (`cleanup delete`) | destroy |
|
| 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 |
|
| change the operator's identities, the away channel, or a factor's enrolment | destroy |
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: designed
|
status: designed
|
||||||
code: []
|
code: []
|
||||||
updated: 2026-10-09
|
updated: 2026-10-10
|
||||||
decisions:
|
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/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md
|
- 02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md
|
||||||
- 02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md
|
- 02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md
|
||||||
- 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
|
- 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
|
||||||
@@ -21,6 +22,11 @@ delivery group is two or more deliveries that share a head branch name, delivere
|
|||||||
are owned by one module, `mesh-delivery`, which records every step and asks the controller for each act.**
|
are owned by one module, `mesh-delivery`, which records every step and asks the controller for each act.**
|
||||||
([ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md).)
|
([ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md).)
|
||||||
|
|
||||||
|
**From its merge on, a delivery is delivered by the walk of its batch.** The controller collects merges in a
|
||||||
|
rolling merge window and plans each batch once, as one walk with at most one commit per repository, one walk
|
||||||
|
at a time. Every delivery the walk answers ends with it
|
||||||
|
([ADR 0276](../../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||||
|
|
||||||
This is the delivery of [to-be 10](10-delivery.md) carried out. To-be 10 says what is built: the comparison
|
This is the delivery of [to-be 10](10-delivery.md) carried out. To-be 10 says what is built: the comparison
|
||||||
of source and artifacts decides, and nothing here changes that. This design says who answers for one
|
of source and artifacts decides, and nothing here changes that. This design says who answers for one
|
||||||
commit's journey and in what order the steps are allowed.
|
commit's journey and in what order the steps are allowed.
|
||||||
@@ -57,7 +63,9 @@ Its id is `<owner>/<repository>@<twelve characters of its commit>`. It holds:
|
|||||||
its diffset (what ADR 0238 called the "change plan", under the glossary's name);
|
its diffset (what ADR 0238 called the "change plan", under the glossary's name);
|
||||||
- its **state**, its **transitions** (the last hundred, each with when, the event, from, to and why), and its
|
- its **state**, its **transitions** (the last hundred, each with when, the event, from, to and why), and its
|
||||||
**machine steps** while delivering;
|
**machine steps** while delivering;
|
||||||
- once merged, **the commit it landed on the trunk as**, and the walk that delivers it.
|
- once merged, **the commit it landed on the trunk as**, and the walk that delivers it: the walk of the batch
|
||||||
|
that answers its merge, which may carry a later commit of its repository that contains it (see *The merge
|
||||||
|
window and the batch*).
|
||||||
|
|
||||||
## The state table
|
## The state table
|
||||||
|
|
||||||
@@ -72,15 +80,15 @@ allows.
|
|||||||
| checked | a verdict arrived | 1 minute | nothing: it is decided at once |
|
| checked | a verdict arrived | 1 minute | nothing: it is decided at once |
|
||||||
| ready | it passed; it waits to be merged | none | — |
|
| ready | it passed; it waits to be merged | none | — |
|
||||||
| rejected | it failed its check | none | — |
|
| rejected | it failed its check | none | — |
|
||||||
| published | merged on the trunk its modules follow, its walk opened and asking nothing; it waits for its turn or its word | 30 minutes | `superseded` when a newer delivery took over its walk; `go` when its walk started |
|
| published | merged on the trunk its modules follow, its walk opened and asking nothing; it waits for its turn or its word | 30 minutes | `superseded` when a walk that does not answer its merge took over its walk; `go` when its walk started |
|
||||||
| held | it waits for a person; read again each tick, so it ends by itself when it moves nothing or a later merge covers it (ADR 0270) | 1 day | nothing: it is the operator's |
|
| held | it waits for a person; read again each tick, so it ends by itself when it moves nothing or a later merge covers it (ADR 0270) | 1 day | nothing: it is the operator's |
|
||||||
| delivering | its walk runs; its builds are asked and registered tier by tier | 2 hours | `delivered`, `failed` or `superseded`, as the walk's record says |
|
| delivering | its walk runs; its builds are asked and registered tier by tier | 2 hours | `delivered`, `failed` or `superseded`, as the walk's record says |
|
||||||
| delivered, failed, superseded, stopped, retired, covered | final | — | — |
|
| delivered, failed, superseded, stopped, retired, covered | final | — | — |
|
||||||
|
|
||||||
The transitions are those of ADR 0239 decision 2 and [ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md). A delivery that is
|
The transitions are those of ADR 0239 decision 2 and [ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md). A delivery that is
|
||||||
merged while not `ready` goes to `held`. Its check did not pass, and only a person decides that it goes on. A
|
merged while not `ready` goes to `held`. Its check did not pass, and only a person decides that it goes on. A
|
||||||
delivery merged with no walk opened for it within ten minutes is held too, saying so: nothing it moves follows
|
delivery merged with no walk answering it within ten minutes is held too, with the true reason, and listed by
|
||||||
that branch, or the merge was not heard. Nothing waits silently.
|
`stalled` at once. Nothing waits silently.
|
||||||
|
|
||||||
**What needs no person ends by itself** (ADR 0270):
|
**What needs no person ends by itself** (ADR 0270):
|
||||||
|
|
||||||
@@ -107,6 +115,49 @@ module and the build: `sent` (the first machine's send, or the rest's), `judging
|
|||||||
readings so far), `passed`, `failed`, `rolled-back`. They are read from the walk's own record in the
|
readings so far), `passed`, `failed`, `rolled-back`. They are read from the walk's own record in the
|
||||||
controller's `plan-moved` event, never inferred from time.
|
controller's `plan-moved` event, never inferred from time.
|
||||||
|
|
||||||
|
## The merge window and the batch
|
||||||
|
|
||||||
|
*2026-10-10, by [ADR 0276](../../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).*
|
||||||
|
|
||||||
|
- **The window.** The controller puts every merge into a branch a module follows into the open **batch**.
|
||||||
|
Each merge restarts the **merge window**; it closes when none arrived for `merge-window` (90 seconds by
|
||||||
|
default) or at the latest `merge-window-at-most` (10 minutes) after the batch's first merge. The batch, its
|
||||||
|
merges and its times are kept in the controller's store and survive a restart.
|
||||||
|
- **The cut.** When the window closes and no walk is open, the batch becomes one walk: one delivery plan for
|
||||||
|
every commit in it, tiered along the module graph across repositories, with at most one commit per
|
||||||
|
repository, the latest of its branch. The walk names every merge it answers: its own commits, and the
|
||||||
|
earlier merges of a repository that its later commit contains.
|
||||||
|
- **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.
|
||||||
|
- **The ends.** A walk done ends every delivery it answers: `delivered` for its own commits, `covered` for
|
||||||
|
an earlier merge of a repository it carried later, naming that merge. A walk that failed fails its own
|
||||||
|
commits' deliveries and marks none delivered; each earlier merge it answered is then walked on its own
|
||||||
|
commit, the newest first, one walk each, until one is delivered. A walk stopped stops them all.
|
||||||
|
- **A group inside a walk.** The pairs of a delivery group's order (ADR 0249) are edges of the walk's
|
||||||
|
tiers beside the module graph. mesh-delivery gives a walk its word only when every delivery it carries is
|
||||||
|
at its turn.
|
||||||
|
- **The bookkeeping.** mesh-delivery ties a walk to every live delivery whose merge it answers, follows a
|
||||||
|
folded walk to the walk that took it over, and never ties a walk to a delivery that has ended. A waiting
|
||||||
|
walk no live delivery holds is adopted as a delivery of its own, or listed by `stalled` at once.
|
||||||
|
- **Shown.** `plans` lists the open batch first: "assembling: 13 s left (at the latest 00:31:09); grouped:
|
||||||
|
novox/mesh-catalog@48bda475 (answers 553b7191), novox/mesh-controller@a6bc0931; plan not yet calculated",
|
||||||
|
and a batch waiting behind an open walk names that walk. The controller says the batch on the bus as
|
||||||
|
`plan-moved` in the state `assembling`, and mesh-delivery's `deliveries` and `show` say it for each merged
|
||||||
|
delivery it holds.
|
||||||
|
- **Bounds.** A batch still assembling a minute past `merge-window-at-most` with no walk open, and a batch
|
||||||
|
waiting behind an open walk past that walk's bound, each raise a condition. A merged delivery that no walk
|
||||||
|
answers within ten minutes is held with the true reason (the open walks of its repository and that none
|
||||||
|
names its merge, or that the controller holds none) and listed by `stalled` at once.
|
||||||
|
|
||||||
## A delivery group
|
## A delivery group
|
||||||
|
|
||||||
- **Membership**: the pull request's head branch name, across the mesh's repositories, among open pull
|
- **Membership**: the pull request's head branch name, across the mesh's repositories, among open pull
|
||||||
@@ -161,7 +212,7 @@ In this order, each by its owner:
|
|||||||
| `groups` | every group with its members in order and its derived state |
|
| `groups` | every group with its members in order and its derived state |
|
||||||
| `what-if` | the delivery plan a diffset would have, asked of the controller's planner, kept nowhere |
|
| `what-if` | the delivery plan a diffset would have, asked of the controller's planner, kept nowhere |
|
||||||
| `table` | the state table and the machine-step table, with bounds and H2's repairs |
|
| `table` | the state table and the machine-step table, with bounds and H2's repairs |
|
||||||
| `stalled` | every delivery held past its bound, with the transition H2 may take |
|
| `stalled` | every delivery held past its bound, with the transition H2 may take, and every waiting walk no live delivery holds |
|
||||||
| `recheck` | a rejected or ready delivery back to proposed, with why |
|
| `recheck` | a rejected or ready delivery back to proposed, with why |
|
||||||
| `release` | a held delivery to delivering, with why: a person's word |
|
| `release` | a held delivery to delivering, with why: a person's word |
|
||||||
| `stop` | any delivery not final to stopped, with why; its walk is ended through the controller |
|
| `stop` | any delivery not final to stopped, with why; its walk is ended through the controller |
|
||||||
@@ -191,7 +242,8 @@ up.
|
|||||||
- Any other walk waits for `deliver` only while the seat has a holder on record. With none on record, the
|
- Any other walk waits for `deliver` only while the seat has a holder on record. With none on record, the
|
||||||
controller starts it as it always did.
|
controller starts it as it always did.
|
||||||
- A waiting walk is said by the controller whatever mesh-delivery says of itself: S16 raises
|
- A waiting walk is said by the controller whatever mesh-delivery says of itself: S16 raises
|
||||||
`plan.<walk>.waiting` after 30 minutes, urgent after 4 hours, naming `plans go <walk> --why` as the way on.
|
`plan.<walk>.waiting` after 30 minutes, urgent after 4 hours, naming the merges the walk answers and
|
||||||
|
`plans go <walk> --why` as the way on.
|
||||||
A holder that is down is also D3's `holder-silent`; one that is up and never says go is said by S16 alone.
|
A holder that is down is also D3's `holder-silent`; one that is up and never says go is said by S16 alone.
|
||||||
|
|
||||||
## The switch
|
## The switch
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ code:
|
|||||||
- mesh-controller internal/catalogue (the seats node-nfs-server and node-mounts)
|
- mesh-controller internal/catalogue (the seats node-nfs-server and node-mounts)
|
||||||
- mesh-catalog modules/nfs-server
|
- mesh-catalog modules/nfs-server
|
||||||
- mesh-catalog modules/mounts
|
- mesh-catalog modules/mounts
|
||||||
updated: 2026-10-08
|
updated: 2026-10-10
|
||||||
decisions:
|
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/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/0051-shared-data-is-the-operators.md
|
||||||
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.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.
|
`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
|
- **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
|
`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.
|
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
|
- **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,
|
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
|
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
|
export goes; the mesh's stays. *Undo:* `zfs set sharenfs=<the old value>` by a person, recorded as a
|
||||||
hand-act.
|
hand-act.
|
||||||
7. **The downloads**, the same way: read-only first, then read-write.
|
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
|
8. **The gaming device**: an agent asks for `smb-credentials` with `secret-ask`, and the operator types it at
|
||||||
to `sources`; `node-mounts.list` says it `absent` while the device is off, and it mounts on access
|
that machine's desk (or gives it with `secret accept` at the terminal); the agent proposes the source in
|
||||||
when it is on. Then its fstab line is adopted.
|
`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.
|
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
|
**Root.** Both modules' tools act as root through `sudo -n`, and the `sudo` module grants the operator
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: in-progress
|
||||||
|
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
|
||||||
|
|
||||||
|
**The mesh keeps its issues in a module of its own, `mesh-issues`, one instance on the control-node, holding the seat
|
||||||
|
`issue-tracker`. It hands out every
|
||||||
|
issue's number from one counter that two callers can never both advance, holds each issue's status and links, refuses
|
||||||
|
a change the development cycle would refuse, and announces every change on the bus. hq keeps a folder only for a design
|
||||||
|
or governance issue, under the tracker's number.**
|
||||||
|
([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),
|
||||||
|
from [research 042](../../01-RESEARCH/042-the-mesh-keeps-its-own-issues/00-overview.md).)
|
||||||
|
|
||||||
|
An **issue** is a question about the mesh that someone must answer. The **issue tracker** is the module that holds
|
||||||
|
issues, named `mesh-issues`; the seat it holds is `issue-tracker`. The glossary holds both words.
|
||||||
|
|
||||||
|
**Acceptance.** An agent opens an issue and gets the next number; two agents opening at the same moment never get the
|
||||||
|
same one.
|
||||||
|
|
||||||
|
This document designs slice 1 in detail. The slices after it are listed in [§ 8](#8-the-slices-after-the-first),
|
||||||
|
each with what the operator sees when it is done.
|
||||||
|
|
||||||
|
## 1. The parts
|
||||||
|
|
||||||
|
```
|
||||||
|
control-node
|
||||||
|
|
||||||
|
mesh-issues (seat issue-tracker, held once for the mesh)
|
||||||
|
├ its process, launched by the node runtime
|
||||||
|
├ bucket issues one key per issue, by number; a short history per key
|
||||||
|
├ bucket counters one key per sequence; slice 1 has `issue` only
|
||||||
|
├ bucket grooming the backlog pass of the duplicate check (§ 9)
|
||||||
|
├ setting owners the registered list `located-in` may name
|
||||||
|
├ verbs open, get, list, move, note, link, import, groom,
|
||||||
|
│ served by the control-node's tool runner
|
||||||
|
├ 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
|
||||||
|
```
|
||||||
|
|
||||||
|
The module is written in Go against the SDK. It is built on the model of hits, a headless issue tracker on NATS, and
|
||||||
|
carries none of its code; its README says so.
|
||||||
|
|
||||||
|
## 2. The seat and its verbs
|
||||||
|
|
||||||
|
The module declares the seat `issue-tracker` at mesh scope and claims it exclusively
|
||||||
|
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)): there is one tracker per mesh. Its verbs are
|
||||||
|
the seat's tools, served through the tool runner and announced on the bus
|
||||||
|
([ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)), so an
|
||||||
|
agent finds them with `mesh_search` ("open an issue") and calls them as `issue-tracker.<verb>`.
|
||||||
|
|
||||||
|
| Verb | Takes | Does |
|
||||||
|
|---|---|---|
|
||||||
|
| `open` | a title, the report as text, optionally `located-in` and a folder path | checks the issue, then takes the next number and writes it; returns the number. Refuses an empty title or report, and an owner not on the registered list |
|
||||||
|
| `get` | a number | the issue whole: fields, notes, links |
|
||||||
|
| `list` | optionally a status, an owner, a phrase | compact rows, newest first, short enough to read in a chat: number, status, owner, title |
|
||||||
|
| `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.
|
||||||
|
|
||||||
|
**The caller is the bus credential, never an argument.** The runtime hands the module the identity of each call's
|
||||||
|
caller; the module records that identity as who acted. No verb takes an actor.
|
||||||
|
|
||||||
|
## 3. What one issue holds
|
||||||
|
|
||||||
|
- `number` (its key), `title`, `status` (`open`, `diagnosing`, `located`, `resolved`, `wontfix`), `opened`, `closed`;
|
||||||
|
- `located-in`: entries of the registered list of owners (repositories and modules), never free text;
|
||||||
|
- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`, and a reason for `wontfix`;
|
||||||
|
- `notes`: appended, each with who wrote it, when, and the text;
|
||||||
|
- `links`, typed: to another issue (`duplicates`, `relates-to`), a pull request, a decision record, a design, a
|
||||||
|
condition key or a delivery;
|
||||||
|
- `record`: the path of the issue's folder in hq, when it has one.
|
||||||
|
|
||||||
|
**The registered list of owners** is a setting of the module, changed through the controller's settings verb like any
|
||||||
|
other module setting. Slice 1 starts it with the repositories in hq's `00-META/repos.md` and the modules the catalogue
|
||||||
|
registers.
|
||||||
|
|
||||||
|
**An issue that has a folder in hq** kept its status in that folder's frontmatter until the import of slice 3 ran;
|
||||||
|
until then the tracker held its number, title and `record`, took its notes and links, and `move` refused it with the
|
||||||
|
reason: its status lives in its folder. Since the import, every issue is whole in the tracker, folder or not, and a
|
||||||
|
report in hq carries no status key. No status is written in two places for one issue.
|
||||||
|
|
||||||
|
## 4. State, the counter and events
|
||||||
|
|
||||||
|
**Buckets the module declares, the controller creates**
|
||||||
|
([ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)).
|
||||||
|
The manifest's `state` names two buckets, `issues` and `counters`, with their options: `issues` keeps a short history
|
||||||
|
per key, `counters` one value per key, neither expires. The module never creates a bucket. One issue is one value; at
|
||||||
|
the 256 KiB a value may be, a note that would push an issue past it is refused with that reason (the largest issue
|
||||||
|
folder in hq is 25 KiB).
|
||||||
|
|
||||||
|
**The counter is advanced by compare-and-set.** Beside `get`, `put`, `delete`, `keys` and `watch`, the runtime's state
|
||||||
|
verbs hold two writes that are refused when another writer was first, in the runtime and the SDK's state interface
|
||||||
|
together ([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)):
|
||||||
|
|
||||||
|
- `create` writes a key only if it does not exist;
|
||||||
|
- `update` writes a key only if its revision is still the one the caller read, and returns the new revision.
|
||||||
|
|
||||||
|
The SDK's `Increment` reads a counter and writes the next number with `create` or `update` at the revision it read,
|
||||||
|
trying again a bounded number of times when another writer was first. The counter is started once, by `create` at
|
||||||
|
the number before the first, so it never starts twice. `open` then runs in this order:
|
||||||
|
|
||||||
|
1. validate the issue; a refused issue spends no number;
|
||||||
|
2. read `counters/issue` and its revision;
|
||||||
|
3. `update` it to the next number at that revision; if another caller moved it first, the write is refused, and
|
||||||
|
`open` reads again and retries, a bounded number of times;
|
||||||
|
4. `create` the issue's key; an existing key is a fault the module reports loudly and never overwrites.
|
||||||
|
|
||||||
|
So the counter does not rely on there being one instance: two instances, or two calls racing in one, still never hand
|
||||||
|
out one number twice. **The counter starts** above the highest issue number on `main` and on every open branch of hq
|
||||||
|
at the moment the module is switched on. Numbers already skipped stay unused, and a number never moves.
|
||||||
|
|
||||||
|
**Every change is an event the module declares** under `emits`
|
||||||
|
([ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md)): `opened`, `moved`, `noted`, `linked`. Each carries
|
||||||
|
the number, what changed, and who changed it, so the events are the audit trail, and anything watching the bucket or
|
||||||
|
the events sees a change as it happens. No event carries a secret, and an issue holds none.
|
||||||
|
|
||||||
|
## 5. The cycle's rules, at the write
|
||||||
|
|
||||||
|
`move` refuses what `cycle.py` refuses at merge today, and the rules playbook 03 states:
|
||||||
|
|
||||||
|
| The issue moves to | Needs |
|
||||||
|
|---|---|
|
||||||
|
| any status | one of the five |
|
||||||
|
| `located` | `located-in` names at least one registered owner |
|
||||||
|
| `resolved` | `located-in`, and `fixed-by` saying what fixed it |
|
||||||
|
| `resolved`, for a core issue (its `located-in` names the controller, the node-engine, the tool runner, the SDK, or the catalogue's bus, forge or build agent) | `replay` (an id from mesh-lab's replays register) or `replay-none` saying why none is possible ([ADR 0237](../../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` 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
|
||||||
|
|
||||||
|
Through the mesh's tools first. An agent opening an issue calls `issue-tracker.open`, gets the number, and, for a design
|
||||||
|
or governance issue, writes `04-ISSUES/NNN-slug/` under it with the number in the report. Playbook 03's first step
|
||||||
|
changes to that the day slice 1 is live; nothing else in playbook 03 changes in slice 1. `list` answers "what is open"
|
||||||
|
in one call, from any session, and the responder on a channel answers from it once the seat exists.
|
||||||
|
|
||||||
|
## 7. How it is checked
|
||||||
|
|
||||||
|
| Rule | Checked by |
|
||||||
|
|---|---|
|
||||||
|
| Two opens at once never share a number | the module's test over a real bus: many concurrent `open` calls, from two instances, yield distinct consecutive numbers; the runtime's test that `update` at a stale revision is refused |
|
||||||
|
| A refused open spends no number | the module's test: an invalid open, then a valid one, gets the next number |
|
||||||
|
| An issue's key is never overwritten | the module's test: a `create` on an existing key is refused and reported |
|
||||||
|
| The cycle's rules hold at the write | the module's table test over § 5, one case per row, each refused case changing nothing |
|
||||||
|
| The module creates no bucket | the runtime refuses a module that tries (ADR 0201 §3); the controller's raise asserts both buckets |
|
||||||
|
| The caller is who acted | the module's test that the recorded actor is the call's identity, whatever the arguments say |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
| Slice | Delivers | Done when the operator sees |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | The `mesh-issues` module: `open`, `get`, `list`, `move`, `note`, `link`; a counter by compare-and-set in the SDK; playbook 03 takes its numbers from it | an agent opens an issue and gets the next number; two agents opening at the same moment never get the same one |
|
||||||
|
| 2 | Decision and research numbers from the tracker, each its own key in `counters`; a number is reserved when a branch starts | two agents writing a decision at once get two numbers, and no hq pull request is renumbered |
|
||||||
|
| 3 | `import`: every issue folder in hq read through the records module and written as an issue with its own number, idempotent and showing what it would change before it does (built 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:
|
||||||
|
none of them is in any slice above, and each needs a decision of its own.
|
||||||
@@ -52,6 +52,7 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | [ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md), [ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md) |
|
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | [ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md), [ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md) |
|
||||||
| [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | [ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md), [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) |
|
| [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | [ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md), [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) |
|
||||||
| [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **In progress.** A node shares one folder at a time through `nfs-server`, which provides it as a provision to the private network only, mapping every client to the folder's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | [ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), [ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md) |
|
| [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **In progress.** A node shares one folder at a time through `nfs-server`, which provides it as a provision to the private network only, mapping every client to the folder's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | [ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), [ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md) |
|
||||||
|
| [`53-the-issue-tracker.md`](53-the-issue-tracker.md) | **Designed.** The mesh keeps its issues in one module, `issues`, on the control-node: every number from one counter advanced by compare-and-set, the cycle's issue rules refused at the write, an event per change; hq keeps a folder only for a design or governance issue | [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) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-08
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller (upgrades.go, mergeCandidates and readsFrom), mesh-controller internal/inventory (dependencies.go, the packages edge), mesh-controller internal/builder (builder.go and source.go, a context's fingerprint), mesh-catalog modules/route-proxy and modules/build-agent (their recipes and build contexts)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 338. A module built from a shared repository moves on every merge to it
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
On 2026-10-08 the controller's repository took eight merges. Every one of them produced the same
|
||||||
|
delivery plan: build `mesh-controller`, then `build-agent`, then `route-proxy`, in three tiers. Each
|
||||||
|
tier waited for its own first-node gate, about three and a half minutes each, so every merge of the
|
||||||
|
controller took three gates where one would have done.
|
||||||
|
|
||||||
|
`build-agent` (the build seat's holder) and `route-proxy` (the public route proxy on the home server)
|
||||||
|
are packaged in the catalogue, but their recipes compile code from the controller's repository. Their
|
||||||
|
manifests say so with a build context: `build.artifacts[].context` names the git seat, the repository
|
||||||
|
`novox/mesh-controller` and the ref `main`.
|
||||||
|
|
||||||
|
The plan rebuilt and moved them whatever the merge changed. Two examples:
|
||||||
|
|
||||||
|
- Mesh-controller PR #159, merged as `ca09a07f`, changed files under `cmd/mesh-controller`,
|
||||||
|
`internal/broker`, `internal/link`, `internal/catalogue/verbs.go` and the root `module.json`. Nothing
|
||||||
|
under `examples/route-proxy` changed. Its walk said, on the home server, "recreates 1 of route-proxy's
|
||||||
|
3 container(s) (with a new image: server)" and "recreates 1 of build-agent's 1 container(s)".
|
||||||
|
- Mesh-controller PR #163, merged as `9b028b42`, did the same. The route proxy's program at that commit
|
||||||
|
is byte for byte the program at the commit before it (measured in the diagnosis), and it was still
|
||||||
|
sent a new image and restarted.
|
||||||
|
|
||||||
|
The controller's `plans` what-if shows that the files do not matter. A merge that changes only the
|
||||||
|
controller's `README.md` plans the same three modules in three tiers:
|
||||||
|
|
||||||
|
> a merge of novox/mesh-controller would build 3 module(s) in 3 tier(s) … build-agent, route-proxy
|
||||||
|
> package source from novox/mesh-controller, so they are rebuilt without their own source moving
|
||||||
|
|
||||||
|
## What the operator expects
|
||||||
|
|
||||||
|
The operator stated the rule the delivery plan should follow:
|
||||||
|
|
||||||
|
1. Changed files map to the module or modules whose declared build source contains them. One commit can
|
||||||
|
touch several modules.
|
||||||
|
2. The module graph's build dependencies add the modules that are built from a changed module.
|
||||||
|
3. Nothing else counts: not sharing a repository, and not a whole-repository build context.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
- **Deliveries are slow.** A controller merge walked for about fifteen minutes instead of about five,
|
||||||
|
and the walks of one repository must not overlap (issue 335), so every merge after it waits too. The
|
||||||
|
operator considers this a major bug and a main reason deliveries are slow.
|
||||||
|
- **A public service restarts for nothing.** The route proxy on the home server was recreated at
|
||||||
|
merges that did not change its program.
|
||||||
|
- **The planner says it is path-precise and is not.** ADR 0238 says a changed file touches exactly the
|
||||||
|
modules whose build reads it. That holds for a module's own directory and fails for a build context,
|
||||||
|
which is read whole.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
Not checked today. The diagnosis proposes the checks that would hold the fix: a planner test in which a
|
||||||
|
change to a file outside a module's declared source paths moves nothing, and the `plans` what-if above
|
||||||
|
answering one module for a `README.md`-only change.
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
# 338. Diagnosis
|
||||||
|
|
||||||
|
## 2026-10-08 — the record first
|
||||||
|
|
||||||
|
`records_search` for "route-proxy rebuilt" found nothing. "build context" found
|
||||||
|
[ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md) (a build
|
||||||
|
context may live on the git seat) and issue 060's note that the route proxy's build context is the
|
||||||
|
controller's repository, not its own module directory. "fingerprint" found issue 280 and
|
||||||
|
[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).
|
||||||
|
Issue 280 fixed the wide rebuild for a module's own directory and left one sentence that matters here:
|
||||||
|
the source fingerprint hashes "the git tree of each other repository an artifact's context is cloned
|
||||||
|
from", which is the whole repository.
|
||||||
|
|
||||||
|
## How a merge decides what it moves
|
||||||
|
|
||||||
|
Read in mesh-controller at `8170fc58`.
|
||||||
|
|
||||||
|
**Facts, from the code.**
|
||||||
|
|
||||||
|
1. `mergeCandidates` (cmd/mesh-controller/upgrades.go) sorts every catalogued module into two kinds.
|
||||||
|
A module whose own source is the merged repository and branch is a candidate *built from* it. A
|
||||||
|
module whose last build read the merged repository as a context is a *packaging* module
|
||||||
|
(`readsFrom`). The test for the second kind is `sameRepository` and the ref, and nothing else.
|
||||||
|
2. `touchedBy`, the one function ADR 0238 names for mapping changed files onto modules, is applied
|
||||||
|
only to the first kind. A candidate is touched when a changed file lies inside its module directory,
|
||||||
|
or when it is built from the repository's root. The packaging modules never pass through it.
|
||||||
|
3. `reachOfMerge` and the merge handler `SourceMoved` both add every packaging module to what moves:
|
||||||
|
`moved := touched + packaging`. So for a packaging module the rule is **per repository**, not per
|
||||||
|
path. The changed paths and `module_dirs_said` of PR #159's delivery were read, and they decided
|
||||||
|
nothing for `route-proxy` and `build-agent`.
|
||||||
|
4. `mesh-controller` itself is built from the repository's root (its manifest's bundle compiles
|
||||||
|
`cmd/mesh-controller`; its source path is empty), so every changed file in the repository touches
|
||||||
|
it as well.
|
||||||
|
5. The tiers come from `inventory.Dependencies`. Two of its edge kinds make the walk serial here:
|
||||||
|
- a `packages` edge, from a module whose build read a repository to the module whose source that
|
||||||
|
repository is. So `route-proxy` and `build-agent` both depend on `mesh-controller`, because they
|
||||||
|
share its repository, not because either is built from the controller's artifacts;
|
||||||
|
- a `built-by` edge, from every source-built module to the build seat's holder. So `route-proxy`
|
||||||
|
waits for `build-agent`.
|
||||||
|
|
||||||
|
That gives three tiers, each with its own first-node gate.
|
||||||
|
6. The manifests (mesh-catalog `modules/route-proxy/module.json` and `modules/build-agent/module.json`)
|
||||||
|
declare the context as the whole repository: `{seat: git, repository: novox/mesh-controller, ref:
|
||||||
|
main}`. Nothing in a manifest can say which paths of it a build reads. Their recipes copy the whole
|
||||||
|
context (`COPY . .`) and compile one package from it: `./examples/route-proxy` and
|
||||||
|
`./cmd/mesh-builder`.
|
||||||
|
7. These are the only two manifests in the catalogue with a build context. Both name the controller's
|
||||||
|
repository. That repository is also the source of `mesh-controller`, so three modules share it.
|
||||||
|
|
||||||
|
**The operator's rule, against the code.** Rule 1 (a changed file maps to the modules whose declared
|
||||||
|
build source contains it) holds for a module's own directory in `touchedBy`, and is broken by fact 3:
|
||||||
|
a packaging module moves on any file. Rule 2 (the graph's build dependencies add what is built from a
|
||||||
|
changed module) is broken by fact 5: the `packages` edge treats sharing a repository as a build
|
||||||
|
dependency. Rule 3 (nothing else counts) is broken by both. A manifest has no way to declare the build
|
||||||
|
source inside a context (fact 6), so the planner could not obey rule 1 for these modules even if it
|
||||||
|
tried.
|
||||||
|
|
||||||
|
## Why a rebuild of an unchanged program still moved
|
||||||
|
|
||||||
|
Issue 280's fix should make a rebuild of an unchanged source no move. It does not here, for two
|
||||||
|
reasons.
|
||||||
|
|
||||||
|
**The fingerprint reads the whole repository** (fact, internal/builder/builder.go and source.go). For
|
||||||
|
an artifact with a context, the builder clones the repository and records `git rev-parse HEAD^{tree}`,
|
||||||
|
the root tree. Any commit to the repository changes it, so two builds never share a fingerprint.
|
||||||
|
|
||||||
|
**The program embeds the commit** (fact, measured). The builder clones the context with its `.git`
|
||||||
|
directory, the controller's repository has no `.dockerignore`, and the recipes run `COPY . .` and then
|
||||||
|
`go build -trimpath` without `-buildvcs=false`. Go's default then stamps the commit and its time into
|
||||||
|
the binary. Built on the laptop with the recipe's flags at `8170fc58`, `go version -m` on the route
|
||||||
|
proxy shows `vcs.revision=8170fc58…` and `vcs.time=2026-10-08T19:48:03Z`. So every commit gives a
|
||||||
|
different program even where no source it compiles changed. The golang base image carries git, so I
|
||||||
|
expect the same stamp inside the build; I did not inspect a built image.
|
||||||
|
|
||||||
|
**And the image is not reproducible either.** Issue 280 already found that two `docker build`s of one
|
||||||
|
source give two digests. The build records confirm it: `build-agent` was built three times tonight
|
||||||
|
from catalogue commit `7b307a06` and got three different image digests.
|
||||||
|
|
||||||
|
## What actually changed in the two programs
|
||||||
|
|
||||||
|
To separate "the program changed" from "the plan moved it anyway", each program was built on the
|
||||||
|
laptop at each of tonight's merges, with `-trimpath -buildvcs=false`, and hashed.
|
||||||
|
|
||||||
|
| merge | route proxy program | build seat program |
|
||||||
|
|---|---|---|
|
||||||
|
| `3f68a495` | `41b7e844` | `262ef27d` |
|
||||||
|
| `0cf5a3a5` | `41b7e844` (same) | `17ae6be8` |
|
||||||
|
| `ac91357a` | `41b7e844` (same) | `323e4388` |
|
||||||
|
| `e3ec15f7` | `41b7e844` (same) | `9e0b21b0` |
|
||||||
|
| `7f65e627` | `41b7e844` (same) | `8825b46e` |
|
||||||
|
| `9b028b42` (#163) | `41b7e844` (same) | `062d8c27` |
|
||||||
|
| `ca09a07f` (#159) | `f0a5dda5` | `87b9e34e` |
|
||||||
|
| `efcdd5dd` (#161) | `57669ce3` | `9c9fc316` |
|
||||||
|
| `8170fc58` (#165) | `57669ce3` (same) | `9c9fc316` (same) |
|
||||||
|
|
||||||
|
The route proxy's program changed at two of nine merges. The build seat's program changed at eight of
|
||||||
|
nine. **This corrects part of the symptom as first reported**: PR #159 did change the route proxy's
|
||||||
|
program. The proxy imports `internal/broker`, `internal/lease` and `internal/envfile` from the
|
||||||
|
controller's repository, and #159 changed `internal/broker`. "Nothing under `examples/route-proxy`
|
||||||
|
changed" was true, and is the wrong test: a module's build source is its import closure, not its
|
||||||
|
directory. PR #163 (`9b028b42`) is the clean case: the route proxy's program was unchanged and it was
|
||||||
|
still recreated on the home server.
|
||||||
|
|
||||||
|
So a path-precise rule that looked only at `examples/route-proxy` would have missed #159's real
|
||||||
|
change. The build source of the route proxy is `examples/route-proxy`, `internal/broker`,
|
||||||
|
`internal/lease`, `internal/envfile`, `go.mod`, `go.sum` and `vendor/`.
|
||||||
|
|
||||||
|
## Measured: what the extra tiers cost
|
||||||
|
|
||||||
|
From `mesh-controller.plans` (read only), the first-node gate of each tier of tonight's controller
|
||||||
|
walks:
|
||||||
|
|
||||||
|
| walk | mesh-controller | build-agent | route-proxy | three gates |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `3f68a495` | 3m40s | 3m43s | 3m46s | 11m09s |
|
||||||
|
| `0cf5a3a5` | 3m39s | 3m26s | 3m34s | 10m39s |
|
||||||
|
| `e3ec15f7` | 3m02s | 3m51s | 3m14s | 10m07s |
|
||||||
|
| `7f65e627` | 3m37s | 3m36s | 3m33s | 10m46s |
|
||||||
|
| `9b028b42` | 3m02s | 3m36s | 3m32s | 10m10s |
|
||||||
|
| `ca09a07f` | 3m43s | 3m46s | 3m40s | 11m09s |
|
||||||
|
| `efcdd5dd` | 3m38s | 3m40s | 3m37s | 10m55s |
|
||||||
|
|
||||||
|
Two more walks (`a5f53ef9`, `ac91357a`) were superseded part-way, and `8170fc58` was still walking at
|
||||||
|
21:50. The gates alone are 10 to 11 minutes per walk, of which 7 minutes are the two packaging tiers.
|
||||||
|
Each tier also builds before it sends: the build seat's image took 53 seconds at 21:36. Estimated from
|
||||||
|
the plan's creation time and the build records, `ca09a07f` walked from 21:07 to about 21:23 and
|
||||||
|
`efcdd5dd` from 21:32 to about 21:47, about 15 minutes each. The controller's own tier, alone, is about
|
||||||
|
4 to 5 minutes. Of the seven completed walks, five recreated `route-proxy` on the home server
|
||||||
|
(`3f68a495`, `e3ec15f7`, `7f65e627`, `9b028b42`, `ca09a07f`), and its program had changed in one of
|
||||||
|
them. Four recreated `build-agent` (`e3ec15f7`, `7f65e627`, `9b028b42`, `ca09a07f`).
|
||||||
|
|
||||||
|
## A second finding, not yet explained
|
||||||
|
|
||||||
|
The deploy plan names a build of a packaging module by the module's own source commit, which is the
|
||||||
|
catalogue's. `moveFacts.identical` (cmd/mesh-controller/release.go) calls two builds identical when
|
||||||
|
their commits are equal. In `efcdd5dd`'s walk both modules went `7b307a06 → 7b307a06`, nothing was
|
||||||
|
said to be recreated, and the gate still took about 3.5 minutes for each. Yet the route proxy's
|
||||||
|
program did change at `efcdd5dd` (table above). **I do not know whether that program reached the home
|
||||||
|
server.** Either it was sent and the plan said nothing, or it was not sent and the route proxy runs the
|
||||||
|
program from before #161. Next step: compare the image digest the home server's route proxy runs with
|
||||||
|
the digest of `build-1791487124940030405` and of the build before it. The same identity by catalogue
|
||||||
|
commit would also make issue 280's "repeat" rule register a different program as the same build.
|
||||||
|
|
||||||
|
## Ruled out
|
||||||
|
|
||||||
|
- **The changed paths not reaching the planner.** PR #159's delivery record carries its paths and
|
||||||
|
`module_dirs_said: true`. They are read, and `touchedBy` uses them for the controller's own module.
|
||||||
|
They are never consulted for a packaging module.
|
||||||
|
- **The catalogue moving at the same time.** In some walks the catalogue's commit also changed
|
||||||
|
(`32307bd1 → 7b307a06`), but the README-only what-if shows the controller's repository alone is enough
|
||||||
|
to plan both modules.
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
Located. A module whose build reads a second repository as a context moves on **every** merge to that
|
||||||
|
repository, because `mergeCandidates` adds it by repository and never maps the changed files onto it;
|
||||||
|
the dependency graph's `packages` edge then orders it after the repository's own module, and `built-by`
|
||||||
|
orders the route proxy after the build seat, giving three gated tiers. A rebuild that changed nothing
|
||||||
|
still moves, because the context's fingerprint is the whole repository's tree and the Go build stamps
|
||||||
|
the commit into the program.
|
||||||
|
|
||||||
|
## Fix directions (not implemented)
|
||||||
|
|
||||||
|
1. **A manifest declares its build's source paths within a repository**, and the planner uses only
|
||||||
|
those, plus the module graph's build edges. For a context this means a field beside `repository`
|
||||||
|
and `ref`, such as a list of paths. `touchedBy` then treats a context module like any other: it moves
|
||||||
|
when a changed file is inside one of its paths. The `packages` edge goes, or is drawn only when a
|
||||||
|
changed path is inside the declared set. The builder fingerprints the git trees of those paths, not
|
||||||
|
the root tree, so a rebuild with an unchanged source is no move. Manifests that must change:
|
||||||
|
`route-proxy` (paths: `examples/route-proxy`, `internal/broker`, `internal/lease`,
|
||||||
|
`internal/envfile`, `go.mod`, `go.sum`, `vendor`) and `build-agent` (`cmd/mesh-builder` and its
|
||||||
|
twelve internal packages, the same three root files). They are the only two with a context today.
|
||||||
|
The controller's own manifest builds from the root and so also claims `examples/` and
|
||||||
|
`cmd/mesh-builder`; it should declare its paths too, or a change to the route proxy alone moves the
|
||||||
|
controller.
|
||||||
|
- **Tradeoff:** a hand-written list goes stale. When the route proxy starts importing another
|
||||||
|
internal package and nobody adds it, a real change is missed, which is worse than a needless
|
||||||
|
rebuild. The list therefore needs a check: the merge gate or the repository's own check compares
|
||||||
|
the declared paths with the program's import closure (`go list -deps`) and fails when the closure
|
||||||
|
reaches outside them. A derived alternative is to compute the closure at build time and record it
|
||||||
|
with the build; that is exact for Go and needs a reader per language.
|
||||||
|
2. **Reproducible builds**: `-buildvcs=false` (or a `.dockerignore` that leaves `.git` out of the
|
||||||
|
context) in both recipes, so an unchanged source gives an identical program. This alone does not
|
||||||
|
stop the walk: the image digest still differs (issue 280), and the planner would still plan and gate
|
||||||
|
three tiers. It is cheap and worth doing beside 1, so the program's identity no longer depends on the
|
||||||
|
commit.
|
||||||
|
3. **Split the route proxy and the build seat into repositories of their own.** Then the existing
|
||||||
|
per-directory rule already applies, with no new manifest field. The tradeoff: the route proxy and
|
||||||
|
the build seat share `internal/broker`, `internal/catalogue` and more with the controller. Splitting
|
||||||
|
means a shared library in the SDK (where the mesh's rule says shared code belongs) or copies of it,
|
||||||
|
and a change to that library then moves all three again, through a real build edge.
|
||||||
|
|
||||||
|
Recommendation: 1 with its check, and 2 beside it. A further question for the fix: whether `built-by`
|
||||||
|
should order a walk at all. It is a running-order edge for the build seat's worker (issue 206); the
|
||||||
|
operator's rule 2 counts only modules built *from* a changed module.
|
||||||
@@ -36,6 +36,12 @@ break adds a line of the caller's own to such a file.
|
|||||||
|
|
||||||
## Why it is an issue
|
## 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
|
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
|
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
|
agent session on a machine that runs the mesh MCP server. The generic `command` verb reaches the same
|
||||||
|
|||||||
+7
-1
@@ -3,7 +3,7 @@ status: located
|
|||||||
opened: 2026-10-09
|
opened: 2026-10-09
|
||||||
located-in: [mesh-catalog (modules/claude-code), mesh-controller (internal/catalogue/terminal_keys.go)]
|
located-in: [mesh-catalog (modules/claude-code), mesh-controller (internal/catalogue/terminal_keys.go)]
|
||||||
fixed-by:
|
fixed-by:
|
||||||
amended-design:
|
amended-design: 03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md
|
||||||
---
|
---
|
||||||
|
|
||||||
# 340. An agent can plant a hook in every agent session on the mesh
|
# 340. An agent can plant a hook in every agent session on the mesh
|
||||||
@@ -76,6 +76,12 @@ own settings file. It does not see a tool call that changes the managed files of
|
|||||||
operator's own home is not written from the tools (see "How it is checked"). In the operator's words: "once we build a UI, we
|
operator's own home is not written from the tools (see "How it is checked"). In the operator's words: "once we build a UI, we
|
||||||
can also update the context through it". A surface of the operator's own, which can tell the operator from
|
can also update the context through it". A surface of the operator's own, which can tell the operator from
|
||||||
an agent, is the intended way back.
|
an agent, is the intended way back.
|
||||||
|
> **Amended 2026-10-10 by [ADR 0274](../../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).**
|
||||||
|
> Instruction sections at the mesh and node scopes gain a second way beside the terminal: an agent proposes one
|
||||||
|
> with `claude_code_instructions_propose`, and every node takes it only on the operator's warrant (an ask at the
|
||||||
|
> level approve, answered on a channel that proves who answered), which each node proves from the router's own
|
||||||
|
> record. The other kinds stay the terminal's, and approved sections never render from the bus, so the still-open
|
||||||
|
> item below is unchanged.
|
||||||
3. **Passwords are not settings.** They come from the key vault and can be rotated. Every module found taking
|
3. **Passwords are not settings.** They come from the key vault and can be rotated. Every module found taking
|
||||||
a password, token or API key through a setting is recorded in
|
a password, token or API key through a setting is recorded in
|
||||||
[issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md), to move to
|
[issue 342](../342-a-password-is-taken-as-a-setting-and-outranks-the-vaults/00-report.md), to move to
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller (checks.go, the check request names the pull request's head as the ref), mesh-controller internal/builder (check.go, the check clones and checks out that ref and never merges the base), mesh-catalog modules/mesh-delivery (table.go, a ready delivery is rechecked only when a person asks or its group changes; holder.go, a merge on the trunk leaves the repository's other open deliveries as they were)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 341. A merge check judges the pull request's head, not what merging it makes
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-09 a node-engine pull request passed its merge check, and its merge broke the trunk. The
|
||||||
|
trunk then did not compile, so no build of it could be made.
|
||||||
|
|
||||||
|
What happened, in order (UTC):
|
||||||
|
|
||||||
|
1. **00:15.** Node-engine #64 and #65 (issue 339's follow-ups) merged. #65's merge commit, `d28ca95`, is
|
||||||
|
the node-engine build the nodes run. Both pull requests added a field `Applied.Declared` to the
|
||||||
|
node-engine's store record, a string, under the JSON key `declared`.
|
||||||
|
2. **00:49.** Node-engine #66 (ADR 0269) was checked, at head `dfb1415`. That check was asked after the
|
||||||
|
trunk had moved, so the check was not out of date by the clock. `mesh/merge-gate` passed ("4 of 4
|
||||||
|
compose") and `mesh/repo-check` passed (its `merge-check.sh`, every package `ok`). Read with
|
||||||
|
`mesh-delivery.checks` for the commit: both statuses `success`, set at 00:49:42, and "mergeable — every
|
||||||
|
status main requires is a success".
|
||||||
|
3. **00:54.** #66 merged, as `690966d`. #66 had added its own `Applied.Declared` field, a `*FoundUnit`,
|
||||||
|
**under the same JSON key `declared`**. The two edits touched different lines, so git merged them
|
||||||
|
without a conflict. The result does not compile:
|
||||||
|
`internal/store/store.go:145:2: Declared redeclared`.
|
||||||
|
4. **00:55.** The walk the merge started (`plan-1791507264215942736`) failed: "`mesh-host` failed to build
|
||||||
|
in tier 0". `mesh/delivery` on the head reads `failure`. Nothing was sent to a node, and every node kept
|
||||||
|
`d28ca95`.
|
||||||
|
5. **01:09.** Node-engine #67 repaired the trunk. It renamed #66's field to `DeclaredUnit`, key
|
||||||
|
`declared_unit`, and added a test that pins both keys.
|
||||||
|
|
||||||
|
The facts behind the "different lines" claim, read from the node-engine's repository: `dfb1415` does not
|
||||||
|
contain `d28ca95`. Their common ancestor is `a2c2c35` (#63's merge). `dfb1415` holds one `Declared`
|
||||||
|
(store.go line 107), `d28ca95` holds the other (line 139), and `690966d` holds both (lines 107 and 145).
|
||||||
|
|
||||||
|
No node was harmed, because a walk builds before it sends. But the merge check said "this may merge" of
|
||||||
|
a merge that did not build, and both required statuses were green when it merged.
|
||||||
|
|
||||||
|
## The cost before this: checks that went stale
|
||||||
|
|
||||||
|
The same night, the session that merged the pull requests below had to ask for their checks again by
|
||||||
|
hand, or rebase a branch, before the verdict was right (as that session reported it). Each time, another
|
||||||
|
merge or the order of merges had changed what the check would see. What each pull request's own text
|
||||||
|
shows:
|
||||||
|
|
||||||
|
- **Controller #165** (issue 335). Its body says `mesh/repo-check` "fails on
|
||||||
|
`TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose` until #159 merges". The head held the
|
||||||
|
trunk of before #159. A check of the head alone stays red until the branch is rebased, even after #159
|
||||||
|
is on the trunk.
|
||||||
|
- **Controller #159** (issue 330). Node-engine #60's body says the controller's repository check "failed
|
||||||
|
on exactly this test without it": the check reads the node-engine's trunk beside the controller, so #159
|
||||||
|
waited for #60 to merge and then for a new check.
|
||||||
|
- **Controller #148**. It was rebased so that its check would run on the current trunk.
|
||||||
|
- **Controller #169 and catalogue #145** (ADR 0268), and **controller #171 and catalogue #147** (the zfs
|
||||||
|
module). Each pair names a merge order in its body ("#169 first", "Do not merge before mesh-controller
|
||||||
|
#171"). The second of each pair needed its check asked again once the first had merged.
|
||||||
|
|
||||||
|
In every case a person kept track of which verdicts the last merge had made stale. That is the work the
|
||||||
|
merge check exists to remove.
|
||||||
|
|
||||||
|
## Why it is a design issue
|
||||||
|
|
||||||
|
The merge check is the rule that "a change is judged before it merges"
|
||||||
|
([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)).
|
||||||
|
The forge requires its two statuses on the trunk
|
||||||
|
([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)
|
||||||
|
decision 5). A person and an agent both read "both green" as "this merge is safe". But the check judges
|
||||||
|
something else: the pull request's head, alone. Two pull requests that each pass can break the trunk
|
||||||
|
together, and a recheck after the trunk moved judges the same head and passes again.
|
||||||
|
|
||||||
|
Nothing says that a verdict stops counting when the trunk moves. The delivery's state table moves a ready
|
||||||
|
delivery back to proposed only when a person asks or its group changes
|
||||||
|
([ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
||||||
|
A merge of another pull request into the same trunk changes neither.
|
||||||
|
|
||||||
|
## What is expected
|
||||||
|
|
||||||
|
- A merge check judges exactly what merging would make: the pull request's head merged into the current
|
||||||
|
trunk. The forge keeps no merge ref for a pull request (`git ls-remote` lists only
|
||||||
|
`refs/pull/<n>/head`), so the build seat makes that merge itself in its clone. A head that does not
|
||||||
|
merge cleanly is a failure that says so.
|
||||||
|
- A verdict is tied to the trunk commit it was judged against. When the trunk moves, every open pull
|
||||||
|
request's verdict into it is stale. The check either runs again by itself or is marked out of date (the
|
||||||
|
required statuses back to pending), so that a merge is never let through on a verdict for a trunk that
|
||||||
|
no longer exists.
|
||||||
|
- The two required statuses then mean "this merge is green", which is what everyone already reads them as.
|
||||||
|
|
||||||
|
## The records say the head
|
||||||
|
|
||||||
|
ADR 0237 decision 4 has the build seat check "the head". ADR 0238 decision 1 says "A commit not on the
|
||||||
|
trunk — a pull request's head, any branch — is only checked", and decision 2 computes the delivery plan
|
||||||
|
from "a repository, the branch it merges into, the commit at hand". Neither record says the head is merged
|
||||||
|
with its trunk first. **So the fix needs a decision record** that extends ADR 0237 §4 and ADR 0238 §1–§2
|
||||||
|
(what is checked is the merge of the head into the trunk at a named trunk commit) and ADR 0239's state
|
||||||
|
table (a trunk move makes a ready verdict stale). ADR 0238's publishing rule is not touched: a merge made
|
||||||
|
to check a pull request is still a commit off the trunk, only checked, never registered.
|
||||||
|
|
||||||
|
The diagnosis is in [01-diagnosis.md](01-diagnosis.md).
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# 341 — Diagnosis
|
||||||
|
|
||||||
|
## 2026-10-09
|
||||||
|
|
||||||
|
**The records first.** `records_search` for "redeclared", "refs/pull" and "merged into its base" found
|
||||||
|
nothing. "recheck" found [issue 305](../305-a-recheck-left-the-old-verdict-standing/00-report.md), which
|
||||||
|
is close but not the same. 305 made a recheck put both statuses back to pending before the new check
|
||||||
|
runs. It also recorded that "some of the rechecked pull requests turned red: judged against the trunk of
|
||||||
|
that day, they no longer composed". So stale verdicts were seen on 2026-10-07 too. 305 fixed how a
|
||||||
|
recheck is asked; it did not change what a check judges or when a verdict goes stale.
|
||||||
|
|
||||||
|
### What the check builds (mesh-controller at `36008e0`)
|
||||||
|
|
||||||
|
- `cmd/mesh-controller/checks.go`, around line 259: the build request for a pull request's check sets
|
||||||
|
`Ref: p.Commit`, the pull request's head. The trunk goes in as `Check.Base`, a branch name.
|
||||||
|
- `internal/builder/check.go`, line 52, the check's own description: it "clones the repository at the
|
||||||
|
pull request's head".
|
||||||
|
- `internal/builder/check.go`, lines 249–256 and 265: `clone` runs `git clone` and then
|
||||||
|
`git checkout <ref>` with `spec.Ref`. Nothing merges the base into that tree.
|
||||||
|
- `spec.Base` (declared at line 83, "what the gate compares the change against") is read only to pick
|
||||||
|
the check script from the trunk (`TheCheckThatJudges`, line 270, issue 310's fix) and to show the
|
||||||
|
touched manifests as they are on the trunk (around line 582). Neither builds the trunk with the change.
|
||||||
|
- A delivery group's other heads (lines 297–310) are cloned beside the head, each at its own head. They
|
||||||
|
are composed together for the merge gate. The repository's own `merge-check.sh` still runs on the head
|
||||||
|
alone.
|
||||||
|
|
||||||
|
So `mesh/repo-check` ran `go test` on `dfb1415`, a tree with one `Declared`, and passed. The tree that
|
||||||
|
merging made, with two, was never built before the merge.
|
||||||
|
|
||||||
|
### When a verdict goes stale (mesh-catalog at `63de373`)
|
||||||
|
|
||||||
|
- `modules/mesh-delivery/cmd/mesh-delivery/table.go`, line 60 and line 167: `EvRecheck` takes a rejected
|
||||||
|
or ready delivery back to proposed. Its comment says "a person asked for it again, or its group
|
||||||
|
changed".
|
||||||
|
- `holder.go`, `PullMerged` (line 347): a merge records the merge commit on that pull request's own
|
||||||
|
delivery and links its walk. It does not touch the repository's other open deliveries into the same
|
||||||
|
trunk.
|
||||||
|
- The forge marks a head pending only when it announces a new head (issue 305). A merge into the trunk is
|
||||||
|
not a new head of any other pull request.
|
||||||
|
|
||||||
|
So after #64/#65 merged, #66's verdict was not marked stale by anything. The recheck at 00:49 was asked by
|
||||||
|
a person. It judged the same head and passed again.
|
||||||
|
|
||||||
|
### What the records decided
|
||||||
|
|
||||||
|
- [ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)
|
||||||
|
decision 4: the controller "asks the build seat a check: the head, and beside it the controller the mesh
|
||||||
|
runs and its main, the catalogue, the node-engine it runs, and mesh-lab".
|
||||||
|
- [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)
|
||||||
|
decision 1: "A commit not on the trunk — a pull request's head, any branch — is only checked".
|
||||||
|
Decision 2: a delivery plan is computed from "a repository, the branch it merges into, the commit at
|
||||||
|
hand".
|
||||||
|
- [ADR 0239](../../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)'s
|
||||||
|
state table: `ready → recheck → proposed` when "a person asked, or its group changed".
|
||||||
|
|
||||||
|
The code does what the records say. The records name the head and never the merge, and say nothing of
|
||||||
|
a verdict going stale when the trunk moves. **This is a design gap, and its fix needs a decision record**
|
||||||
|
(playbook 02), not only a code change.
|
||||||
|
|
||||||
|
### Ruled out
|
||||||
|
|
||||||
|
- **A flaky test or a toolchain difference.** The failure is a compile error that exists only in the
|
||||||
|
merge. `dfb1415` and `d28ca95` each compile; `690966d` does not.
|
||||||
|
- **A check run before the trunk moved.** The check that let #66 merge was set at 00:49:42, 34 minutes
|
||||||
|
after #64 and #65 merged.
|
||||||
|
- **The forge merging badly.** git merged the two edits without a conflict because they touch different
|
||||||
|
lines. That is correct for git. A semantic conflict like this one is found only by building the merge.
|
||||||
|
|
||||||
|
### Owners
|
||||||
|
|
||||||
|
- **mesh-controller**: the check request and the builder's clone. The check builds the merge of the head
|
||||||
|
into the trunk commit it names, and says that trunk commit with its verdict.
|
||||||
|
- **mesh-catalog modules/mesh-delivery**: a trunk move makes the ready and rejected verdicts of the
|
||||||
|
repository's other open deliveries into that trunk stale. They are proposed again, or held out of date,
|
||||||
|
with the statuses back to pending (issue 305's path).
|
||||||
|
- **The forge's module** (mesh-catalog modules/gitea), possibly: if the fix chooses the forge's own
|
||||||
|
"outdated branch" protection instead, the protection is set there.
|
||||||
|
|
||||||
|
### Directions for the decision (not chosen here)
|
||||||
|
|
||||||
|
1. **The build seat merges, the delivery owner rechecks on a trunk move.** The check clones the trunk at
|
||||||
|
a named commit, merges the head into it (refusing a conflict), and runs both layers on that tree. Every
|
||||||
|
merge into a trunk proposes the open deliveries into it again. Cost: one check per open pull request
|
||||||
|
into that trunk per merge, on the build seat. How many that is on a busy night is not measured yet.
|
||||||
|
2. **The forge refuses an outdated branch.** Gitea's branch protection can block a merge when the pull
|
||||||
|
request's branch is behind its base. The check then stays a check of the head, and the head always
|
||||||
|
contains the trunk. Cost: every merge forces every other open pull request to rebase or merge the
|
||||||
|
trunk, by a person or an agent, and each of those is a new head and a new check. That is the manual
|
||||||
|
work the report lists, made mandatory.
|
||||||
|
3. **Both**: the merge is what is checked, and a verdict carries the trunk commit it was judged against.
|
||||||
|
The merge is refused while that commit is not the trunk's current one, until the automatic recheck
|
||||||
|
answers.
|
||||||
|
|
||||||
|
A delivery group already composes its members' heads for the merge gate. Whatever is chosen should say
|
||||||
|
how a group's composed check meets the trunk: merged into it, member by member in the group's order.
|
||||||
|
|
||||||
|
### Replay
|
||||||
|
|
||||||
|
This is a core issue (its `located-in` names the controller). It resolves with a replay in mesh-lab's
|
||||||
|
register, or a `replay-none:` saying why none is possible. A replay is possible: two branches off one
|
||||||
|
trunk commit, each adding the same field to one struct, each passing alone, and the second checked after
|
||||||
|
the first merged. Before the fix the second passes; after it, the second fails with the compile error.
|
||||||
@@ -23,8 +23,8 @@ The same day, read through the mesh's tools: the controller runs on the control-
|
|||||||
|
|
||||||
**The records name a command that does not exist.** Issue 339, issue 340, ADR 0266 (hq pull request 225, not yet
|
**The records name a command that does not exist.** Issue 339, issue 340, ADR 0266 (hq pull request 225, not yet
|
||||||
merged) and the agent module's README say that certain changes are made "at the controller's terminal" alone, and
|
merged) and the agent module's README say that certain changes are made "at the controller's terminal" alone, and
|
||||||
some give the line to type, such as `mesh-controller settings set claude-code '<json>'` and
|
some give the line to type, such as "`mesh-controller settings set claude-code '<json>'`" and
|
||||||
`mesh-controller node agent-account <node> <account>`. None of them says how the operator reaches that terminal, and
|
"`mesh-controller node agent-account <node> <account>`". None of them says how the operator reaches that terminal, and
|
||||||
nothing in the mesh provides it:
|
nothing in the mesh provides it:
|
||||||
|
|
||||||
- The controller is a mesh process. The node-engine unpacks its bundle under the mesh's own directory and writes a
|
- The controller is a mesh process. The node-engine unpacks its bundle under the mesh's own directory and writes a
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ Four repositories, in the order they merge:
|
|||||||
and runs the line through `runVerb`'s path, without `MESH_VERB` for the terminal and with
|
and runs the line through `runVerb`'s path, without `MESH_VERB` for the terminal and with
|
||||||
`MESH_VERB=mesh-cli` otherwise, after the `command` verb's refusals.
|
`MESH_VERB=mesh-cli` otherwise, after the `command` verb's refusals.
|
||||||
4. **mesh-catalog:** the module `mesh-cli` (its program, its archive into `/usr/local/bin`, its zsh alias `nox`),
|
4. **mesh-catalog:** the module `mesh-cli` (its program, its archive into `/usr/local/bin`, its zsh alias `nox`),
|
||||||
assigned to every node; and the agent module's README, whose `mesh-controller settings set …` becomes
|
assigned to every node; and the agent module's README, whose "`mesh-controller settings set …`" becomes
|
||||||
`mesh-cli settings set …`.
|
`mesh-cli settings set …`.
|
||||||
|
|
||||||
Each code change is a branch and a pull request, merged by the operator after ADR 0272 is accepted. The records
|
Each code change is a branch and a pull request, merged by the operator after ADR 0272 is accepted. The records
|
||||||
that tell the operator to type `mesh-controller <command>` at the terminal mean `mesh-cli <command>` on the
|
that tell the operator to type `mesh-controller <command>` at the terminal mean `mesh-cli <command>` on the
|
||||||
control-node once phase 1 is live; ADR 0266's `mesh-controller node agent-account` line is changed on its own
|
control-node once phase 1 is live; ADR 0266's "`mesh-controller node agent-account`" line is changed on its own
|
||||||
branch, which is not yet merged.
|
branch, which is not yet merged.
|
||||||
|
|
||||||
## Later
|
## Later
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ note. Each gap says what is open, why it matters, and what would close it.
|
|||||||
*Closed* on 2026-10-09 by mesh-controller #177: the build-source check judges the first matching rule, as the
|
*Closed* on 2026-10-09 by mesh-controller #177: the build-source check judges the first matching rule, as the
|
||||||
forge does.
|
forge does.
|
||||||
|
|
||||||
|
14. **Ways to root the node-engine's judge does not see.** ADR 0266 §3 lists them under *not judged, so not seen*:
|
||||||
|
among them a root-run unit or script the agent account can write, a directory on root's `PATH` it can write,
|
||||||
|
root's ssh keys readable by it, and a setuid program on a filesystem the search does not walk. None is known
|
||||||
|
on the control-node, but a healthy verdict says nothing about them. The record left them "for a later record"
|
||||||
|
with nothing tracking it; added here on 2026-10-10.
|
||||||
|
*Closes with:* each way judged by the node-engine beside the others, or a record that decides against judging
|
||||||
|
it and says why.
|
||||||
|
|
||||||
## Why it is an issue
|
## Why it is an issue
|
||||||
|
|
||||||
Each gap is a known way around a rule the mesh states. A gap that is written down can be weighed and closed. A
|
Each gap is a known way around a rule the mesh states. A gap that is written down can be weighed and closed. A
|
||||||
|
|||||||
+8
@@ -62,3 +62,11 @@ first, the earlier one second; one plan is left open, at the later commit, build
|
|||||||
done. `TestTheBranchOrderIsTheMerges` holds the order and the lookup, to the nanosecond. The first and the
|
done. `TestTheBranchOrderIsTheMerges` holds the order and the lookup, to the nanosecond. The first and the
|
||||||
last fail on the commit before the fix, and every condition of the change was mutated and caught.
|
last fail on the commit before the fix, and every condition of the change was mutated and caught.
|
||||||
A replay in mesh-lab's register is still owed before this core issue resolves (ADR 0237).
|
A replay in mesh-lab's register is still owed before this core issue resolves (ADR 0237).
|
||||||
|
|
||||||
|
## A sequel (2026-10-10)
|
||||||
|
|
||||||
|
Planning an earlier merge at the later merge's commit left that earlier merge's delivery without a walk:
|
||||||
|
mesh-delivery matches a walk to the delivery merged as its commit, so two catalogue merges 18 seconds apart
|
||||||
|
left the earlier one held for a person and the walk carrying it waiting for a word nothing gave, for 58
|
||||||
|
minutes, with no condition raised. See
|
||||||
|
[issue 362](../362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md).
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
---
|
|
||||||
status: open
|
|
||||||
opened: 2026-10-09
|
|
||||||
located-in: []
|
|
||||||
fixed-by:
|
|
||||||
amended-design:
|
|
||||||
---
|
|
||||||
|
|
||||||
# 353. 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 waits for the operator to push it by hand. This report waits for the same push.
|
|
||||||
|
|
||||||
## 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 37 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 whether 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.
|
|
||||||
Either needs a decision record.
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller (asker.go, asker_wire.go), mesh-controller internal/broker (users.go, asksSeatOf)]
|
||||||
|
fixed-by: mesh-controller pull request 186
|
||||||
|
amended-design:
|
||||||
|
replay-none: the whole of it is one process's record against one bus's user list; the controller's own test drives the asker with the bus's word held and withheld (TestNothingIsAskedWhileTheBusLacksTheControllersGrant), and a container runtime adds nothing to that
|
||||||
|
---
|
||||||
|
|
||||||
|
# 353. The controller asked before the bus held its grant
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-09, local times on the control node, from its controller's journal (one process throughout):
|
||||||
|
|
||||||
|
- 17:32 the router (the `operator-channel` holder) was unassigned, so that a merge gate of the catalogue had
|
||||||
|
nothing to send there; 17:52:54 a walk of another module pushed the control node and composed the bus's user
|
||||||
|
list **without** the controller's grant to ask: the grant is composed from a holder of the seat that is
|
||||||
|
*assigned*, and none was.
|
||||||
|
- 17:54:05 the controller started asking (the router was assigned again by then, so its check of the record
|
||||||
|
said a router was here), and the bus refused its publish of the ask, on the same connection, seven times
|
||||||
|
until 17:56:05. Each refusal was followed by "the operator could not be asked about machine.…: context
|
||||||
|
deadline exceeded". The self-check raised `bus.controller.refused`.
|
||||||
|
- 17:55:56 the push that put the router back on the control node sent it the user list **with** the grant; 17:56:05 the bus reloaded
|
||||||
|
it in place (the same second the watchdogs saw "jetstream not enabled", the reload's moment); 17:56:10 the
|
||||||
|
same connection asked successfully, and again at 18:03:53. The server applies a reloaded permission to a
|
||||||
|
standing connection: no reconnect was needed or made.
|
||||||
|
- `bus.controller.refused` was raised at 17:54:06 and cleared by itself at 18:11:44, about 15 minutes after
|
||||||
|
the last refusal at 17:56:05 (the controller's condition history). The condition's evidence lists the same
|
||||||
|
refusal read again every 30 seconds until 18:10:35; the journal shows no refusal after 17:56:05, so those
|
||||||
|
are the signal reading its record again, not new refusals.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
- **The record said yes before the bus did.** The controller's grant to ask follows the router's assignment
|
||||||
|
([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)
|
||||||
|
§3: a caller-named verb is granted to the module that uses the seat), and reaches the bus only when the
|
||||||
|
machine holding the bus is next pushed. Between `assign` and `push`, and on any push of that machine made
|
||||||
|
while the router is unassigned, the controller believed its own record of the router and published into a
|
||||||
|
refusal. The composition is right; the asker trusted it before it was on the bus.
|
||||||
|
- **The failure was loud in the wrong place.** Seven bus refusals and a warning about the bus, when the fact
|
||||||
|
was "the control node has not been pushed since the router was assigned", which the controller knows (its
|
||||||
|
push already judges whether the bus's machine is behind on the user list, to send it first).
|
||||||
|
|
||||||
|
## Acceptance test (the operator's view)
|
||||||
|
|
||||||
|
While the bus has not been given the controller's grant to ask, the controller asks nothing and the condition
|
||||||
|
"Questions for you not delivered" says which push carries the grant; once that push lands, the questions go
|
||||||
|
out, and the bus never refuses the controller's ask.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- `TestNothingIsAskedWhileTheBusLacksTheControllersGrant` (mesh-controller `cmd/mesh-controller/asker_test.go`):
|
||||||
|
nothing is asked while the bus's machine was not sent the user list composed now, it is said once, the
|
||||||
|
conditions that need the operator are raised as undelivered naming the push, and they are asked once the bus
|
||||||
|
holds the grant.
|
||||||
|
- Live: after the fix is rolled out, `assign` the router, read `mesh-controller.conditions` for
|
||||||
|
`asks-undelivered` naming the push, `push` the control node, and read that the asks go out with no
|
||||||
|
`bus.controller.refused` raised.
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-host (the node-engine: its repository, module, binary, units, launcher and paths), mesh-tools node-tools (the tool runner: its module, unit and bus account), mesh-controller (the names it keys the core on, and the words it shows), mesh-catalog (the registrations, and the modules that name the two), hq (the designs' code lines, repos.md, the glossary's identifier lines)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 354. A decided rename was never scheduled, and the operator believed it live
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
[ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)
|
||||||
|
(2026-10-07) settled the words: the program on every node is the **node-engine**, and the program that serves
|
||||||
|
every module's tools on a node is the **tool runner**. It left the code renames out on purpose — "`mesh-host`
|
||||||
|
an identifier until the code rename", "`node-tools` an identifier until renamed", and its §6 *What is not
|
||||||
|
renamed here*: "code is renamed by the repositories that own it, each with its own change" — and named no
|
||||||
|
issue, no owner and no date for them.
|
||||||
|
|
||||||
|
On 2026-10-09 the operator took the rename as long implemented and live. It is not. As of that day:
|
||||||
|
|
||||||
|
- **The repositories**, at their heads (the node-engine's at `b0d03be`, the controller's at `e6b00e2e`, the
|
||||||
|
catalogue's at `6ae112d`): the node-engine's repository and module are `mesh-host`; its binary is
|
||||||
|
`nox-mesh-host`; its service units are `nox-mesh-host.service`, `nox-mesh-host-resume.service` and
|
||||||
|
`nox-mesh-host-roused.service`; its launcher is `/usr/lib/nox-mesh-host/launch`; it is delivered under
|
||||||
|
`/usr/lib/nox-mesh-host/versions/<version>` and keeps its state under `/var/lib/mesh-host`. The tool
|
||||||
|
runner is the module `node-tools`, the unit `node-tools.service` and the bus account `node-tools`.
|
||||||
|
- **The live mesh**, read through the controller's `node` verb for the laptop at 18:05: `mesh-host` and
|
||||||
|
`node-tools` stand among its assigned modules, and `node-tools.runtime process node-tools.service` among
|
||||||
|
what it runs.
|
||||||
|
- **The conditions.** An open condition on the control-node (messenger not healthy) says that
|
||||||
|
"`mesh-host hand-over` at the machine" hands a directory to the mesh (its words, quoted; the operator's line is
|
||||||
|
`nox node hand-over`, issue 356); every hand-act record of the day
|
||||||
|
names its caller as `<the laptop>.node-tools`.
|
||||||
|
- **The record.** `records_search` for "until the code rename" finds ADR 0244's own table row and nothing
|
||||||
|
else; for "rename mesh-host", nothing. No issue, design or record tracks the rename.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
- **A deferral nothing tracks reads as done.** ADR 0244 is accepted; the glossary says node-engine; the
|
||||||
|
governing documents were reworded the same day. Everything a reader meets says the new name, and the one
|
||||||
|
sentence that says the code is not renamed says "later" with nobody's name on it. Two days later the
|
||||||
|
operator believed it done. This is the rule of `AGENTS.md` — an unenforced rule is indistinguishable from
|
||||||
|
a wrong one — in its deferral form: a record that puts work off without an issue is indistinguishable
|
||||||
|
from one whose work landed.
|
||||||
|
- **It is not one record's slip.** The check this issue adds, run over every accepted record from the
|
||||||
|
date ADR 0244 was accepted, found deferrals without an issue in ADR 0241 ("a later change requires them
|
||||||
|
once both holders serve them"), ADR 0270 ("left for a later change"), ADR 0273 ("not decided here", four
|
||||||
|
items) and ADR 0244 itself. ADR 0270 and 0273 are dated 2026-10-09, are held to the rule, and have
|
||||||
|
[issue 355](../355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md)
|
||||||
|
and [issue 357](../357-what-adr-0273-left-undecided-has-no-owner/00-report.md) now; ADR 0241 is dated
|
||||||
|
before the rule's start and stands as found.
|
||||||
|
- **Impact.** The operator plans against a mesh that does not exist: in conversation the mesh says
|
||||||
|
node-engine, while every file, unit, package and condition on a node says `mesh-host`, so a person at a
|
||||||
|
machine (`systemctl status nox-mesh-host`, `ls /var/lib/mesh-host`) and a person reading this repository
|
||||||
|
do not describe the same thing. The glossary's *Identifier until renamed* lines tell an agent the name is
|
||||||
|
temporary and never say when.
|
||||||
|
|
||||||
|
## The fix, in two parts
|
||||||
|
|
||||||
|
1. **This rename** is tracked here. The inventory of what carries the old names, the risk of renaming the
|
||||||
|
paths of a self-updating daemon on live nodes, and a staged plan are in
|
||||||
|
[`01-diagnosis.md`](01-diagnosis.md). Acceptance: **`mesh-cli node show <node>` and every condition say
|
||||||
|
node-engine; no file, unit or package on a node is named `mesh-host`; the tool runner likewise.**
|
||||||
|
2. **The rule**, from 2026-10-09: a decision that defers work names the issue that tracks it, with an
|
||||||
|
owner, or the deferral is not accepted. Playbooks [02](../../00-META/process/02-graduation.md)
|
||||||
|
(graduation) and [04](../../00-META/process/04-build-handoff.md) (handoff) say it;
|
||||||
|
[`cycle.py`](../../00-META/checks/cycle.py) checks it: an accepted record dated from 2026-10-09, or an
|
||||||
|
older one corrected under the rule, whose text puts work off ("until renamed", "left for later", "not
|
||||||
|
decided here", "postponed", "future work", "deferred", "a later change") names the issue as tracking it
|
||||||
|
("tracked by issue N"), in that paragraph or list item, or in a dated progressive insight in the same
|
||||||
|
section or quoting it. ADR 0244 carries the insight that points here.
|
||||||
|
|
||||||
|
This resolves when the acceptance sentence holds on every node. It is a core issue
|
||||||
|
([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)):
|
||||||
|
it resolves with a replay, or with `replay-none:` saying why none is possible.
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
# 354. Diagnosis
|
||||||
|
|
||||||
|
## 2026-10-09
|
||||||
|
|
||||||
|
**The record.** `records_search` for "until the code rename" finds only ADR 0244's table row; for the
|
||||||
|
old name with "rename", nothing. Issues 142, 194 and 210 carry the old name in their titles and keep it: an issue keeps
|
||||||
|
its words (ADR 0244 §5).
|
||||||
|
|
||||||
|
### What carries the old names
|
||||||
|
|
||||||
|
**This repository**, on `main` at `c613cf0`:
|
||||||
|
|
||||||
|
| Where | What | How much |
|
||||||
|
|---|---|---|
|
||||||
|
| `03-DESIGN/01-to-be/` | `mesh-host` and `node-tools` in prose and in `code:` lines | 24 documents, 97 lines; 19 designs name `mesh-host` in `code:`, which is the repository's name and stays right until the repository is renamed |
|
||||||
|
| `03-DESIGN/00-as-is/` | `code:` lines | 2 documents |
|
||||||
|
| `00-META/` | `repos.md` (the table says `mesh-host` exists, tier 0), playbook 07's list of repositories, the glossary's two *Identifier until renamed* lines | 3 documents |
|
||||||
|
| `02-DECISIONS/`, `01-RESEARCH/`, `04-ISSUES/` | the old names as written | 38, 9 and 135 documents; kept, because a record keeps its words |
|
||||||
|
|
||||||
|
**The code**, in the checkouts at the heads the report names:
|
||||||
|
|
||||||
|
- **The node-engine's repository, `mesh-host`.** Its manifest: module `mesh-host`, slug `host`, the
|
||||||
|
artifact's binary `nox-mesh-host`, the resource `launcher` at `/usr/lib/nox-mesh-host/launch`, the
|
||||||
|
resource `next` (the delivered archive) at `/usr/lib/nox-mesh-host/versions/<version>`. Its packaging:
|
||||||
|
`nox-mesh-host.service` (whose start command is the launcher), `nox-mesh-host-resume.service`,
|
||||||
|
`nox-mesh-host-roused.service`, `nox-mesh-host-launch`, `nox-mesh-host-rollback`,
|
||||||
|
`nox-mesh-host-network.sh` and an OpenRC script. The launcher's environment (`MESH_HOST_STATE_DIR`,
|
||||||
|
`MESH_HOST_LIBEXEC`, `MESH_HOST_BIN`, and its limits) and its fallback binary `/usr/bin/nox-mesh-host`.
|
||||||
|
The state directory `/var/lib/mesh-host`: the node's state, the foundation lock, and the launcher's
|
||||||
|
files (known-good, rolled-back, trial, the start counter). The commands `cmd/mesh-host` (its flag set is
|
||||||
|
named `mesh-host`) and `cmd/mesh-bootstrap`. The placement guard's `engineModule` (`mesh-host`) and
|
||||||
|
`engineTrees` (both directories). The words a report says, which conditions quote:
|
||||||
|
"`mesh-host hand-over` at the machine". The Go module path, which the controller's builder check vendors. The
|
||||||
|
report names a socket under `/run/nox-mesh-host`; I did not find one in the checkout at `b0d03be`, so
|
||||||
|
the diagnosis in that repository confirms it or strikes it.
|
||||||
|
- **The tool runner**, in the `mesh-tools` repository under `node-tools/`: the module `node-tools`, the
|
||||||
|
unit `node-tools.service`, the bus account `node-tools`, the mesh MCP server's description of itself
|
||||||
|
("reached as <node>.node-tools"), and the caller's address every hand-act record keeps,
|
||||||
|
`<node>.node-tools`.
|
||||||
|
- **The controller, `mesh-controller`**: 51 lines naming `mesh-host`, 18 naming `node-tools`. The core
|
||||||
|
modules the merge gate and the checks key on (`coreModules`: module to repository), the module the probes
|
||||||
|
key on (`hostModule`), the runtime module (`RuntimeModule`, in the catalogue and the broker packages),
|
||||||
|
the bus account (`BusNodeTools`), the broker's kind (`KindNodeTools`), the lease witness
|
||||||
|
(`NodeToolsService`, `ComponentNodeTools`, and a component named `mesh-host`), the seat dependencies, the
|
||||||
|
reconnects, and the put-back ordering of a send, keyed on the strings `mesh-host` and `mesh-controller`.
|
||||||
|
The words it shows: `"mesh-host": "the node-engine"` and `"node-tools": "the node tools"` — the second
|
||||||
|
is a retired word.
|
||||||
|
- **The catalogue, `mesh-catalog`**: 20 files name one or both — the README, the desktop session code of
|
||||||
|
five modules, the container runtime's tools and tests, the bus's tools, the service manager's client,
|
||||||
|
the SSH client's manifest, the lab's tools and the forge's tests. And the registrations: a catalogue row
|
||||||
|
for `mesh-host` (built from its own repository, ADR 0069) and one for `node-tools` (from `mesh-tools`),
|
||||||
|
and every node's assignment of both.
|
||||||
|
|
||||||
|
The owner of each area is the repository named. For the symptom's words, the whole of the rename, and the
|
||||||
|
check, this repository.
|
||||||
|
|
||||||
|
### The risk
|
||||||
|
|
||||||
|
**Renaming the paths of a self-updating daemon on live nodes.** The node-engine delivers itself: its
|
||||||
|
launcher, placed by the module's own declaration under `/usr/lib/nox-mesh-host`, runs the newest delivered
|
||||||
|
version under that directory's `versions/` on trial, and goes back to the known-good one recorded under
|
||||||
|
`/var/lib/mesh-host` when the trial fails
|
||||||
|
([to-be 45 §8](../../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md),
|
||||||
|
[ADR 0227](../../02-DECISIONS/0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md)
|
||||||
|
rule 8). Its unit is placed when the machine joins, not by the mesh
|
||||||
|
([to-be 21](../../03-DESIGN/01-to-be/21-the-installation-in-full.md): the installer does not install the
|
||||||
|
unit). So a build that writes under new paths while the launcher reads the old ones is a build the launcher
|
||||||
|
cannot find; and a launcher that reads only new paths cannot go back to a known-good version under the old
|
||||||
|
ones — it halts the node ("this node needs a person"), which on a server is a person at a keyboard. The
|
||||||
|
module's name is keyed in the controller's core handling (the put-back ordering, the gate's core modules,
|
||||||
|
the lease witness), so a renamed module is, to a controller that knows one name, a new module beside an old
|
||||||
|
one. The tool runner has the same shape at a smaller scale: the bus account and the unit are named in the
|
||||||
|
controller's user list and in every node's assignment.
|
||||||
|
|
||||||
|
Hence: **one release in which both names are accepted, on every side, before any release in which the old
|
||||||
|
one is dropped**, each release walked one node first — the laptop, where a halted node is the operator at a
|
||||||
|
keyboard — and then the rest. I think no stage should move before the delivery fixes of issues 351 and 352
|
||||||
|
are live, because a core walk is exactly what those broke; that is a guess from their symptoms, not a
|
||||||
|
measurement.
|
||||||
|
|
||||||
|
### The staged plan
|
||||||
|
|
||||||
|
0. **This repository** (this change): the issue; the glossary's identifier lines point here; the rule and
|
||||||
|
its check.
|
||||||
|
1. **The words shown**, safe at any time and in any order: the controller's shown words ("the node tools"
|
||||||
|
becomes the tool runner), the node-engine's report words (the hand-over as the node-engine's command),
|
||||||
|
the mesh MCP server's description of itself. No path changes.
|
||||||
|
2. **Both names accepted, for one release.** The controller treats `mesh-host` and the node-engine's new
|
||||||
|
module name as one core component (core modules, witness, put-back ordering), and `node-tools` and the
|
||||||
|
tool runner's as another (runtime module, bus account, kind). The node-engine's launcher and upgrade
|
||||||
|
package read the old and the new library and state directories, and the node-engine moves its state
|
||||||
|
once, the first time it runs under the new name. Both unit names run the same launcher. The conditions
|
||||||
|
and the `node` verb say node-engine. The merge gate composes every node green; walked one node first.
|
||||||
|
3. **The rename proper.** The new module names registered, assigned beside the old on the first node, the
|
||||||
|
old unassigned there, the acceptance sentence checked on that node, then the rest. The repository
|
||||||
|
renamed on the forge, with its build source and its Go module path; the packaging renamed. The tool
|
||||||
|
runner the same: module, unit, bus account — and the caller's address in hand-act records changes with
|
||||||
|
it, which whoever reads those records must know.
|
||||||
|
4. **The old names dropped**, one release later: the controller's acceptance of them, the launcher's old
|
||||||
|
paths, the glossary's two identifier lines (the old names move to *Not:* lines, and `words.py` then
|
||||||
|
fails on them in prose), the designs' `code:` lines and `repos.md` following the repository.
|
||||||
|
|
||||||
|
**Acceptance:** `mesh-cli node show <node>` and every condition say node-engine; no file, unit or package on a
|
||||||
|
node is named `mesh-host`; the tool runner likewise.
|
||||||
|
|
||||||
|
### Open
|
||||||
|
|
||||||
|
- **The new identifiers.** The glossary's words are node-engine and tool runner; the `nox-` prefix on the
|
||||||
|
binary and the units is the packaging's. Whether the binary is `nox-node-engine`, the directories
|
||||||
|
`/usr/lib/node-engine` and `/var/lib/node-engine`, and the tool runner's module `tool-runner`, is the
|
||||||
|
operator's call before stage 2 is built.
|
||||||
|
- **Whether the state directory moves at all.** What is under it is the node's own (the report sequence,
|
||||||
|
the known-good version): a new name beside a kept path is an option the diagnosis in the node-engine's
|
||||||
|
repository weighs.
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 355. A held delivery of a module that runs on one machine only is never covered
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||||
|
(2026-10-09) ends a held delivery by itself when a later merge that contains it was delivered to every
|
||||||
|
machine running its modules — the delivery is *covered*. Its consequences say: "A module that runs on one
|
||||||
|
machine only is never covered. A walk records 'the rest running it' only after its first machines passed
|
||||||
|
and the rest were sent. A module on one machine has no rest, so its steps never say it reached every
|
||||||
|
machine. mesh-delivery itself, on the control node alone, is such a module: a held merge of it stays held
|
||||||
|
for a person, as before this record. Reading 'every machine running it' from the first machines when they
|
||||||
|
are all the machines would close it, and is left for a later change."
|
||||||
|
|
||||||
|
That later change had no issue, no owner and no date. The check added with
|
||||||
|
[issue 354](../354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||||
|
found the sentence on the day the record was accepted. This issue is the pointer the record lacked; the
|
||||||
|
record carries a dated insight naming it.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
A held delivery of mesh-delivery — or of any module placed on one machine — that a later merge covered
|
||||||
|
stays held for a person, which is the hand-work ADR 0270 set out to remove; and nothing told the operator
|
||||||
|
this case was excluded except one sentence in the record's consequences. A deferral nothing tracks reads as
|
||||||
|
done (issue 354).
|
||||||
|
|
||||||
|
## Open
|
||||||
|
|
||||||
|
- Whether a walk whose first machines are all the machines should record that it reached every machine
|
||||||
|
(the record's own suggestion), or whether *covered* should read the machines running a module from the
|
||||||
|
placement rather than from the walk's steps.
|
||||||
|
- The owner is mesh-delivery, in the controller's repository, as the record says; `located-in:` is filled
|
||||||
|
when the diagnosis confirms where the walk's record is written.
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-host (cmd/mesh-host, internal/upgrade, internal/link), mesh-controller (cmd/mesh-controller/nodes.go, internal/broker, internal/link), mesh-catalog (checks/words)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 356. The operator's command on a node ran a stale engine, and a hand-over had no channel
|
||||||
|
|
||||||
|
**Severity: high.** A command on every node answered for a build that was not running, and the one act a
|
||||||
|
condition asked of the operator could not be done through the mesh.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-09 at 18:01 (local) the operator, on the control-node, did what a module's condition told them to:
|
||||||
|
the messenger module was waiting for a person, its data directory used as found
|
||||||
|
([issue 339](../339-a-verb-can-place-a-directory-at-the-machines-own/00-report.md)), and the condition said to
|
||||||
|
run the node-engine's `hand-over` "at the machine", as root. They typed it, with the directory's path, and the
|
||||||
|
node-engine answered **"unexpected argument"**.
|
||||||
|
|
||||||
|
Measured on that machine, right after:
|
||||||
|
|
||||||
|
- The command on `PATH` is the engine placed by hand at installation (`/usr/bin/nox-mesh-host`, the launcher's
|
||||||
|
fallback). Its `help` lists `profile`, `inventory`, `apply`, `reconcile`, `bundle`, `overlay`, `owned` and
|
||||||
|
`version`, and no `hand-over`: it is a build from before that subcommand existed.
|
||||||
|
- The engine running the machine is a delivered one, under the engine's own versions directory, started by its
|
||||||
|
launcher ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md)). It has `hand-over`.
|
||||||
|
Nothing had ever written the delivered build over the one placed by hand: the launcher runs the fallback only
|
||||||
|
while nothing is delivered, and the engine's retirement of old versions never touches it.
|
||||||
|
- Meanwhile the messenger's process crash-looped: its data directory was owned by an account of the machine
|
||||||
|
(uid 1000, mode 700) and left so, as issue 339 decides, while the module now runs as an account of its own.
|
||||||
|
|
||||||
|
So two faults, one visible through the other:
|
||||||
|
|
||||||
|
1. **The command the operator types on a node is not the engine that runs it.** After every delivery of the
|
||||||
|
engine, the file on `PATH` falls one build further behind, and answers — with `help`, `version`, `owned`,
|
||||||
|
or a refusal — for a build the machine is not running.
|
||||||
|
2. **The hand-over of issue 339 was a terminal act at the machine with no channel.** The condition told the
|
||||||
|
operator to go to the machine, as root, and type the daemon's name. [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)
|
||||||
|
had given the operator one command, `nox`, on every node, and the operator's rule of the same day says a
|
||||||
|
person never types a daemon's name; the way out of issue 339 was written before either.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
- **A tool that answers for a build that is not running is the lie this mesh exists to refuse.** The mesh's own
|
||||||
|
words: evidence over assertion, failure must be loud. The file on `PATH` asserted a version, listed commands,
|
||||||
|
and refused one that exists, with a successful-looking refusal.
|
||||||
|
- **An act the mesh asks of its operator must have a channel the mesh provides.** ADR 0272 made "at the
|
||||||
|
controller's terminal" a real place. "At the machine, as root" is the opposite: `ssh` and `sudo`, which the
|
||||||
|
operator's rules forbid for a mesh machine, and which no agent may do either.
|
||||||
|
- **The texts the mesh addresses to its operator named the daemons' lines** — the condition, the engine's
|
||||||
|
statement, the apply's log, READMEs and design documents — and nothing checked them.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- On every node, from its first delivered engine on, `nox-mesh-host …` typed at the machine ran a stale build.
|
||||||
|
On the four nodes today that is a build from before `hand-over`, the tunnel take's `--tunnel`, and the
|
||||||
|
judgements of ADR 0266.
|
||||||
|
- The messenger module stayed down for as long as its directory stayed as found, and the only way out the mesh
|
||||||
|
named could not be typed.
|
||||||
|
|
||||||
|
## The acceptance test (the operator's view)
|
||||||
|
|
||||||
|
- After a delivery of the node-engine proves itself on a node, `nox-mesh-host version` typed at that machine
|
||||||
|
answers the build the launcher is running, and its `help` lists what that build serves.
|
||||||
|
- `nox node hand-over <node> <path>` on the control-node, from the operator's own account, hands the directory
|
||||||
|
the module's condition names to the mesh; the engine on that node says so in the answer; the condition clears
|
||||||
|
at the node's next apply, which gives the directory its declared owner and mode. The same line through any
|
||||||
|
verb, or from another node's `nox`, is refused and nothing is recorded.
|
||||||
|
- No condition, verb answer, README or design document tells the operator to type the node-engine's or the
|
||||||
|
controller's binary; `merge-check.sh` here and in the catalogue fail on one that does.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- **The engine placed by hand becomes the proved build** (`mesh-host` `internal/upgrade`, `cmd/mesh-host`): on the
|
||||||
|
evidence known-good is written on — the mesh has taken a report under this build — the engine copies itself
|
||||||
|
over the fallback when the bytes differ; a copy, renamed into place, never a link. Tests: a stale fallback
|
||||||
|
becomes the proved engine's bytes with mode 0755 and the running file is untouched; one already a copy is
|
||||||
|
left as it is (same inode, no temporary file left); an engine running from the fallback itself renews nothing;
|
||||||
|
a missing fallback is placed; a link at the fallback's path is refused and its target untouched; an
|
||||||
|
unreadable running engine is said, not copied as nothing; the fallback's path is the launcher's variable or
|
||||||
|
its default. Each fails with its check taken out.
|
||||||
|
- **The hand-over is the controller's terminal's, carried to the engine** (mesh-controller
|
||||||
|
`cmd/mesh-controller/nodes.go`, `internal/link`, `internal/broker`; `mesh-host` `internal/link`,
|
||||||
|
`cmd/mesh-host`): `node hand-over <node> <path>` is a `node` subcommand that is not a read, so the controller's
|
||||||
|
terminal-only rule refuses it through every verb and through `mesh-cli` outside the terminal — tested
|
||||||
|
through `terminalOnly`, the generic command verb's composition and the ordinary mesh-cli line. The line's
|
||||||
|
node and path are judged before anything is asked (absolute, clean, no `..`, no doubled or trailing
|
||||||
|
separator; a node the mesh knows). The ask is a request on the machine's own subject. **The subject
|
||||||
|
proves nothing about who asked**: only the controller is granted a publish there (a row of the writers table,
|
||||||
|
held at composition), but the bus lets any principal allowed to answer reply to a message it received on the
|
||||||
|
reply subject that message named, so a tool server running as the operator's account could deliver a body there
|
||||||
|
(found by the review of 2026-10-09). So the controller **signs the ask** with the mesh's key — the node, the
|
||||||
|
path, who asked, an expiry a minute ahead and a fresh nonce, over a fixed context so it is never a declaration's
|
||||||
|
signature — and the engine verifies it with the key it verifies declarations with before it reads anything out
|
||||||
|
of it, then refuses an ask for another node, expired, good for too long, without a nonce of its own, or taken
|
||||||
|
before. Tests on the engine's side refuse each of those (unsigned, another key, signed as a declaration, the
|
||||||
|
bare ask, another node, expired, no expiry, too long, no or a short nonce, a replay) with the handler given
|
||||||
|
nothing; the controller's side holds the signature, the context, the nonce and the expiry. The node's grant gains
|
||||||
|
the subject and the right to answer what it was asked (the golden composition shows exactly that). The engine
|
||||||
|
then judges every value before it records anything — who asked (one short line), the path (absolute, clean,
|
||||||
|
one its state records as used as found, on disk a directory and not a link, reached through no link) — and a
|
||||||
|
refusal records nothing. **At the apply, the link is refused again at the moment of use**: a handed-over
|
||||||
|
directory must still resolve to itself, and is changed only through one descriptor opened from `/` with no
|
||||||
|
link followed on any component; a test swaps its parent, its grandparent and itself for links to a victim
|
||||||
|
tree after the hand-over, and the victim keeps its mode. The request's and the answer's field names are held by a test on each
|
||||||
|
side.
|
||||||
|
- **The words**: the engine's statement, the controller's wait and condition name
|
||||||
|
`nox node hand-over <node> <directory>` on the control-node, and never the engine's binary; the condition's
|
||||||
|
words are plain (a path is in the evidence, never in the summary). hq's `words.py` and the catalogue's
|
||||||
|
`checks/words` refuse a daemon's line (ADR 0272, its insight of 2026-10-09); each has a test of its pattern,
|
||||||
|
and `words.py` compares the catalogue's copy.
|
||||||
|
- **Live, at acceptance:** after the controller and then the engine are delivered, the four nodes' journals say
|
||||||
|
"renewed the engine placed by hand … to this build", and `nox-mesh-host version` at a machine answers the
|
||||||
|
delivered build; `nox node hand-over <control-node> <the messenger's data directory>` answers that it is handed
|
||||||
|
to the mesh, the messenger's condition clears at the next apply, and the module runs.
|
||||||
|
|
||||||
|
The diagnosis is in [01-diagnosis.md](01-diagnosis.md).
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
# Diagnosis — issue 356
|
||||||
|
|
||||||
|
## 2026-10-09, 18:01 — what was measured
|
||||||
|
|
||||||
|
On the control-node, the operator typed the node-engine's `hand-over` with the messenger's data directory and was
|
||||||
|
answered "unexpected argument". `help` from the same command listed no `hand-over`. The launcher's journal named
|
||||||
|
the engine it runs: a delivered build under the engine's versions directory. The messenger's unit crash-looped;
|
||||||
|
its data directory was used as found, owned by a machine account (uid 1000) with mode 700, while the module had
|
||||||
|
moved to an account of its own.
|
||||||
|
|
||||||
|
## The causes, from the code
|
||||||
|
|
||||||
|
**1. Nothing ever wrote a delivered engine over the one placed by hand.** The launcher (`mesh-host`
|
||||||
|
`packaging/nox-mesh-host-launch`, delivered as the engine's module file) picks the newest delivered version that
|
||||||
|
was not rolled back, and falls back to `/usr/bin/nox-mesh-host` — the file a person copied at installation
|
||||||
|
([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md)) — only while nothing is delivered.
|
||||||
|
The engine's own upgrade code (`internal/upgrade`) delivers under the versions directory, proves a build by
|
||||||
|
writing `known-good`, and retires versions older than the running one's predecessor; it never reads or writes the
|
||||||
|
fallback. The fallback is also the one `nox-mesh-host` on any `PATH`, so from the first delivery on, what a person
|
||||||
|
typed was a build older than what ran. Nothing said so: the engine's `version` answers from its own path, and the
|
||||||
|
stale file answers its own.
|
||||||
|
|
||||||
|
**2. The hand-over was a local subcommand only.** Issue 339's way out, `hand-over <dir>` (`mesh-host`
|
||||||
|
`cmd/mesh-host/found.go`), reads the node's state and writes `handed-over.json` beside it; the next apply reads
|
||||||
|
that file. It had no bus side: the engine hears the mesh on its declaration subject and on
|
||||||
|
`mesh.node.<node>.ask.report` only, and the controller had no `node hand-over`. The engine's statement, the
|
||||||
|
controller's wait text (`module_health.go`) and the condition's "needs" line all told the operator to run it "at
|
||||||
|
the machine", as root.
|
||||||
|
|
||||||
|
**3. No check held the texts to ADR 0272.** `words.py` reads the glossary's retired words; a daemon's command
|
||||||
|
line is not a word.
|
||||||
|
|
||||||
|
## Ruled out
|
||||||
|
|
||||||
|
- A delivery that failed half way (the versions directory holding an older build): the launcher's journal named
|
||||||
|
the delivered build, and `known-good` named it.
|
||||||
|
- The launcher running the fallback: it does so only while nothing is delivered, and three versions were.
|
||||||
|
|
||||||
|
## The fix
|
||||||
|
|
||||||
|
**Which fix for the stale command.** Two were weighed. (a) `/usr/bin/nox-mesh-host` becomes a small program that
|
||||||
|
runs the launcher's current version. Rejected: the launcher runs that very file as the engine while nothing is
|
||||||
|
delivered, so a shim there would run itself; and the choice of version (newest delivered, never one rolled back,
|
||||||
|
a rollback's pin) is the launcher's policy, which a shim would copy. (b) The proved engine writes itself over the
|
||||||
|
file when the bytes differ: a copy renamed into place, never a link, on the same evidence `known-good` is written
|
||||||
|
on. Chosen. The launcher's fallback is then the newest proved build, which is also the best one to fall back on,
|
||||||
|
and the command on the machine is the engine that runs it.
|
||||||
|
|
||||||
|
**The hand-over's channel.** `nox node hand-over <node> <path>` on the control-node. The controller's `node`
|
||||||
|
subcommands that are not reads are terminal-only already (ADR 0266's allow list), so the line is refused through
|
||||||
|
every verb and through `mesh-cli` from any other node without a new rule. The controller judges the line, asks
|
||||||
|
the node's engine on `mesh.node.<node>.ask.hand-over` — a request on core NATS — and prints the engine's answer,
|
||||||
|
failing on a refusal. Only the controller is granted a publish on that subject (a writers-table row), but that
|
||||||
|
does not say who the engine hears: the bus lets a principal allowed to answer reply to a message it received on
|
||||||
|
any reply subject the message named, and the tool runner, which runs as the operator's account and so as every
|
||||||
|
agent, is allowed to answer (the review of 2026-10-09 proved it on a permissioned server). So the ask is signed
|
||||||
|
with the mesh's key (node, path, who asked, a minute's expiry, a nonce; over a fixed context, so the signature is
|
||||||
|
never a declaration's), and the engine verifies it before reading anything, refusing another node's, an expired
|
||||||
|
one and a replayed nonce. The engine judges every value and records the hand-over, or refuses and records
|
||||||
|
nothing. **The apply checks again**: a component of the path may have become a link after the hand-over was
|
||||||
|
recorded, so a handed-over directory must still resolve to itself and is changed only through a descriptor opened
|
||||||
|
from `/` that followed no link. The engine's local `hand-over` stays, for a
|
||||||
|
machine no mesh can reach; its usage says the operator's line.
|
||||||
|
|
||||||
|
**The words.** Every text addressed to the operator names the `nox` line. hq's `words.py` gains the daemons'
|
||||||
|
lines (ADR 0272's insight of 2026-10-09 says the rule and the check); the catalogue's `checks/words` refuses the
|
||||||
|
same lines in module strings and manifest descriptions, from its copy `retired-lines`.
|
||||||
|
|
||||||
|
The pull requests: `mesh-host` #74 (the fallback renewed, the engine's side of the ask, its words),
|
||||||
|
mesh-controller #187 (the `node hand-over` line, the node's grant, its words),
|
||||||
|
mesh-catalog #165 (the catalogue's check and its copy of the lines), and this one (the record, `words.py`, the
|
||||||
|
documents' lines).
|
||||||
|
|
||||||
|
**Order of delivery.** The controller first: the node's bus grant (the subject, and the right to answer) is in
|
||||||
|
the controller's composition of the user list, and an engine delivered before it would have its subscription
|
||||||
|
refused and say so. Then the engine. The catalogue's check and this record in any order.
|
||||||
|
|
||||||
|
## What remains
|
||||||
|
|
||||||
|
- **A handed-over directory goes through no link on every apply, not only the first** (the confirmation review
|
||||||
|
of 2026-10-09). After the first apply the record is no longer used as found, so the directory read as the
|
||||||
|
mesh's own and was given its mode and owner by path. The hand-over is now checked before that, and a test
|
||||||
|
swaps a parent after an apply already gave the directory its owner (`mesh-host` #74).
|
||||||
|
- **The same by-path change for every other mesh directory outside a home is a gap older than this issue**, and
|
||||||
|
is left to [issue 358](../358-a-mesh-directory-outside-a-home-is-changed-by-path-through-any-link/00-report.md):
|
||||||
|
closing it here would change every placement on every node.
|
||||||
|
|
||||||
|
- A mesh-lab replay of the stale command (an engine delivered twice, the fallback compared) and of the hand-over
|
||||||
|
(asked, refused through a verb, recorded through the terminal) is owed before this issue resolves: a core
|
||||||
|
issue resolves with `replay:` ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)).
|
||||||
|
- The engine's other local subcommands (`key`, `enrol`, `overlay take`, `reconcile`, `apply`, `profile`,
|
||||||
|
`inventory`, `owned`, `bundle`, `version`) stay install-time and rescue commands, typed as root at a machine no
|
||||||
|
mesh reaches; the engine's usage and README say so. A `nox` line for any of them is a later decision.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: []
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 357. What ADR 0273 left undecided has no owner
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
[ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)
|
||||||
|
(2026-10-09: the agent's home is the module's, and the operator's rules and lessons are registered through it)
|
||||||
|
says in its consequences that four things are not decided there, and names no issue, owner or date for any of
|
||||||
|
them. The check added with
|
||||||
|
[issue 354](../354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md) found
|
||||||
|
them on the day the record was accepted. This issue is the pointer the record lacked; the record carries a
|
||||||
|
dated insight naming it.
|
||||||
|
|
||||||
|
1. **Clearing old copies of hand versions.** The agent module keeps the hand version of every file it undoes,
|
||||||
|
in its state, and "clearing old copies is not decided here, as ADR 0243 left it". Without it the module's
|
||||||
|
state only grows.
|
||||||
|
2. **A per-project lesson** — one bound to a repository rather than the operator.
|
||||||
|
3. **Lessons for an agent the mesh runs**, rather than the operator's own agent.
|
||||||
|
4. **A channel of the operator's own that can tell the operator from an agent** — what issue 340 calls
|
||||||
|
"once we build a UI" — which would become a third route for registering into the operator's home.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
A deferral nothing tracks reads as done (issue 354). Each of the four is a gap a reader of ADR 0273 can only
|
||||||
|
find by reading its consequences to the end; the first is also a store that grows without a bound.
|
||||||
|
|
||||||
|
## Open
|
||||||
|
|
||||||
|
- Whether each item needs a decision record of its own, or whether item 1 is a setting of the agent module
|
||||||
|
that its owner (the `claude-code` module in the catalogue) can decide in a change.
|
||||||
|
- Item 4 waits on the operator's channels (ADR 0259) and the controller's terminal; its owner is not known
|
||||||
|
until the channel is designed.
|
||||||
|
- `located-in:` is filled when each item's owner is confirmed. The item resolved first is struck through here
|
||||||
|
with its pull request, and the issue resolves when all four are decided or declined.
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
status: open
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-host (internal/apply/apply.go applyDirectory, internal/apply/home_links.go chmodDirPath, chownPath)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 358. A mesh directory outside a home is given its mode and owner by path, through any link in its path
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Found by the review of [issue 356](../356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)
|
||||||
|
on 2026-10-09, by reading the code; nothing was run against a node.
|
||||||
|
|
||||||
|
The node-engine sets a directory's mode and owner by path when the directory is the mesh's own and lies outside
|
||||||
|
an account's home: `applyDirectory` calls `chmodDirPath` and `own`, which call `os.Chmod` and `os.Chown` on the
|
||||||
|
path. Both follow a link anywhere in the path. The directory applier refuses a link at the directory's own name,
|
||||||
|
and issue 339's placement guard refuses the machine's own trees, but nothing refuses a link in a parent at the
|
||||||
|
moment of use. Below a home the engine already walks every component without following a link (ADR 0266).
|
||||||
|
|
||||||
|
Issue 356 closed this for a directory handed to the mesh by the operator: it is changed only through one
|
||||||
|
descriptor opened from `/` with no link followed, on every apply. The same treatment for every other mesh
|
||||||
|
directory was left out of that fix on purpose: it touches every placement on every node, and a machine whose
|
||||||
|
data lives behind a parent that is a link (a mount point reached through one, say) would be refused where it
|
||||||
|
is applied today.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
Root follows a path a parent of which another account may be able to replace. Whoever can turn a parent of a
|
||||||
|
mesh directory into a link between two applies directs root's `chmod` and `chown` at the link's target.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Not known whether any node has a mesh directory with a parent another account can write; finding that out is
|
||||||
|
the first step of the diagnosis.
|
||||||
|
|
||||||
|
## The acceptance test (the operator's view)
|
||||||
|
|
||||||
|
No apply of any node changes the mode or owner of anything but the directory its declaration names, whatever
|
||||||
|
link appears in that directory's path between applies; and every directory the mesh applies on the four nodes
|
||||||
|
today still applies.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
To be written with the fix: a test that swaps a parent of an ordinary mesh directory for a link after an apply
|
||||||
|
and finds the link's target untouched; and the guard run over the declarations every node would be sent today.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-09
|
||||||
|
located-in: [mesh-controller (cmd/mesh-controller/desk_secret.go, internal/conditions/plain.go)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 359. A secret given reached the operator as "needs a look", and nothing said why
|
||||||
|
|
||||||
|
**Severity: medium.** The one notification meant to tell the operator that a module's own secret changed told
|
||||||
|
them nothing they could act on, and the fall back to borrowed words was silent everywhere.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-09 at 23:32 (local) the operator gave a chat module's bot token at the controller's terminal, with
|
||||||
|
the controller's `secret accept` and the value on standard input. The urgent desktop notification that followed
|
||||||
|
read, in full:
|
||||||
|
|
||||||
|
> Novox needs a look — Needs you: read the details to see what to do. The mesh noticed a problem it calls
|
||||||
|
> "secret given" with novox.
|
||||||
|
|
||||||
|
That is the scope's fallback wording ([ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)),
|
||||||
|
with the verdict kept as [ADR 0258](../../02-DECISIONS/0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md)
|
||||||
|
requires. It names neither the module nor what was given, and "read the details" sends the operator elsewhere
|
||||||
|
for the one thing the condition exists to say: a secret changed, and silencing it is the answer when they gave
|
||||||
|
it themselves. The condition was raised on every channel on purpose — the security review of 2026-10-09 wanted
|
||||||
|
a bot token changed by somebody else to be heard of — so the words are the whole of its value.
|
||||||
|
|
||||||
|
## Two faults
|
||||||
|
|
||||||
|
1. **The condition's own words broke the plain rule.** Its producer wrote a headline, an explanation, a needs
|
||||||
|
line and a resolved line of its own; the explanation carried the clock time of the giving. The plain rule
|
||||||
|
refuses a clock time (the channel says when, in the operator's time), so the keeper replaced all of them with
|
||||||
|
the scope's words.
|
||||||
|
2. **The replacement was silent.** The keeper tells a hook of every observation said in borrowed words. A test
|
||||||
|
suite sets that hook and fails on it; the serving controller sets none, so nothing — no log line, no
|
||||||
|
condition — said that a producer's words had been refused, or why. The condition's own unit test built the
|
||||||
|
observation without passing it through a keeper, so the suite's check never saw it either.
|
||||||
|
|
||||||
|
## What is asked
|
||||||
|
|
||||||
|
- The secret-given words pass the plain rule, with a test that holds them to it through a keeper.
|
||||||
|
- Words refused at run time are never silent: the keeper logs the kind and the reason, once per process.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Diagnosis — issue 359
|
||||||
|
|
||||||
|
## 2026-10-09 — the cause, confirmed by a test
|
||||||
|
|
||||||
|
A test in the controller's repository built the secret-given observation as the terminal does (the module, the
|
||||||
|
secret's name, "at the controller's terminal", 23:32) and held its words to the plain rule. The answer was
|
||||||
|
`explanation: a clock time or date (23:32)`. With the time removed and nothing else changed, the same words
|
||||||
|
passed, from the terminal and from a desk. The secret's name, written with a hyphen, is not refused by the rule;
|
||||||
|
the clock time alone was the cause.
|
||||||
|
|
||||||
|
The keeper's fallback (`plainly` in the controller's conditions package) then took the scope's words: "… needs a
|
||||||
|
look", and "a problem it calls" the kind with its hyphens as spaces. Since the producer had a needs line, the
|
||||||
|
verdict stayed "Needs you:" with the fallback needs, "read the details to see what to do."
|
||||||
|
|
||||||
|
## Why nothing said so
|
||||||
|
|
||||||
|
The hook told of borrowed words (`Unworded`) is set only by test suites. In the serving controller it is unset,
|
||||||
|
and the keeper replaced the words without a trace. The suite-wide check in the controller's command package
|
||||||
|
fails any condition a test raises through a keeper with words that are not plain; the secret-given test called
|
||||||
|
the producer directly and asserted on its fields, so that check never ran on it.
|
||||||
|
|
||||||
|
## The fix
|
||||||
|
|
||||||
|
In the controller's repository, on its own pull request, linked to this issue:
|
||||||
|
|
||||||
|
- The words name the module and the machine, say the secret's name as words ("telegram token"), say how it was
|
||||||
|
given, and carry no time: "New secret given for telegram on" the machine; "If you gave it, nothing else is
|
||||||
|
needed. If you did not, somebody else now holds what telegram acts with." The needs line says silencing is the
|
||||||
|
answer when the operator just gave it. Severity (urgent) and the one answer (silence for a week) are unchanged.
|
||||||
|
- A test holds the words to the plain rule and observes them through a keeper, so the suite's check covers them;
|
||||||
|
it fails against the old words with the cause above.
|
||||||
|
- With no hook set, the keeper logs which kind is said in borrowed words and why, once per kind and reason per
|
||||||
|
process, so a refusal in the running controller is visible in its journal.
|
||||||
|
|
||||||
|
Ruled out: the bound on lengths (the old needs line was 102 characters of 120), the outward content rule (no
|
||||||
|
address or secret shape in the words), and the machine name.
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-10
|
||||||
|
located-in: [mesh-catalog modules/dunst (the dunstrc's click lines and its context menu)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 360. The operator cannot choose a notification's action on a touchpad
|
||||||
|
|
||||||
|
**Severity: medium.** The desk's answers ([ADR 0253](../../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md),
|
||||||
|
[ADR 0258](../../02-DECISIONS/0258-a-desk-click-performs-only-an-acknowledgement-and-an-answer-is-a-one-time-ticket.md))
|
||||||
|
reach the screen and cannot be given there. Every notification with actions ends unanswered, or is answered
|
||||||
|
on another channel.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
The operator works on the laptop and on the workstation with a touchpad, which gives a left and a right
|
||||||
|
click but no middle click. A desktop notification from the desk that carries actions (for example "Silence
|
||||||
|
for a week") shows dunst's mark for them, "(A)", and nothing the operator can do on it chooses one:
|
||||||
|
|
||||||
|
- a tap (left click) closes the notification, because the dunstrc the mesh writes says
|
||||||
|
`mouse_left_click = close_current`;
|
||||||
|
- the actions are bound to the middle click alone (`mouse_middle_click = do_action, close_current`), which
|
||||||
|
the touchpad cannot give;
|
||||||
|
- the context menu, the other way to an action, does not open either. The dunstrc names `dmenu`, the
|
||||||
|
node-launcher's command, which lives in the account's own `~/.local/bin`. dunst runs in the account's
|
||||||
|
service manager, and on the laptop on 2026-10-10 the running dunst's `PATH` held no such directory.
|
||||||
|
|
||||||
|
ADR 0253 states the desk's actions are offered "a middle click, or its context menu". On these two
|
||||||
|
machines neither is reachable, and nothing says so: the notification closes on a tap as if it had been read.
|
||||||
|
|
||||||
|
## Expected
|
||||||
|
|
||||||
|
On the laptop and the workstation: a tap on a notification with one action runs it; with several, a menu
|
||||||
|
(rofi) opens to choose one; a notification without actions closes on a tap, as today.
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Diagnosis — issue 360
|
||||||
|
|
||||||
|
## 2026-10-10 — what dunst does on each click, from its own documentation and code
|
||||||
|
|
||||||
|
dunst(5), for the version the machines run (1.13.2): `mouse_[left/middle/right]_click` takes a list of
|
||||||
|
`none`, `do_action`, `open_url`, `close_current`, `close_all`, `context` and `context_all`, run in order.
|
||||||
|
`do_action` "invokes the action determined by the action_name rule. If there is no such action, open the
|
||||||
|
context menu", and `action_name` defaults to `default`: "the action set as default action or the only
|
||||||
|
available action will be invoked."
|
||||||
|
|
||||||
|
dunst's source (`notification_do_action` in `src/notification.c`, the click handler in `src/input.c` and the
|
||||||
|
menu in `src/menu.c`, tag v1.13.2) settles the three cases:
|
||||||
|
|
||||||
|
| the notification has | `do_action` | then `close_current` |
|
||||||
|
|---|---|---|
|
||||||
|
| one action | runs it at once | closes it |
|
||||||
|
| several actions, none named `default` | opens the context menu for this notification and locks it | the close waits for the menu; it happens after the chosen action is sent, and not at all when the menu is dismissed |
|
||||||
|
| no action and no link | nothing | closes it |
|
||||||
|
| no action, one link | opens the link in the browser | closes it |
|
||||||
|
| several actions, one named `default` | runs `default` without a menu | closes it |
|
||||||
|
|
||||||
|
A touch on Wayland is also read as the left click (`BTN_TOUCH`).
|
||||||
|
|
||||||
|
So `mouse_left_click = do_action, close_current` gives the expected behaviour, with two edges: a link in a
|
||||||
|
notification without actions opens on a tap, and an action named `default` would be taken without the menu.
|
||||||
|
The desk's tokens are tickets, never `default`; the dunst module's `send` can refuse that token so the edge
|
||||||
|
cannot be reached.
|
||||||
|
|
||||||
|
## The context menu
|
||||||
|
|
||||||
|
The dunstrc's `dmenu = dmenu -p dunst` calls the node-launcher's dmenu-compatible command, a script the
|
||||||
|
launcher module places in the account's `~/.local/bin`. The `PATH` of the running dunst on the laptop was
|
||||||
|
read from its process environment on 2026-10-10 and held only system directories and a tool's own, not
|
||||||
|
`~/.local/bin`. dunst starts the menu with a search of that `PATH` (`g_spawn_async_with_pipes` with
|
||||||
|
`G_SPAWN_SEARCH_PATH`), and a command it cannot find is only a line in its log ("Cannot spawn dmenu"), so
|
||||||
|
the menu does not open and the operator sees nothing. Calling
|
||||||
|
`rofi -dmenu` (a system package) removes the dependency on the account's `PATH`; the dunst module then
|
||||||
|
declares the `rofi` package itself, since its file names it.
|
||||||
|
|
||||||
|
## Ruled out
|
||||||
|
|
||||||
|
- The keyboard: i3's configuration is the mesh's (the `i3` module, with other modules' lines placed from
|
||||||
|
their contributions to `node-display-session`, ADR 0212), so a key for `dunstctl context` can be one more
|
||||||
|
contribution, from the dunst module. It does not replace the tap, which is the operator's way on a
|
||||||
|
touchpad.
|
||||||
|
|
||||||
|
## Owner
|
||||||
|
|
||||||
|
mesh-catalog, `modules/dunst`: its dunstrc (the left click and the menu), its packages, its contribution to
|
||||||
|
i3, and the token check in its `send`.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-10
|
||||||
|
located-in: [mesh-host (internal/accounts, rootsearch), mesh-controller (cmd/mesh-controller/agent_account.go, internal/link)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 361. The control-node's agent account is never judged: its search for setuid programs cannot finish in fifteen minutes
|
||||||
|
|
||||||
|
**Severity: high.** The router refuses to link a messaging channel on a machine that is not root-free
|
||||||
|
([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md) §8),
|
||||||
|
and a machine whose agent account is not judged is not root-free. On the control-node the judgement can never
|
||||||
|
be given, so linking the operator's phone there is blocked for as long as the search is bounded.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-10 at 00:43 (local), after the control-node was given an agent account
|
||||||
|
([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 controller's `root-free` verb for that machine answered:
|
||||||
|
|
||||||
|
> not judged (search incomplete): the search for setuid programs did not finish within 15m0s; it is tried
|
||||||
|
> again later
|
||||||
|
|
||||||
|
The control-node holds its root and home filesystems on a software RAID of spinning disks, with millions of
|
||||||
|
files between them. The node-engine's search for setuid programs (ADR 0266 §3 item 10) walks every local
|
||||||
|
filesystem mounted without `nosuid`; that walk, cold, takes far longer than fifteen minutes there. Each attempt
|
||||||
|
was killed at its bound, judged nothing, and was tried again after a back-off, which ends the same way.
|
||||||
|
|
||||||
|
## Why it is a design fault, not one slow disk
|
||||||
|
|
||||||
|
- **A bounded search judges nothing on a machine larger than the bound.** The bound was chosen so the
|
||||||
|
controller could tell "still running" from "stuck" (`rootsearch.Bound`, read by both). It turned a slow
|
||||||
|
machine into one that is never judged, failing closed for good rather than for a while.
|
||||||
|
- **Nothing was kept across a restart.** The search's result lived in the engine's memory only, so every
|
||||||
|
engine restart (every update of the engine) started from nothing and judged nothing until a whole walk
|
||||||
|
finished again.
|
||||||
|
- **The controller's quiet was the same value.** The self-check stayed quiet while a search was "within its
|
||||||
|
bound" — fifteen minutes — so a search that runs for hours would raise the urgent condition on every
|
||||||
|
restart even once the search itself was allowed to finish.
|
||||||
|
|
||||||
|
## What is asked (the operator's direction, 2026-10-10)
|
||||||
|
|
||||||
|
1. The search runs to completion, in the background, at the lowest CPU and I/O priority, one at a time, with
|
||||||
|
no bound on the whole search.
|
||||||
|
2. It is resumable: its progress is kept every minute beside the engine's state, so a restart, an update or a
|
||||||
|
reboot carries it on from where it was rather than starting over.
|
||||||
|
3. Its outcome is kept, with when it started and finished. A verdict is given only from the last **complete**
|
||||||
|
search while it is younger than a stated age; "not judged yet" before the first one completes; never
|
||||||
|
"cannot become root" from an incomplete or stale one.
|
||||||
|
4. A fresh search runs at most once a day, and after any apply that changes an account, a group, a sudo rule or
|
||||||
|
a package.
|
||||||
|
5. The controller's own reading of the search (its quiet) changes with it, consistently.
|
||||||
|
|
||||||
|
**Why a day-old search is sound.** Only root can make a setuid- or setgid-root file: the agent account can
|
||||||
|
neither set the bit on a file root owns nor give a file of its own to root. So a complete search stays true until
|
||||||
|
root acts, and root's acts through the mesh (applies) start a new search. A file that appears behind a running
|
||||||
|
walk's cursor was made by root too. What is left is root acting by hand — the operator — which the stated age
|
||||||
|
bounds.
|
||||||
|
|
||||||
|
The trail and the fix are in [`01-diagnosis.md`](01-diagnosis.md).
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 361 — diagnosis
|
||||||
|
|
||||||
|
## 2026-10-10 — where the bound lives, and who read it
|
||||||
|
|
||||||
|
- **The node-engine** (`mesh-host`, `internal/accounts`): `SetuidCache` ran the search in the background, at most
|
||||||
|
hourly, under a context with a deadline of `rootsearch.Bound` (15 minutes) for the whole search — `find` over
|
||||||
|
every mount point with `-xdev`, then the package manager about every path found. A search past the deadline
|
||||||
|
was an error, said as `not judged (search incomplete)`, and tried again after a back-off doubling from an hour
|
||||||
|
to six. The result was kept in memory only.
|
||||||
|
- **The controller** (`mesh-controller`, `cmd/mesh-controller/agent_account.go`): `searchQuietFor` was
|
||||||
|
`link.RootSearchBound`, which is `rootsearch.Bound`. It decides only how long the self-check raises nothing
|
||||||
|
while an agent account's verdict says `not judged yet (search running)`, counted from when the controller
|
||||||
|
first saw it waiting. The `root-free` verb and `judgedConfined` never read it: they need a healthy verdict in
|
||||||
|
a statement heard within 15 minutes (`verdictFreshFor`, a different value — how often an engine states its
|
||||||
|
health — which this issue does not change).
|
||||||
|
- **Nothing else** used the bound: `SetuidCache` had one caller (`moreWays`), and the engine made one cache.
|
||||||
|
- **Ruled out:** a stuck `find` (each attempt ended at exactly its bound) and a filesystem that should not be
|
||||||
|
searched (the walk covered only the local root, home and boot filesystems, as ADR 0266 asks).
|
||||||
|
|
||||||
|
## The fix (`mesh-host` pull request 75, `mesh-controller` pull request 190)
|
||||||
|
|
||||||
|
**The walk is the engine's own and resumable.** `filepath.WalkDir` per filesystem, staying on its device as
|
||||||
|
`find -xdev` did, with the same prunes and the same match (a regular file, setuid and owned by root, or setgid
|
||||||
|
and of root's group), never following a link. It visits each directory's entries in lexical order, so its
|
||||||
|
progress is one cursor per filesystem — the last path visited — compared name by name (`/a/b` is walked before
|
||||||
|
`/a-c` although `-` sorts before `/`). Every minute the progress (when the walk started, its filesystems, those
|
||||||
|
done, the cursor, the matches so far) is written beside the engine's state as a new file renamed into place,
|
||||||
|
readable by root alone. After a restart the walk carries on from the cursor: what is before it is skipped
|
||||||
|
without being read, the directories leading back to it are read again. A walk older than the stated age, or one
|
||||||
|
an apply asked to redo, is thrown away and begun again.
|
||||||
|
|
||||||
|
**Low priority, one at a time, no bound.** The walk runs on a goroutine locked to an operating-system thread of
|
||||||
|
its own, set to nice 19 and the idle I/O class; on Linux both are a thread's, so the engine's other work keeps
|
||||||
|
its priority, and the thread ends with the goroutine. The package manager is run the same way. A priority that
|
||||||
|
cannot be lowered fails the search rather than walk at full priority. There is no deadline: the search ends when
|
||||||
|
it has walked everything, or with an error (a directory it could not read), which is incomplete.
|
||||||
|
|
||||||
|
**A verdict only from a complete, fresh search.** The last complete search is kept beside the engine's state
|
||||||
|
with its start and finish, and judges while it is younger than `rootsearch.FreshFor`, counted from its start.
|
||||||
|
Before one completes, or once it is older, the verdict is `not judged yet (search running)`; after a failure,
|
||||||
|
`not judged (search incomplete)`. A kept file is believed only as a regular file, opened without following a
|
||||||
|
link, owned by the engine's account and writable by it alone, in a directory that is the same.
|
||||||
|
|
||||||
|
**When a search runs.** Once a day (`rootsearch.Every`, from the last complete one's start); after an apply
|
||||||
|
whose outcomes moved a user, a package, an action, or a file of the user and group databases or of sudo's,
|
||||||
|
doas's or polkit's rules (`apply.MovesRoot`), at most once an hour from the last search's end, the last complete
|
||||||
|
search judging meanwhile; and at once when a filesystem appears that the last search did not walk, which that
|
||||||
|
search does not judge.
|
||||||
|
|
||||||
|
**The values, one place** (`rootsearch`, read by both repositories):
|
||||||
|
|
||||||
|
| Value | What it is | Set to |
|
||||||
|
|---|---|---|
|
||||||
|
| `Every` | how often a full search runs at most | 24 hours |
|
||||||
|
| `Quiet` | how long the controller raises nothing while an agent account waits for a search, from when it first saw it waiting | 12 hours |
|
||||||
|
| `FreshFor` | how long a complete search judges, from its start | `Every` + `Quiet` = 36 hours |
|
||||||
|
|
||||||
|
`FreshFor` is `Every` plus `Quiet` so that a daily search which finishes within the controller's quiet never
|
||||||
|
leaves an hour unjudged; the controller's test holds that relation. The operator's direction named 26 hours as an
|
||||||
|
example; 36 is chosen because a full walk at idle priority on the control-node is expected to take hours, and
|
||||||
|
with 26 hours a daily search longer than two hours would leave the account unjudged — and phone linking refused —
|
||||||
|
every day until it finished.
|
||||||
|
|
||||||
|
## 2026-10-10 — the independent review, and what it changed
|
||||||
|
|
||||||
|
An independent review of both pull requests asked four questions: can a stale or incomplete search yield "free";
|
||||||
|
can the agent account steer the kept files or the walk; does the walk hurt the machine; do the controller and the
|
||||||
|
engine agree. What it found, and what was done:
|
||||||
|
|
||||||
|
- **A cursor of a name that is not UTF-8 came back changed.** JSON strings replace such bytes, so a resumed walk
|
||||||
|
skipped every name that sorted between the real cursor and its replacement, and could finish "free" without
|
||||||
|
having read them. Any account can make such a name. The walk's paths are now kept as their bytes.
|
||||||
|
- **An ask to search again was lost to a restart**, and the restarted engine carried on the walk that started
|
||||||
|
before root acted. The ask now forgets the walk under way and stops it keeping progress.
|
||||||
|
- **A fresh search judged a filesystem it never walked.** A complete search now judges only while it walked every
|
||||||
|
filesystem a search would walk now; a filesystem mounted since makes the verdict "not judged yet" until walked.
|
||||||
|
- **A mount point inside a walked filesystem was read before it was known to be another filesystem.** A FUSE
|
||||||
|
mount root cannot read failed the walk, and a network mount whose server is away could hang it. Every mount
|
||||||
|
point the kernel lists is now left unread. Mount points are read with all the kernel's escapes.
|
||||||
|
- **A file found and gone before the package manager was asked failed every resumed search.** The walk looks at
|
||||||
|
what it found again when it ends, and drops what went or is no longer setuid-root.
|
||||||
|
- **Outcomes that moved nothing started a search**, and a search asked for could follow a walk of hours at once.
|
||||||
|
Only created, updated, corrected and removed outcomes count now, and an ask waits an hour from the last
|
||||||
|
search's end.
|
||||||
|
- **Not changed, because the mesh cannot do it:** the review asked that a file or archive declared setuid start a
|
||||||
|
search. The engine writes a file's and an archive's permission bits only, so neither carries a setuid bit.
|
||||||
|
|
||||||
|
## Open, after the fix
|
||||||
|
|
||||||
|
- **How long the first walk on the control-node takes is not measured.** Until the first complete search, the
|
||||||
|
verdict there is `not judged yet`, and linking stays refused. The engine says each search's end and how long
|
||||||
|
it ran in its journal; that line is the measurement. If a walk takes longer than `Quiet`, the self-check
|
||||||
|
raises the urgent condition, which is right: it is then not known to finish.
|
||||||
|
- **The idle I/O class is honoured only by a disk scheduler that has classes** (BFQ). Under another scheduler
|
||||||
|
the walk still runs at nice 19 but competes for the disk as any reader does.
|
||||||
|
- **An ask to search again is not kept across a restart.** The walk under way is forgotten when it is asked,
|
||||||
|
so a restarted engine does not carry an older walk on; the ask itself is lost, and the daily search covers it.
|
||||||
|
The last complete search keeps judging meanwhile, on the reasoning in the report.
|
||||||
|
- **The agent account can make the search fail, never pass.** A tree deeper than the longest path the kernel
|
||||||
|
takes, or a directory unreadable to root through FUSE, ends every walk incomplete; a directory of millions of
|
||||||
|
entries makes the engine hold them all at once. Each is "not judged" — the urgent condition, and linking
|
||||||
|
refused — never a pass. Reading by directory handle, as `find` did, would close the first.
|
||||||
|
- **A filesystem mounted only on use** (an automounted boot partition) makes the verdict "not judged yet" each
|
||||||
|
time it appears unwalked, until a walk covers it.
|
||||||
|
- **Part of the walk's cost escapes its priority**: Go's garbage collector runs on threads of normal priority,
|
||||||
|
and a software RAID's own threads do not carry the idle class to the disks.
|
||||||
|
- **ADR 0266 §3 item 10** still says "at most hourly" and "at most 15 minutes". On resolution it gains a
|
||||||
|
progressive insight naming this issue and the values above.
|
||||||
|
- **A later option, not built now:** a fanotify filesystem mark with `FAN_ATTRIB` (Linux 5.1 and later) on
|
||||||
|
every searched filesystem would report each change of mode or owner as it happens, turning the daily walk into
|
||||||
|
a true incremental search.
|
||||||
|
- **Resolution** of this core issue needs a replay in mesh-lab's register, or `replay-none:` with the reason.
|
||||||
|
|
||||||
|
## 2026-10-10 — the search shows its progress, and the operator can ask for a fresh one
|
||||||
|
|
||||||
|
- **The progress was kept and seen by nobody.** At 01:40 the operator asked how far the first walk on the
|
||||||
|
control-node had got. The walk kept its progress every minute beside the engine's state, but `root-free` said
|
||||||
|
only "not judged yet (search running), started 01:27", and no tool read further. The node-engine serves no
|
||||||
|
tools of its own, so the progress now travels where the verdict already does: the agent account's verdict
|
||||||
|
carries it as fields, and its reason gains one line that `root-free` repeats. The line holds filesystems done
|
||||||
|
of the total, the mount point being walked, the entries visited, the setuid- or setgid-root files found so far,
|
||||||
|
when the progress was last kept, and how long each finished filesystem took. Counts, times and mount points
|
||||||
|
only: the cursor and every file found are often under an account's home, and stay in the engine's state and
|
||||||
|
journal. The line moves at each checkpoint (once a minute) and is never part of what makes a verdict
|
||||||
|
"changed", so the journal does not repeat it every minute; the statement is said every minute anyway while
|
||||||
|
the account is not healthy. An entry is counted once across a resume, as it is kept with the cursor it
|
||||||
|
matches. The time a filesystem took counts only the time the engine walked it, not the time it was stopped.
|
||||||
|
- **A change by hand was not searched for until the next day.** The first complete search on the control-node
|
||||||
|
ended at about 01:48 and found two setuid-root programs no package owns, copies in the operator's home. When the
|
||||||
|
operator removes them by hand, no apply says so, and nothing searches again until the daily search. The
|
||||||
|
operator now asks for one at the controller's terminal: `nox node setuid-search <node>` on the control-node.
|
||||||
|
The controller signs the ask with the mesh's key, as it signs a hand-over, under a signing context of its own.
|
||||||
|
The engine throws away the last complete search and any walk under way or kept, and the next look starts a
|
||||||
|
full search. Until it completes, the verdict is "not judged yet", with its progress.
|
||||||
|
- **Why at the terminal alone, not a verb.** A search can never make a machine root-free wrongly: it judges only
|
||||||
|
once complete. But every ask throws a walk away. Asked again and again, it would keep the machine unjudged
|
||||||
|
for good and its disks busy, and whoever may call a verb includes agents. The operator needs the line only
|
||||||
|
after a change made by hand.
|
||||||
|
- **A find counts only where the agent account can execute it.** The operator asked how the agent account could
|
||||||
|
reach the two finds at all. It cannot: they sit in the operator's home, which is mode 0700. A setuid- or
|
||||||
|
setgid-root program no package owns now counts against an account only when that account can execute it.
|
||||||
|
Every directory from the root down must be searchable for it and the file executable for it, judged by its
|
||||||
|
uid and all its groups (owner, group or other bits as the kernel picks them). The judge errs towards a way:
|
||||||
|
a link on the path, a POSIX ACL on any component, or a mode it could not read counts as reachable.
|
||||||
|
The search still keeps every find. Reachability is read again at every judgement (one `lstat` per path
|
||||||
|
component), because the operator's account can open its own home without root and no search would see it.
|
||||||
|
A find in a home is said by count ("in an account's home"), never by path. Its path stays in the engine's
|
||||||
|
journal line that ends each search. On resolution, ADR 0266 §3 item 10 gains this as a progressive insight
|
||||||
|
beside the values above.
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
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: mesh-catalog#171, mesh-controller#197
|
||||||
|
replay: R362
|
||||||
|
amended-design: 03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 362. Two catalogue merges seconds apart left their delivery waiting for a person
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Shortly after midnight on 2026-10-10 (local time) two pull requests of the catalogue were merged
|
||||||
|
18 seconds apart: the claude-code change at 00:20:52, then the dunst change at 00:21:09. Both had passed
|
||||||
|
their checks.
|
||||||
|
|
||||||
|
`mesh-delivery.deliveries` then showed every delivery of the dunst pull request `superseded`, and the
|
||||||
|
claude-code delivery `held`, waiting for "a person: merged, and the controller opened no walk for it within
|
||||||
|
10m0s — nothing it holds follows that branch, or the merge was not heard: a person decides". The
|
||||||
|
controller's `plans` said of the walk for the catalogue's head, which carried both merges: "published; its
|
||||||
|
walk waits for mesh-delivery's word — `plans go … --why …` starts it by hand".
|
||||||
|
|
||||||
|
Both reasons were wrong. A walk had been opened, and it contained the held merge. Nothing gave it the word.
|
||||||
|
The operator's change sat undelivered for 58 minutes, until the operator's hand-act `mesh-controller.plans
|
||||||
|
go` started the walk. **No condition asked anyone to act in that time**: the hold is bounded at 24 hours
|
||||||
|
before `mesh-delivery.stalled` lists it, and the walk's own wait raised only a warning, at 00:51, which said
|
||||||
|
there was nothing to do and pointed at the ended delivery (the diagnosis's second entry; this sentence said
|
||||||
|
before that the walk's wait raised nothing).
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
A merge that passed its checks must reach its nodes without a person, and a delivery that cannot must say
|
||||||
|
so loudly and truly. Here both failed: two ordinary merges close together, which happens whenever the
|
||||||
|
operator merges a batch, stopped the mesh's delivery silently, and the reason it gave pointed away from the
|
||||||
|
cause.
|
||||||
|
|
||||||
|
## Where it lives
|
||||||
|
|
||||||
|
The diagnosis ([01-diagnosis.md](01-diagnosis.md)) traces it to two parts that each behave as written and
|
||||||
|
disagree on what a walk's commit means:
|
||||||
|
|
||||||
|
- The controller plans a merge it hears after a later merge of the same branch at the later merge's commit,
|
||||||
|
and supersedes the earlier walk (the fix for
|
||||||
|
[issue 349](../349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md)).
|
||||||
|
So the claude-code merge got a walk at the dunst merge's commit.
|
||||||
|
- mesh-delivery finds the delivery a walk walks by its walk id or by the commit it was merged as. The new
|
||||||
|
walk matched the dunst delivery, which its first walk's supersession had already ended, so no live
|
||||||
|
delivery held the walk and nothing gave it its turn. The claude-code delivery, merged as a different
|
||||||
|
commit, never got a walk and was held after ten minutes.
|
||||||
|
|
||||||
|
## Open
|
||||||
|
|
||||||
|
- How mesh-delivery learns which merges a walk carries: the controller's walk record naming every merge it
|
||||||
|
answers (its commit and the commits planned into it), or mesh-delivery reading containment itself, as it
|
||||||
|
already does to decide *covered*.
|
||||||
|
- A superseded walk whose successor matches only final deliveries: whether the successor should be adopted
|
||||||
|
as a delivery of its own, so that something always holds a waiting walk.
|
||||||
|
- A walk waiting for mesh-delivery's word past a bound, and a delivery held because no walk was opened,
|
||||||
|
raise a condition to the operator instead of waiting silently (failure must be loud).
|
||||||
|
|
||||||
|
## Decided
|
||||||
|
|
||||||
|
The operator decided the model that removes this shape, not only its bookkeeping:
|
||||||
|
[ADR 0276](../../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md). Merges are collected in a rolling merge window and each batch is walked once, with at most one
|
||||||
|
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.
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
# 362. Diagnosis
|
||||||
|
|
||||||
|
## 2026-10-10 — the trail
|
||||||
|
|
||||||
|
Times are local (UTC+2). Commits are the catalogue's.
|
||||||
|
|
||||||
|
**What the forge did.** The claude-code pull request was merged as 553b7191 at 00:20:52, and the dunst pull
|
||||||
|
request as 48bda475 at 00:21:09. 48bda475 is the branch's head and contains 553b7191.
|
||||||
|
|
||||||
|
**What the controller did** (its journal, 00:21:14 to 00:21:15):
|
||||||
|
|
||||||
|
1. It heard the dunst merge first and opened a walk at 48bda475 for dunst, waiting for mesh-delivery's word.
|
||||||
|
2. It then heard the claude-code merge, and printed "553b7191 was merged before 48bda475 (…, building): what
|
||||||
|
it moved is built from 48bda475, which contains it". This is the rule that answered
|
||||||
|
[issue 349](../349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md):
|
||||||
|
`SourceMoved` replaces the merge's commit and merge time with the newest planned merge's, when that merge
|
||||||
|
is later on the branch (`laterOnTheBranch`).
|
||||||
|
3. The second walk, also at 48bda475 and with the same merge time, superseded the first by when each was made
|
||||||
|
(`supersededBy`, `earlierOnTheBranch`), and carried claude-code and dunst. It waited for mesh-delivery's
|
||||||
|
word.
|
||||||
|
|
||||||
|
So every walk the controller opened was at 48bda475. None was at 553b7191.
|
||||||
|
|
||||||
|
**What mesh-delivery did** (`mesh-delivery.show` for both deliveries):
|
||||||
|
|
||||||
|
1. The dunst delivery (merged as 48bda475) took the first walk, matched by its commit, and became
|
||||||
|
`published` at 00:21:14.
|
||||||
|
2. The first walk's record came back superseded, and the table's `superseded` transition ended the dunst
|
||||||
|
delivery at 00:21:15 ("a newer delivery to the same trunk took over its walk").
|
||||||
|
3. The second walk was matched to a delivery by `deliveryWalking`, which looks first for a delivery holding
|
||||||
|
that walk id and then for any delivery whose `merged_as` is the walk's commit, final or not. That is the
|
||||||
|
dunst delivery again, already final. The walk was linked there, so it was never kept as unmatched and
|
||||||
|
never adopted as a delivery of its own. A turn (`turn`) is given only to a `published` or `delivering`
|
||||||
|
delivery, so nothing ever gave the second walk its word.
|
||||||
|
4. The claude-code delivery, merged as 553b7191, matched no walk. Ten minutes after its merge
|
||||||
|
(`unwalkedPast`) it was held for a person at 00:30:58, with a reason that names two causes, neither of
|
||||||
|
which happened.
|
||||||
|
|
||||||
|
After the operator's `plans go`, the second walk's machine steps (claude-code and dunst, each on a node) were recorded on the superseded dunst delivery, which confirms that mesh-delivery reads that walk as
|
||||||
|
the dunst delivery's.
|
||||||
|
|
||||||
|
**Why nothing was raised.** A delivery held for a person is bounded at 24 hours before
|
||||||
|
`mesh-delivery.stalled` lists it (`Bounds` in its table). The walk waiting for mesh-delivery's word is
|
||||||
|
counted against the controller's plan wait bound of 30 minutes for `status`, but no condition reached the
|
||||||
|
operator in the 58 minutes it waited. I did not trace why the controller's side stayed quiet; that belongs
|
||||||
|
to the fix.
|
||||||
|
|
||||||
|
## Ruled out
|
||||||
|
|
||||||
|
- **The merge was not heard.** The controller's journal shows it was heard and planned within 23 seconds.
|
||||||
|
- **Nothing follows that branch.** claude-code follows the catalogue's trunk; the second walk built it.
|
||||||
|
- **A failed check.** Both heads passed the merge gate and the repository's check.
|
||||||
|
|
||||||
|
## Located
|
||||||
|
|
||||||
|
Two parts, each correct by its own rule, disagree on what a walk's commit means. The controller (since issue
|
||||||
|
349) plans an earlier merge at a later commit that contains it; mesh-delivery assumes a walk's commit is the
|
||||||
|
merge commit of exactly one delivery. `located-in` names both. The fix is to make the walk say which merges
|
||||||
|
it answers, or to make mesh-delivery match by containment, and to let no waiting walk be held by nothing.
|
||||||
|
|
||||||
|
## 2026-10-10 — what the controller's side said
|
||||||
|
|
||||||
|
The first entry says no condition reached the operator in the 58 minutes. The controller's condition history
|
||||||
|
says more precisely what happened. S16 (a walk waiting for its delivery's word) raised
|
||||||
|
`plan.plan-1791584475080493383.waiting` at 00:51:32, thirty minutes after the walk opened, as its bound says.
|
||||||
|
It was a warning, its explanation was "Nothing for you to do … It becomes a question for you if it still waits
|
||||||
|
after 4 hours", and its summary sent the reader to "`mesh-delivery.show` for the delivery that landed as
|
||||||
|
48bda475", which is the dunst delivery the supersession had already ended. It cleared at 01:20:32, when the
|
||||||
|
operator's `plans go` started the walk.
|
||||||
|
|
||||||
|
So the controller's side was not silent; it was mild and pointed at the wrong delivery. S16 cannot tell a
|
||||||
|
group member waiting its turn from a walk nobody holds; only mesh-delivery knows that. That is why ADR 0276
|
||||||
|
decision 9 has mesh-delivery list a waiting walk no live delivery holds through `stalled` at once, and has
|
||||||
|
S16 name the merges a walk answers.
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-10
|
||||||
|
located-in: [mesh-controller internal/builder (what a build says it was made from, and what an image compiling Go is handed), mesh-controller cmd/mesh-controller (the planner's mapping, touchedBy and readsFrom), mesh-controller internal/inventory (the packages edge), mesh-catalog modules/route-proxy and modules/build-agent (the package each recipe compiles)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 363. ADR 0267 was decided and never built
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
[ADR 0267](../../02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md)
|
||||||
|
(accepted 2026-10-08) decided that a merge moves only the artifacts whose build source it touches, and a
|
||||||
|
module only along build dependencies on artifacts. [To-be 47](../../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md)
|
||||||
|
and [to-be 10](../../03-DESIGN/01-to-be/10-delivery.md) were amended to name it. Its consequences say "the
|
||||||
|
build handoff opens on mesh-controller and the catalogue".
|
||||||
|
|
||||||
|
On 2026-10-10 the operator took the rule as built and live. It is not:
|
||||||
|
|
||||||
|
- **The code**, at the controller's trunk on 2026-10-10: `mergeCandidates` and `readsFrom` still move a module
|
||||||
|
whose build reads a context on any merge to that repository, whatever the files; `touchedBy` still reads
|
||||||
|
every file of a repository as the module built from its root; `dependencies.go` still draws a `packages`
|
||||||
|
edge; the builder still fingerprints a context's whole tree. The planner tests still hold the rows tagged
|
||||||
|
*issue 338* as today's behaviour.
|
||||||
|
- **The plans.** Every merge to the controller's repository still plans the controller, the build seat's
|
||||||
|
holder and the route proxy, in three gated tiers, sent to every machine running them — for a change to the
|
||||||
|
controller's own command as for one to its README. This is
|
||||||
|
issue 338 (hq pull request #231, still open), unchanged.
|
||||||
|
- **The record.** No pull request in any repository names ADR 0267, and no issue tracks its build. ADR 0267's
|
||||||
|
*How this is checked* table names tests that do not exist.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
It is the pattern of [issue 354](../354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||||
|
in its decision form: a record accepted, its designs amended, and the work it hands to code repositories
|
||||||
|
started by nobody. Everything a reader meets — the record, the two designs, the glossary's *build plan* —
|
||||||
|
describes the rule, and nothing says it is not running. The operator plans against a mesh that does not
|
||||||
|
exist: a controller merge costs three gates (about 15 minutes, against 4 to 5 for its own tier, as ADR 0267
|
||||||
|
measured) and recreates the route proxy on its node for changes that never touch its program.
|
||||||
|
|
||||||
|
Issue 354's check refuses a record that defers work without an issue. ADR 0267 defers nothing in words — it
|
||||||
|
says what is built and by whom — so that check does not see it. A decision handed to a code repository with
|
||||||
|
no pull request and no issue is the same silence.
|
||||||
|
|
||||||
|
## The fix
|
||||||
|
|
||||||
|
The build of ADR 0267, in the order its records require (each its own pull request, linked here):
|
||||||
|
|
||||||
|
1. **mesh-controller, the builder.** A build says its build source per repository: a Go program's import
|
||||||
|
closure (derived, never narrower than `go list -deps`), an archive's directory, an image's recipe and the
|
||||||
|
package it compiles (the new artifact field `compiles`). An image naming that package is handed its build
|
||||||
|
source and nothing else, so a recipe reading past it fails by name; its fingerprint is over that source.
|
||||||
|
Said only for a build of a commit on the trunk.
|
||||||
|
2. **mesh-controller, the planner.** A changed file moves the modules whose recorded build source holds it;
|
||||||
|
a module with none recorded, or with an earlier merge's build still pending, is read as before. The
|
||||||
|
`packages` edge is no longer drawn and neither widens nor orders a plan.
|
||||||
|
3. **mesh-catalog.** The route proxy and the build seat's holder name the package their recipes compile.
|
||||||
|
Merged after (1) is live: a builder that predates the field refuses the manifest.
|
||||||
|
|
||||||
|
What ADR 0267 decides and this does not build yet — artifact-grain edges in the inventory, a dependent left
|
||||||
|
out when its base is rebuilt to the same digest, declared build-source paths for an artifact without a
|
||||||
|
reader, and the catalogue check refusing a whole-repository build source — stays open here, each to be built
|
||||||
|
on its own pull request.
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
status: located
|
||||||
|
opened: 2026-10-10
|
||||||
|
located-in: [mesh-controller cmd/mesh-controller (check.go moduleCheckFor), mesh-controller internal/builder (check.go gateLayer)]
|
||||||
|
fixed-by:
|
||||||
|
amended-design:
|
||||||
|
---
|
||||||
|
|
||||||
|
# 364. The merge gate refuses a module that uses a seat another module declares
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
On 2026-10-10 the catalogue's pull request for the agent module's proposed instructions
|
||||||
|
(ADR 0274, still on its own pull request, hq 268, so not linked here)
|
||||||
|
added `uses: [operator-channel]` to the module's manifest, so the module could ask the operator. Its
|
||||||
|
`mesh/merge-gate` failed: "a manifest the change touches fails the module check: claude-code uses the seat
|
||||||
|
"operator-channel", which no module declares and the mesh does not define". Its `mesh/repo-check`, which runs the
|
||||||
|
same module check over every manifest of the catalogue, passed. The router's module declares that seat.
|
||||||
|
|
||||||
|
## Why it is an issue
|
||||||
|
|
||||||
|
The gate's first step runs the controller's `module check` over **the manifests the change touches** and nothing
|
||||||
|
else. The rules between manifests then run over that set, so a seat another module declares is "declared by no
|
||||||
|
module". The command's own documentation says the opposite: "A seat another module declares is unknown unless that
|
||||||
|
module's manifest is passed too. Both are printed as a note, not as a problem — a check that refused what it could
|
||||||
|
not see would teach people to ignore it." The code refuses it.
|
||||||
|
|
||||||
|
So **no module can start using a seat a module declares**, the operator channel's above all (ADR 0259 says any module
|
||||||
|
asks through it), and no channel can claim a kind of the channel bench, unless the pull request also touches the
|
||||||
|
declaring module's manifest. The searched literal text was in no record.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Every module that becomes an asker is blocked at the gate. The first is the agent module's proposals (ADR 0274).
|
||||||
|
|
||||||
|
## Cause
|
||||||
|
|
||||||
|
`moduleCheckFor` counts every problem `CatalogueProblems` gives as a failure, and the gate gives it only the touched
|
||||||
|
manifests.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
The gate says it gives only some of a repository's manifests (an environment variable the judge reads); the module
|
||||||
|
check then says a seat none of them declares as a note. Given every manifest, as the catalogue's own check and
|
||||||
|
registration do, it stays a refusal. A judge that predates the variable ignores it and refuses as before, so the fix
|
||||||
|
takes effect once the controller that carries it is the one the mesh runs. In the controller's pull request for this
|
||||||
|
issue, with its tests: the module check over one manifest that uses a module's seat refuses it, and notes it when told
|
||||||
|
it was given only some.
|
||||||
+10
-5
@@ -22,22 +22,27 @@ both.
|
|||||||
|
|
||||||
```
|
```
|
||||||
NNN-short-name/
|
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
|
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`
|
## Frontmatter, on `00-report.md`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
status: open | diagnosing | located | resolved | wontfix
|
|
||||||
opened: YYYY-MM-DD
|
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
|
## Rules
|
||||||
|
|
||||||
- Anyone may open an issue. No localisation is required to report one.
|
- Anyone may open an issue. No localisation is required to report one.
|
||||||
|
|||||||
@@ -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` |
|
| 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 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 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) |
|
| Which repo owns what code? | [`00-META/repos.md`](00-META/repos.md) |
|
||||||
| Cross-cutting status view? | the `hq-status` skill (generated, never stored) |
|
| Cross-cutting status view? | the `hq-status` skill (generated, never stored) |
|
||||||
|
|
||||||
Statuses live **only** in frontmatter; follow the pointers there (`decisions:`, `code:`,
|
Statuses of research, designs and decisions live **only** in frontmatter; follow the pointers there
|
||||||
`became:`, `fixed-by:`) instead of reconstructing history from memory.
|
(`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
|
## 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)).
|
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.
|
No new top-level folders without explicit confirmation.
|
||||||
- **Status lives in YAML frontmatter** — on research overviews (`status`, `became`), design
|
- **Status lives in YAML frontmatter** — on research overviews (`status`, `became`), design
|
||||||
docs (`layer`, `status`, `code`, `updated`), issue reports (`status`, `located-in`,
|
docs (`layer`, `status`, `code`, `updated`) and decision records (`status`, `date`, `deciders`).
|
||||||
`fixed-by`, `amended-design`) 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.
|
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
|
- **`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
|
what was decided, the options weighed, or a consequence another record relies on. Fixing a broken
|
||||||
|
|||||||
Reference in New Issue
Block a user