diff --git a/00-META/checks/README.md b/00-META/checks/README.md index 50e6ba0c..525cbeca 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -62,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 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)); -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 diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py index 0164ada4..becc8725 100644 --- a/00-META/checks/cycle.py +++ b/00-META/checks/cycle.py @@ -56,6 +56,75 @@ CORE = re.compile(r"\b(mesh-controller|mesh-host|mesh-tools|mesh-sdk|node-tools| REPLAYS_FROM = "2026-10-07" REPLAY_ID = re.compile(r"^R\d+\b") +# A decision that defers work names the issue that tracks it (issue 354). ADR 0244 left the code +# renames of the node-engine and the tool runner "for a later rename", with no issue, owner or date; +# 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): return os.path.relpath(path, ROOT) @@ -233,6 +302,38 @@ def main(): 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") + # ---- 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 = ( len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md"))) + len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))) diff --git a/00-META/glossary.md b/00-META/glossary.md index f9292dd4..aba13671 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -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 description that means the node-engine by it is found by review. *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 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 ([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. *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 is not a third thing beside the store and the bus — it *is* those two, named together. *Not:* ~~substrate~~ @@ -499,6 +501,15 @@ 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. - **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. +- **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 diff --git a/00-META/process/02-graduation.md b/00-META/process/02-graduation.md index 487c293e..05db4aec 100644 --- a/00-META/process/02-graduation.md +++ b/00-META/process/02-graduation.md @@ -13,6 +13,13 @@ 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, 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: ```yaml diff --git a/00-META/process/04-build-handoff.md b/00-META/process/04-build-handoff.md index 4c26bafc..8399f81e 100644 --- a/00-META/process/04-build-handoff.md +++ b/00-META/process/04-build-handoff.md @@ -24,6 +24,10 @@ ## Rules - 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 request. - Work in an isolated worktree, never a shared checkout. A failed `cd` in a shared checkout diff --git a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md new file mode 100644 index 00000000..f0930b12 --- /dev/null +++ b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/00-overview.md @@ -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. diff --git a/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md new file mode 100644 index 00000000..bd74b609 --- /dev/null +++ b/01-RESEARCH/040-the-agents-home-is-the-modules-and-the-operators-rules-and-lessons/01-what-the-home-holds-and-the-options.md @@ -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. diff --git a/02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md b/02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md index 99a94906..4126719d 100644 --- a/02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md +++ b/02-DECISIONS/0216-the-agents-configuration-is-registered-through-its-module-at-three-scopes-and-served-as-one-plugin.md @@ -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 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.** - A status tool lists the home's items and says which the mesh placed. It also names any that diff --git a/02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md b/02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md index a76d180f..1216a05c 100644 --- a/02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md +++ b/02-DECISIONS/0243-the-agent-module-removes-a-home-item-it-did-not-place-only-on-the-persons-word-and-keeps-a-copy.md @@ -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 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 - An item the mesh did not place can now leave a machine with its reason recorded and its content diff --git a/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md b/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md index 316d491d..8ec4d1f8 100644 --- a/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md +++ b/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md @@ -182,6 +182,18 @@ work: descriptions call a build request an *ask*; - 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 - **Machine as the one word, node only as an identifier** — the research's proposal, and what the newest diff --git a/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 b/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 index 8773bb34..4670a6e5 100644 --- a/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 +++ b/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 @@ -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 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 > 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 diff --git a/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md b/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md new file mode 100644 index 00000000..3e3a810a --- /dev/null +++ b/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md @@ -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 ''`, 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 ". **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. diff --git a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md index 9d1e0ff8..ec2087b1 100644 --- a/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md +++ b/03-DESIGN/01-to-be/36-the-operators-agent-on-a-machine.md @@ -4,6 +4,7 @@ status: in-progress code: [mesh-catalog modules/claude-code] updated: 2026-10-09 decisions: + - 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/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 @@ -63,12 +64,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 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 -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 -settings, their own rules, skills and tool servers are never read or written by the mesh. **The six -predecessor files are the operator's to remove, once, on each workstation**; the module's documentation -lists them, and until they go the agent reads stale instructions beside the mesh's. +`0700` — and declares it, so the mesh refuses a second module owning it. + +> **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).** +> This paragraph said the module owns only the credentials file inside the directory and holds every other +> path as found, the person's own rules and skills included. Now **every configuration file under the +> 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 @@ -244,28 +254,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 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; - **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 - written there by hand. +- **home:** the agent account's own agent directory on one machine, where the item sits as if + 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: -the operator's `managed_settings` setting (§2), then the mesh scope, then the node scope, then the -mesh's own keys. +Settings take the mesh and node scopes into the managed settings file, laid in this order: the operator's +`managed_settings` setting (§2), then the mesh scope, then the node scope, then the mesh's own keys. The +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 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 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 -only those (ADR 0182). It refuses to register a name the person already uses there. +**The home is the module's** (ADR 0273, replacing "the home scope owns only what it placed"). The module +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)). -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 -state and logs the removal, and can put the item back. +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)) +is now the operator's answer to a file nobody rendered (§10). On the person's word, with a reason, the +module removes it, keeps a checked copy in its own state, logs the removal, and can put the item back. **The tools.** @@ -281,7 +300,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 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 @@ -363,6 +382,121 @@ 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 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/.md` | one personal rule each | +| `skills//`, `commands/.md`, `agents/.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/.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 ''`, 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. + ## How it is checked | Check | Defends | @@ -373,8 +507,18 @@ 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 | | 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 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 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 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 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 | | live: a skill registered at the mesh scope is offered as `nox-mesh:` 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 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 +536,8 @@ a login). 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. - **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). - **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)). The managed files and the credential write are tools of this module that runtime serves. Until the diff --git a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md index ddd912b4..78e17ae7 100644 --- a/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md +++ b/03-DESIGN/01-to-be/40-building-the-operators-agent-and-its-licence-manager.md @@ -1,9 +1,10 @@ --- layer: to-be -status: implemented +status: in-progress code: [mesh-catalog modules/claude-code, mesh-catalog modules/claude-licence-manager] -updated: 2026-10-04 +updated: 2026-10-09 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/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 @@ -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 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 ''` 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 - **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 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 -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. diff --git a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md index 7b0a161f..09e189c9 100644 --- a/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md +++ b/04-ISSUES/340-an-agent-can-plant-a-hook-in-every-agent-session-on-the-mesh/00-report.md @@ -3,7 +3,7 @@ status: located opened: 2026-10-09 located-in: [mesh-catalog (modules/claude-code), mesh-controller (internal/catalogue/terminal_keys.go)] 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 diff --git a/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md new file mode 100644 index 00000000..295218b2 --- /dev/null +++ b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md @@ -0,0 +1,75 @@ +--- +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/` 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; every hand-act record of the day + names its caller as `.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 ` 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. diff --git a/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md new file mode 100644 index 00000000..7cc547c3 --- /dev/null +++ b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/01-diagnosis.md @@ -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/`. 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-tools"), and the caller's address every hand-act record keeps, + `.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 ` 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. diff --git a/04-ISSUES/355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md b/04-ISSUES/355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md new file mode 100644 index 00000000..a5a9d3f1 --- /dev/null +++ b/04-ISSUES/355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md @@ -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. diff --git a/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md b/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md new file mode 100644 index 00000000..6edec2c9 --- /dev/null +++ b/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md @@ -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.