From 94fee5d84912efb85ef0b2c9998d06a2d7f7ba2b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 17 Sep 2026 22:24:57 +0200 Subject: [PATCH 1/2] Fix what the records review found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glossary's authority page still named the controller's seat the-controller in two entries, contradicting its own seat section after ADR 0079; issue 058's heading kept the pre-renumber 059; 055's fixed-by named branches that stop existing after merge (now merge commits/PRs) and its located-in listed file paths where the convention wants repos; 056's located-in named mesh-host, which received no fix, instead of mesh-catalog; and the design layer never said the one-store/one-broker property is enforced — 07-the-foundation and the installation table now state the seats. https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- 00-META/glossary.md | 4 ++-- 03-DESIGN/01-to-be/07-the-foundation.md | 6 ++++++ 03-DESIGN/01-to-be/21-the-installation-in-full.md | 4 ++-- .../00-report.md | 7 +++---- .../00-report.md | 2 +- .../00-report.md | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/00-META/glossary.md b/00-META/glossary.md index 9fb7ddd..7d38f6d 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -9,7 +9,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d - **node** — a machine in the mesh. There are 0..n of them, and each runs the host agent. A node is just a machine that has joined; being one implies nothing about what it runs. -- **control-node** — the one node that also holds the `the-controller` seat. There is exactly one +- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one per mesh. "control-node" is not a separate kind of machine — it is a node that additionally runs the controller (and, today, the foundation). Lose it and the other nodes keep running what they were last told; they simply cannot be told anything new. @@ -21,7 +21,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d - **controller** — the component that decides what each node should be, holds the mesh's records, and tells nodes over the broker. Replaces **"control plane"** (borrowed from networking's control-plane/data-plane, and opaque here). -- **mesh-controller** — the module that runs the controller. It **claims** the `the-controller` +- **mesh-controller** — the module that runs the controller. It **claims** the `mesh-controller` seat at mesh scope, which is what makes it singular. Replaces the module name **`mesh-control`**. (The git repository has been renamed `mesh-control` -> `mesh-controller` on the forge; the module, container and image it produces are `mesh-controller`.) diff --git a/03-DESIGN/01-to-be/07-the-foundation.md b/03-DESIGN/01-to-be/07-the-foundation.md index f0cb231..87d7751 100644 --- a/03-DESIGN/01-to-be/07-the-foundation.md +++ b/03-DESIGN/01-to-be/07-the-foundation.md @@ -23,6 +23,12 @@ decisions: # The foundation +**One store, one broker — enforced, not conventional.** Each foundation module claims a +mesh-scoped seat named after its server (`mesh-store`, `mesh-broker`, `mesh-controller` — +[ADR 0079](../../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)), so +assigning a second holder anywhere in the mesh is refused at resolution rather than silently +raising a second server. + Tier 1. Defined the same way [the controller](06-the-controller.md) is, because the same gap applied: the word was load-bearing and unpinned. diff --git a/03-DESIGN/01-to-be/21-the-installation-in-full.md b/03-DESIGN/01-to-be/21-the-installation-in-full.md index 0e7e170..91c5dae 100644 --- a/03-DESIGN/01-to-be/21-the-installation-in-full.md +++ b/03-DESIGN/01-to-be/21-the-installation-in-full.md @@ -129,8 +129,8 @@ two. | # | module | provides | note | |---|---|---|---| -| 1 | `postgres` | `postgres-database` | **the controller's own records and every module's.** One server, not two | -| 2 | `lavinmq` | `amqp` | the broker every node dials, and what modules are granted vhosts on | +| 1 | `postgres` | `postgres-database` | **the controller's own records and every module's.** One server, not two — it *claims* the mesh-scoped `mesh-store` seat, so a second is refused | +| 2 | `lavinmq` | `amqp` | the broker every node dials, and what modules are granted vhosts on — *claims* `mesh-broker`, one per mesh | | 3 | `mesh-controller` | *claims* `mesh-controller` | decides what runs where | | 4 | `distribution` | `artifact-store` | what the mesh built, pinned by digest — the module is the software (Distribution), the provision is the job | | 5 | `builder` | — | turns source into artifacts | diff --git a/04-ISSUES/055-the-adopted-store-and-broker-may-be-reachable-on-one-node-only/00-report.md b/04-ISSUES/055-the-adopted-store-and-broker-may-be-reachable-on-one-node-only/00-report.md index 8cb1ea0..47adb8f 100644 --- a/04-ISSUES/055-the-adopted-store-and-broker-may-be-reachable-on-one-node-only/00-report.md +++ b/04-ISSUES/055-the-adopted-store-and-broker-may-be-reachable-on-one-node-only/00-report.md @@ -2,10 +2,9 @@ status: resolved opened: 2026-09-16 located-in: - - mesh-controller/cmd/mesh-controller/modules.go - - mesh-controller/cmd/mesh-controller/build.go - - mesh-catalog/modules/lavinmq/module.json -fixed-by: mesh-controller multi-node/broker-reaches-over-overlay; mesh-catalog fix/broker-declares-amqps-port + - mesh-controller + - mesh-catalog +fixed-by: mesh-controller PR 27 (5718add); mesh-catalog PR 24 (a24362b) amended-design: --- diff --git a/04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md b/04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md index 865175f..b7be59c 100644 --- a/04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md +++ b/04-ISSUES/056-an-adopted-module-assigned-to-a-second-node-raises-a-second-server/00-report.md @@ -1,7 +1,7 @@ --- status: resolved opened: 2026-09-17 -located-in: [mesh-controller, mesh-host] +located-in: [mesh-controller, mesh-catalog] fixed-by: mesh-catalog + mesh-controller (the foundation modules claim mesh-scoped seats) amended-design: 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md --- diff --git a/04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md b/04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md index 1adf39d..62feb5c 100644 --- a/04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md +++ b/04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md @@ -6,7 +6,7 @@ fixed-by: amended-design: --- -# 059 — A provisioner runtime crash-loops until the overlay is up +# 058 — A provisioner runtime crash-loops until the overlay is up ## Symptom From becae7ba51b791af67e71ee9cbe067df0d658136 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 17 Sep 2026 22:28:03 +0200 Subject: [PATCH 2/2] ADR 0080: the development cycle is checked, not trusted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flow the process overview draws — idea/symptom -> decision -> to-be design -> code -> as-is — was enforced by nothing. cycle.py now refuses a to-be design naming no decision, an in-progress/implemented design naming no owning code, a located/fixed issue with no owner, a fixed/resolved issue with no fix, and a graduated research overview that does not say what it became. AGENTS.md carries the cycle and a where-to-look table so a fresh session (or a cleared context) finds the chain in frontmatter instead of assuming it. Grounding the check surfaced two real gaps, fixed here: the work-ahead design named no owning code, and research 003 listed one became target twice. https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- 00-META/checks/README.md | 7 + 00-META/checks/cycle.py | 152 ++++++++++++++++++ 00-META/process/00-overview.md | 11 ++ .../003-service-supervision/00-overview.md | 1 - .../0080-the-development-cycle-is-checked.md | 55 +++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/22-the-work-ahead.md | 5 + AGENTS.md | 27 ++++ 8 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 00-META/checks/cycle.py create mode 100644 02-DECISIONS/0080-the-development-cycle-is-checked.md diff --git a/00-META/checks/README.md b/00-META/checks/README.md index 7189f02..f8ff5cd 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -48,3 +48,10 @@ what to do. State what incident it would have caught, and make it fail before you make it pass. A check whose failure has never been observed is a guess about its own correctness. + +## cycle.py + +The development cycle, checked ([ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md)): +a to-be design names a decision, an in-progress/implemented design names its owning code, a +located/fixed issue names its owner, a fixed/resolved issue says what fixed it, a graduated +research overview says what it became. `python3 00-META/checks/cycle.py` diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py new file mode 100644 index 0000000..d946c57 --- /dev/null +++ b/00-META/checks/cycle.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""The development cycle, checked. + +The knowledge flow (00-META/process/00-overview.md) says work moves idea -> research -> +decision -> to-be design -> code, and symptom -> issue -> diagnosis -> fix. Those are rules, +and a rule states how it is checked (AGENTS.md) -- this is how. Everything here reads only +frontmatter, because status lives in frontmatter and nowhere else. + +What is enforced: + + design every 03-DESIGN doc parses, carries `layer:` matching its directory, and a + known `status:`. A TO-BE doc names at least one decision (`decisions:`) -- no + design without a decision -- and once `in-progress` or `implemented` it names + its owning code (`code:`) -- no development without a design that says where. + issues a known `status:`; once `located` or `fixed`, `located-in:` names the owner; + once `fixed` or `resolved`, `fixed-by:` says what fixed it (prose counts -- + "nothing, the capability existed" is an answer). + research a known `status:`; a `graduated` overview says what it `became:`, and every + target it names exists. + +Deliberately NOT enforced: `resolved` issues may leave `located-in` empty (a symptom that +turned out not to be a defect has no owner), and as-is docs need no decisions (they +describe what exists, not what was decided). + + python3 00-META/checks/cycle.py +""" + +import glob +import os +import re +import sys + +ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) + +DESIGN_STATUSES = {"proposed", "designed", "in-progress", "implemented", "abandoned"} +ISSUE_STATUSES = {"open", "diagnosing", "located", "fixed", "resolved"} +RESEARCH_STATUSES = {"active", "graduated", "abandoned"} + + +def rel(path): + return os.path.relpath(path, ROOT) + + +def frontmatter(path): + """The YAML block between the first two --- lines, as {key: raw-value-string}. + + Minimal on purpose, like records.py: enough for the fields these checks read. A list + value (block or inline) is joined into its items; a scalar stays a string. + """ + text = open(path, encoding="utf-8").read() + m = re.match(r"^---\n(.*?)\n---", text, re.S) + if not m: + return None + front, out, key = m.group(1), {}, None + for line in front.split("\n"): + item = re.match(r"^\s+-\s*(.+?)\s*$", line) + if item and key: + out[key].append(item.group(1)) + continue + kv = re.match(r"^([A-Za-z-]+):\s*(.*)$", line) + if not kv: + continue + key, value = kv.group(1), kv.group(2).strip() + if value.startswith("[") and value.endswith("]"): + out[key] = [v.strip() for v in value[1:-1].split(",") if v.strip()] + elif value == "": + out[key] = [] # a block list may follow; stays [] if nothing does + else: + out[key] = value + return out + + +def listy(front, key): + v = front.get(key) + if v is None: + return [] + return v if isinstance(v, list) else ([v] if str(v).strip() else []) + + +def main(): + failures = [] + + def bad(path, why): + failures.append(" %s: %s" % (rel(path), why)) + + # ---- design ------------------------------------------------------------------------ + for layer, name in (("00-as-is", "as-is"), ("01-to-be", "to-be")): + for path in sorted(glob.glob(os.path.join(ROOT, "03-DESIGN", layer, "*.md"))): + if os.path.basename(path) == "README.md": + continue + front = frontmatter(path) + if front is None: + bad(path, "no frontmatter") + continue + if front.get("layer") != name: + bad(path, "layer is %r; this directory is %s" % (front.get("layer"), name)) + status = front.get("status") + if status not in DESIGN_STATUSES: + bad(path, "status %r is not one of %s" % (status, sorted(DESIGN_STATUSES))) + if name == "to-be": + if not listy(front, "decisions"): + bad(path, "names no decisions -- no design without a decision") + if status in ("in-progress", "implemented") and not listy(front, "code"): + bad(path, "status %s but code: names no owner -- no development " + "without a design that says where" % status) + + # ---- issues ------------------------------------------------------------------------ + for path in sorted(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))): + front = frontmatter(path) + if front is None: + bad(path, "no frontmatter") + continue + status = front.get("status") + if status not in ISSUE_STATUSES: + bad(path, "status %r is not one of %s" % (status, sorted(ISSUE_STATUSES))) + if status in ("located", "fixed") and not listy(front, "located-in"): + bad(path, "status %s but located-in is empty" % status) + if status in ("fixed", "resolved") and not listy(front, "fixed-by"): + bad(path, "status %s but fixed-by says nothing" % status) + + # ---- research ---------------------------------------------------------------------- + for path in sorted(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))): + front = frontmatter(path) + if front is None: + bad(path, "no frontmatter") + continue + status = front.get("status") + if status not in RESEARCH_STATUSES: + bad(path, "status %r is not one of %s" % (status, sorted(RESEARCH_STATUSES))) + if status == "graduated": + became = listy(front, "became") + if not became: + bad(path, "graduated but became: names nothing") + for target in became: + if not os.path.exists(os.path.join(ROOT, target)): + bad(path, "became names %s, which does not exist" % target) + + checked = ( + len(glob.glob(os.path.join(ROOT, "03-DESIGN", "0*", "*.md"))) + + len(glob.glob(os.path.join(ROOT, "04-ISSUES", "*", "00-report.md"))) + + len(glob.glob(os.path.join(ROOT, "01-RESEARCH", "*", "00-overview.md"))) + ) + if failures: + print("cycle: %d document(s) break the development cycle:" % len(failures)) + print("\n".join(failures)) + return 1 + print("cycle: %d documents checked, the chain holds" % checked) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/00-META/process/00-overview.md b/00-META/process/00-overview.md index ba54ecf..1a50a69 100644 --- a/00-META/process/00-overview.md +++ b/00-META/process/00-overview.md @@ -52,6 +52,17 @@ the expensive half. | [06](06-writing-a-module.md) | Writing a module | Something that runs today must run on the mesh | | [07](07-feature-branches.md) | Feature branches across repos | Work that changes code, in one repo or several at once | +## The cycle is checked + +The flow above is a rule, and a rule states how it is checked: +[`00-META/checks/cycle.py`](../checks/cycle.py) refuses a to-be design that names no +decision, an `in-progress`/`implemented` design that names no owning code, an issue marked +`located`/`fixed` with no owner or `fixed`/`resolved` with no fix, and a `graduated` +research overview that does not say what it became. Run it with `records.py` and `index.py` +before any HQ merge. What the checks cannot see — that code work actually started from a +handoff — is held by playbooks [04](04-build-handoff.md) and [07](07-feature-branches.md): +a feature branch exists because a design or an issue sent it. + ## Status lives in frontmatter Research overviews, design docs, issue reports and decision records each carry their status as diff --git a/01-RESEARCH/003-service-supervision/00-overview.md b/01-RESEARCH/003-service-supervision/00-overview.md index a785cb1..3bd1dd2 100644 --- a/01-RESEARCH/003-service-supervision/00-overview.md +++ b/01-RESEARCH/003-service-supervision/00-overview.md @@ -3,7 +3,6 @@ status: graduated initiated: 2026-08-22 touches: [03-DESIGN/00-as-is/05-runtime-and-installation.md] became: - - 02-DECISIONS/0005-the-node-host.md - 02-DECISIONS/0005-the-node-host.md - 03-DESIGN/01-to-be/05-the-node-host.md --- diff --git a/02-DECISIONS/0080-the-development-cycle-is-checked.md b/02-DECISIONS/0080-the-development-cycle-is-checked.md new file mode 100644 index 0000000..a090efd --- /dev/null +++ b/02-DECISIONS/0080-the-development-cycle-is-checked.md @@ -0,0 +1,55 @@ +--- +topic: how we work +status: accepted +date: 2026-09-17 +deciders: jochen +reconstructed: false +extends: 0019-how-this-repository-works.md +--- + +# 80. The development cycle is checked, not trusted + +## Context + +[ADR 0019](0019-how-this-repository-works.md) made this repository the source of truth, and the +process overview drew the flow work must follow: an idea or a symptom, a decision, a to-be design, +a build in a code repository, an as-is update on shipping. The playbooks describe every step, and +frontmatter carries every status. + +But the flow itself was enforced by nothing. A design could appear citing no decision; a design +could sit `in-progress` naming no code; an issue could be `fixed` by nobody knows what. Each is +indistinguishable from correct work until somebody reads carefully — and the whole point of the +playbooks is that nobody should have to hold this repository in their head. A session that starts +cold (or an agent after a context clear) must be able to *find* the chain by following frontmatter +pointers, which only works if the pointers are reliably there. + +## Decision + +The development cycle is enforced mechanically, to the extent frontmatter can carry it: + +- **No design without a decision** — every to-be design names at least one record in `decisions:`. +- **No development without a design that says where** — an `in-progress` or `implemented` design + names its owning code in `code:`. +- **No owner-less diagnosis, no fix-less fix** — an issue marked `located` or `fixed` names + `located-in:`; one marked `fixed` or `resolved` says `fixed-by:` (prose counts — "nothing, the + capability existed" is an answer). +- **No silent graduation** — a `graduated` research overview says what it `became:`, and the + targets exist. + +[`00-META/checks/cycle.py`](../00-META/checks/cycle.py) refuses violations, beside `records.py` +and `index.py`; all three run before any merge here. What frontmatter cannot see — that code work +actually started from a handoff — remains held by playbooks 04 and 07: a feature branch exists +because a design or an issue sent it, and a merge is a human checkpoint. + +## Consequences + +A `/clear` costs little: [`AGENTS.md`](../AGENTS.md) now carries the cycle and a where-to-look +table, and the chain a fresh session needs is guaranteed present in frontmatter rather than +reconstructed from memory. The checks are the floor, not the ceiling — they verify pointers exist, +not that their content is true; reading remains the job. + +## References + +- [`00-META/process/00-overview.md`](../00-META/process/00-overview.md) — the flow, and its new + "The cycle is checked" section. +- [ADR 0019](0019-how-this-repository-works.md) — the repository this disciplines. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 6db91b6..7b1b097 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -165,5 +165,6 @@ python3 00-META/checks/index.py fail if stale - **0025** — [The design record is read where it is written, never copied to be found](0025-the-design-record-is-read-not-copied.md) - **0032** — [The local account owns the mesh; a surface delegates to a module](0032-the-local-account-owns-the-mesh.md) *(superseded)* - **0034** — [The local account owns the mesh, and a web application's login is not that](0034-the-local-account-owns-the-mesh.md) +- **0080** — [The development cycle is checked, not trusted](0080-the-development-cycle-is-checked.md) diff --git a/03-DESIGN/01-to-be/22-the-work-ahead.md b/03-DESIGN/01-to-be/22-the-work-ahead.md index 316cd79..a9ddda4 100644 --- a/03-DESIGN/01-to-be/22-the-work-ahead.md +++ b/03-DESIGN/01-to-be/22-the-work-ahead.md @@ -1,6 +1,11 @@ --- layer: to-be status: in-progress +code: + - mesh-host + - mesh-controller + - mesh-catalog + - mesh-lab updated: 2026-09-15 decisions: - 02-DECISIONS/0067-genesis-is-a-pivot.md diff --git a/AGENTS.md b/AGENTS.md index 313ca77..82d7069 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,33 @@ through them. Thin skills in `.claude/skills/` wrap these playbooks for invocati `hq-handoff`, `hq-sync-constitution`, `hq-status`); each defers to its playbook as authoritative and adds only the mechanical scaffolding. +## The development cycle + +Work enters as an **idea** (playbook [01 — research](00-META/process/01-research.md)) or a +**symptom** (playbook [03 — issues](00-META/process/03-issues.md)), becomes a **decision** +([02-DECISIONS](02-DECISIONS/), via playbook [02](00-META/process/02-graduation.md)), lands in a +**to-be design** naming that decision, is handed to a code repository (playbook +[04](00-META/process/04-build-handoff.md), on a feature branch per playbook +[07](00-META/process/07-feature-branches.md)) — and on shipping the as-is is updated and the +design flips to `implemented`. **No design without a decision; no development without a design +that names its owner.** Enforced by [`00-META/checks/cycle.py`](00-META/checks/cycle.py) +alongside `records.py` and `index.py` — run all three before any merge here. + +## Where to look (before assuming anything) + +| Question | Read | +|---|---| +| What does this word mean? | [`00-META/glossary.md`](00-META/glossary.md) | +| How do I do X in this repo? | [`00-META/process/`](00-META/process/) — the playbook index is in `00-overview.md` | +| What was decided, and why? | [`02-DECISIONS/README.md`](02-DECISIONS/README.md) (reading order), then the record | +| What is being built / already runs? | [`03-DESIGN/01-to-be/`](03-DESIGN/01-to-be/) / [`03-DESIGN/00-as-is/`](03-DESIGN/00-as-is/) — each doc's frontmatter says its status, decisions and owning code | +| What is broken or was? | [`04-ISSUES/`](04-ISSUES/) — frontmatter carries status/owner/fix | +| Which repo owns what code? | [`00-META/repos.md`](00-META/repos.md) | +| Cross-cutting status view? | the `hq-status` skill (generated, never stored) | + +Statuses live **only** in frontmatter; follow the pointers there (`decisions:`, `code:`, +`became:`, `fixed-by:`) instead of reconstructing history from memory. + ## Words One name per thing. [`00-META/glossary.md`](00-META/glossary.md) is the authority on