@nyuchi/mzizi-mcp 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +2 -4
  2. package/dist/access.d.ts.map +1 -1
  3. package/dist/auth/authkit-handler.js.map +1 -1
  4. package/dist/auth/mcp-api-handler.d.ts.map +1 -1
  5. package/dist/auth/mcp-api-handler.js.map +1 -1
  6. package/dist/auth/oauth-utils.d.ts.map +1 -1
  7. package/dist/auth/oauth-utils.js.map +1 -1
  8. package/dist/auth/provider-options.d.ts.map +1 -1
  9. package/dist/docs-federation.js.map +1 -1
  10. package/dist/floors.d.ts +4 -4
  11. package/dist/floors.d.ts.map +1 -1
  12. package/dist/fundi-a2a.d.ts.map +1 -1
  13. package/dist/generated/registry-pin.d.ts +1 -1
  14. package/dist/generated/registry-pin.d.ts.map +1 -1
  15. package/dist/generated/registry-pin.js +1 -1
  16. package/dist/generated/registry.d.ts +2 -2
  17. package/dist/generated/registry.d.ts.map +1 -1
  18. package/dist/generated/registry.js +459 -419
  19. package/dist/generated/registry.js.map +1 -1
  20. package/dist/generated/skills.d.ts +1 -1
  21. package/dist/generated/skills.d.ts.map +1 -1
  22. package/dist/generated/skills.js +14 -14
  23. package/dist/generated/skills.js.map +1 -1
  24. package/dist/http.d.ts.map +1 -1
  25. package/dist/http.js.map +1 -1
  26. package/dist/identity.d.ts.map +1 -1
  27. package/dist/issue-desk.d.ts.map +1 -1
  28. package/dist/origin.js.map +1 -1
  29. package/dist/registry.d.ts +0 -1
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js.map +1 -1
  32. package/dist/server.d.ts +7 -7
  33. package/dist/server.js +1 -1
  34. package/dist/server.js.map +1 -1
  35. package/dist/tool-result.d.ts +1 -1
  36. package/dist/tool-result.d.ts.map +1 -1
  37. package/dist/tools.d.ts +5 -5
  38. package/dist/tools.d.ts.map +1 -1
  39. package/dist/tools.js +1 -2
  40. package/dist/tools.js.map +1 -1
  41. package/dist/worker.d.ts.map +1 -1
  42. package/package.json +13 -13
@@ -13,15 +13,15 @@ export const BUNDLED_SKILLS = [
13
13
  "name": "mzizi-language",
14
14
  "category": "language",
15
15
  "description": "Use this skill when writing, checking or fixing Mzizi language source (.mz files): LANGUAGE-TRACKER.md as the one list of what Mzizi can and cannot do, the component syntax (enum tables, props, the view tree, when / for each, records, use motion), the backend service (routes, handlers with when / header / respond, example and ensure contracts; RFC-0011), the `mz check --agent` loop and its NDJSON diagnostics, `mz fix` for every exact fix, `mz contract`, `mz build` (a service to a local Rust + axum package), the diagnostic codes you will meet (MZ0101, MZ0106, MZ0312, MZ0313, MZ0406-MZ0409, MZ0602-MZ0613, MZ07xx, MZ08xx), programs (RFC-0013: fn main, let / var, functions, when / match, loops, int / float, text, list / map / set, records, enums, result with try) checked by `mz check` and run by `mz run`, the language harness, and exactly what is not built yet (modules, a standard library, component lowering, Rust interop, I/O, concurrency, generics, mz test, a release).",
16
- "body_mdx": "# Writing and checking Mzizi\n\n**Mzizi is a general-purpose programming language, built to make Rust better, the way\nTypeScript makes JavaScript better** (`CHARTER.md` v0.4, \"Mzizi: a general-purpose\nprogramming language\"). How: no borrows, lifetimes or ownership in the language you write\n(RFC-0001 §1.8), with the harness at the core, the layer an agent reads. \"Makes Rust better\"\nis the goal Phase 0 measures, not a result. Only a backend `service` lowers to Rust today; no\ncomponent does.\n\nRust is its platform, the way JavaScript is TypeScript's: Mzizi is designed to lower to Rust (RFC-0001 §5), and Mzizi\nRoots is its component model, the way React is JavaScript's. Its goal is to be the language\npeople choose instead of TypeScript, Python and C++: write Mzizi, get Rust underneath. That\nis a goal, not a result. Phase 0 has one goal: to build Mzizi as a programming language,\nmeasured against the best existing language for each kind of task (RFC-0009). Nothing has\nbeen measured against that yet.\nIts syntax, type system and diagnostics are designed for machine authorship.\n\nThe language lives in [`mzizi-dev/mzizi`](https://github.com/mzizi-dev/mzizi) (public). Say\nwhich thing is which when you describe it:\n\n- **The language:** its syntax, type system, semantics, and contracts as a language\n feature.\n- **The harness, the core of Mzizi:** the layer an agent reads and works through. It covers\n the language as an agent sees it (grammar, types, contracts, canonical form), the agent\n protocol (`mz check --agent`, its diagnostics and fixes; RFC-0001 §4), and the plugin\n host that the toolchain, the CLI, the MCP server and plugins attach to natively. Part of\n it exists today, in the toolchain: the `mz check --agent` output this skill teaches. The\n harness as a whole is designed, not built: RFC-0012 (a draft) specifies it, and there is no\n `mz harness` command and no plugin host, so never say it works. It lives in\n `mzizi-dev/mzizi`, the language repository, and the `agent-tools`\n packages (`@nyuchi/mzizi-cli`, `mzizi-mcp`, `fundi`) are its clients. It is not\n `benchmarks/harness/`, which is the Phase 0 benchmark scorer.\n- **The toolchain, which implements and supports the language:** `mz` (the compiler, its\n IR and its NDJSON diagnostics, `mz fix`, `mz contract`, `mz build`), `@nyuchi/mzizi-cli`, the MCP\n server at `mcp.mzizi.dev`, these skills, and the Phase 0 benchmark harness. They are\n meant to attach to the harness. Never call the compiler \"the language\".\n- **The components, which support the language:** Mzizi Roots and the registry\n (`mzizi-dev/mzizi-registry`), the language's UI layer. That is the companion\n `mzizi-roots` skill. Supplying the benchmark's UI tasks is one of their jobs, not what\n they are.\n\nThis skill describes `main` at `cf3a44e` (2026-11-01): 765 tests in the workspace\n(`cargo test --workspace`, 634 of them in the compiler crate) and 33,540 lines in\n`compiler/src`. Where this skill and the code disagree, **the code is the fact** (RFC-0003\n§7.1). Every example below is copied from a file in the repository that passes `mz check`,\n`mz contract` and, for a program, `mz run` there.\n\n## Check the tracker before you claim anything\n\n[`LANGUAGE-TRACKER.md`](https://github.com/mzizi-dev/mzizi/blob/main/LANGUAGE-TRACKER.md) in\nthe language repository is **the one list of what Mzizi can and cannot do** (owner, 2026-09-30).\nIf a capability is not ✅ there, Mzizi does not have it, whatever any other page says, this\nskill included. Its status column is what the code on `main` does, never what an RFC designs.\nRead it before you tell anyone Mzizi can do something, and before you write Mzizi that relies\non a construct this skill does not show.\n\n**In a `program` (RFC-0013, rows C1–C10, all ✅ on `main` since the 2026-10-08 releases):**\nexpressions and operators (arithmetic, comparison with `is`, `and` / `or`, text interpolation\n`\"{x}\"`), bindings (`let`, `var`, assignment), functions with parameters, return types and\nrecursion, control flow (`when` / `else when` / `else`, exhaustive `match`, `when` and `match`\nas values, `for each` over `range(a, to = b)` or a list, `while`, `break`, `continue`, early\n`return`), `int` and `float` with the numeric methods, text methods (`s[i]`, `slice`, `find`,\n`split`, `chars`, `parse_int`, `parse_float`), collections (`list(T)`, `map(K, V)`, `set(K)`,\nbracket literals, indexing that returns an option read with `otherwise`, `in`, the named\nfolds), records with methods, enums with columns, and errors (`result(T, E)`,\n`return error(e)`, prefix `try`, a `match` on a result, and `main` returning a result).\n`mz run` lowers a program to a dependency-free Rust package, builds it with Cargo and runs it.\n\n**Still missing (❌ or partial in the tracker):** modules and imports (P1; one top-level\ndeclaration per file), a standard library (P2), lowering of components (P3 is partial: a\n`service` and a `program` lower, no component does), Rust interop (P6), state and I/O (P8),\nconcurrency (P9), generics and interfaces (P12), a formatter, a language server, `mz test`,\npackage management and a published release (`mzizi-lang-compiler` 0.0.0, `publish = false`).\nErrors are not yet mapped back to `.mz` lines (P5, designed). Do not write Mzizi that needs\nany of these, and do not tell anyone it exists.\n\nFour pilots have run (two on 2026-09-27, two on 2026-10-29) and none showed an advantage for\nMzizi. The run that tests the charter's kill criterion has not happened, so never write that\nMzizi is faster or better.\n\n## What exists, and what does not\n\n**Built and tested, in the toolchain:** the lexer, the recovering parser, the name and type\nresolver, the agent diagnostic protocol (`mz check --agent`), `mz fix`, the content-addressed\nIR, `mz outline`, contract evaluation (`mz contract`), the language harness (`mz harness`,\nRFC-0012: one machine-readable definition of every feature, operator and diagnostic code),\nnine primitives written in Mzizi (`primitives/*.mz`), component, service and program examples\n(`examples/*.mz`, each program with the output `mz run` must print in `examples/<name>.expected`),\nand the Phase 0 benchmark harness and runner. All of it is gated in CI.\n\n**Programs (RFC-0013), built and tested.** A `program` file holds one program with `fn main`.\n`mz check` checks it, and `mz run` lowers it to Rust, builds it and runs it:\n\n```mz\n## Fibonacci numbers by recursion, in RFC-0013's foundation slice (§18.1): functions,\n## calls, `let`, `var`, `when`, integer arithmetic and `print`, and no loops yet.\n## `mz run examples/fib.mz` prints what fib.expected holds; CI compares the two.\nprogram fib\n\n fn main\n print(\"fib(9) is {fib(9)}\")\n let n = 20\n let f = fib(n)\n print(\"fib({n}) is {f}\")\n var total = 0\n total = total + fib(10)\n total = total + fib(11)\n print(\"fib(10) + fib(11) is {total}, and that is fib(12): {total is fib(12)}\")\n print(parity(f))\n end fn main\n\n fn fib(n: int): int\n when n < 2\n return n\n end\n return fib(n - 1) + fib(n - 2)\n end fn fib\n\n fn parity(n: int): text\n when n % 2 is 0 and n > 0\n return \"{n} is even\"\n else\n return \"{n} is odd, or not positive\"\n end\n end fn parity\n\nend program fib\n```\n\n`mz run examples/fib.mz` prints:\n\n```text\nfib(9) is 34\nfib(20) is 6765\nfib(10) + fib(11) is 144, and that is fib(12): true\n6765 is odd, or not positive\n```\n\nErrors are values. A function returns `result(T, E)`, fails with `return error(e)`, and a\ncaller handles the result with `match` or propagates it with prefix `try`:\n\n```mz\n## Checks ages: one function fails with an error, its callers handle it with `match` or\n## propagate it with `try`, and `main` itself returns a result (RFC-0013 §12).\nprogram errors\n\n enum age_problem\n negative say \"is below zero\"\n too_old say \"is above 150\"\n end\n\n fn main: result(none, age_problem)\n print(describe(42))\n print(describe(-3))\n print(describe(200))\n let total = try total_age(20, 22)\n print(\"20 and 22 make {total}\")\n match total_age(20, 151)\n case ok sum\n print(\"20 and 151 make {sum}\")\n case error problem\n print(\"20 and 151 were rejected: {problem} {problem.say}\")\n end\n end fn main\n\n fn check_age(n: int): result(int, age_problem)\n when n < 0\n return error(negative)\n end\n when n > 150\n return error(too_old)\n end\n return n\n end fn check_age\n\n fn describe(n: int): text\n match check_age(n)\n case ok age\n return \"age {age} is fine\"\n case error problem\n return \"{n} is rejected: {problem} {problem.say}\"\n end\n end fn describe\n\n fn total_age(a: int, b: int): result(int, age_problem)\n return try check_age(a) + try check_age(b)\n end fn total_age\n\nend program errors\n```\n\n**The backend slice (RFC-0011), built and tested:** a `service` with HTTP routes and handlers\n(`when`, `header`, `respond`), checked by `mz check`. `mz contract` runs a service in process\nand evaluates its `example` and `ensure` clauses. `mz build` lowers a service to a local\nRust + axum package, which CI compiles, tests and serves. That is **the whole of the lowering\ntoday:** no component lowers, and the package is a local server, with no Workers, WebAssembly\nor Containers target and no deployment. The `mzizi-be` benchmark arm, the probe crate\n`mzprobe` and backend task B1 exist; no backend episode has run.\n\n**Not built:** everything in the \"still missing\" list above, lowering of components,\nrendering, a release and a published binary. A component `.mz` file today is checked and its contract\nevaluated; **nothing runs it.** Do not tell anyone Mzizi \"compiles to Rust\" or is \"production\nready\"; say it is designed to lower to Rust, and that only a service does so today.\n\n**In progress, as design only:**\n\n- **The harness.** RFC-0012 (draft). Only the agent protocol exists (`mz check --agent`,\n `mz fix`, `mz contract`).\n- **Contracts everywhere.** RFC-0010 (draft) extends contracts to functions, handlers,\n services and records. Component contracts (RFC-0006) and service contracts (RFC-0011 §7) are\n evaluated. Of RFC-0010's codes, `MZ0606`, `MZ0611`, `MZ0612` and `MZ0613` exist, for\n services; `MZ0607`–`MZ0610` do not exist in the compiler.\n- **Request bodies** for a service (`body input: <record>` and `reject` blocks, RFC-0011 §4.3)\n are designed and not built. So is state (task B4).\n- **Modules.** There are no imports. Components are referenced by bare name\n (`primitives/confirm_bar.mz` uses `button` and `alert` with no import line). A reference to\n another component's enum (`button_variant.ghost`) is warning `MZ0502`, because it cannot be\n checked until modules exist.\n\n**Results, as they fell.** The first two Phase 0 pilots ran on 2026-09-27 and neither showed an\nadvantage for Mzizi (two more ran on 2026-10-29, also with no advantage). On the frontier model the Mzizi and Dioxus arms tied, with Mzizi about 8%\nfewer transcript tokens. On the ~7B open-weight model, the design target, Mzizi did worse on\nall three metrics. Neither pilot is the kill-criterion run (RFC-0009 §6), and that run has not\nhappened. The pilots were tests inside Phase 0 on a few registry UI components against\nDioxus, not its goal. \"Designed for agents\" and \"aims to replace TypeScript, Python and C++\"\nare accurate; \"faster\" or \"better\" is not.\n\n## Getting `mz`\n\nThere is no release. Build it from source; the compiler crate has zero dependencies:\n\n```bash\ngit clone https://github.com/mzizi-dev/mzizi.git && cd mzizi/compiler\ncargo build --bin mz # ../target/debug/mz\ncargo run --bin mz -- check ../primitives/button.mz\n```\n\n**Never `cargo install mz`.** The `mz` crate on crates.io is an unrelated project, and the\ncompiler crate (`mzizi-lang-compiler`) is `publish = false`.\n\n## The commands\n\n`mz` takes exactly one file per call. `mz check ../primitives/*.mz` is a usage error (exit 2).\nLoop over files instead.\n\n| Command | Does |\n| ----------------------------------- | -------------------------------------------------------------------------------- |\n| `mz check <file>` | human-readable diagnostics |\n| `mz check --agent <file>` | NDJSON: one diagnostic per line, then one summary line. **Target this.** |\n| `mz fix <file>` | applies every `exact` fix in place, in one pass, then re-checks the file |\n| `mz contract <file>` | evaluates the file's `contract` block (only on a file that checks clean) |\n| `mz contract --agent <file>` | the same, as NDJSON; the summary adds `contract_clauses` and `contract_failures` |\n| `mz outline <file>` | the interface only (name, enums, props, contract subjects), as valid Mzizi |\n| `mz hash <file>` / `mz ir <file>` | the content-addressed root hash / every IR node with its hash and path |\n| `mz build <service.mz> --out <dir>` | lowers a service to a Rust + axum Cargo package in `<dir>` (only a clean file) |\n| `mz build <program.mz> --out <dir>` | lowers a program to a dependency-free Rust package in `<dir>` |\n| `mz run <program.mz>` | lowers a program, builds it with Cargo and runs it |\n| `mz harness version` | protocol, language version and the definition's SHA-256 |\n| `mz harness definition --agent` | every language-harness entry (features, operators, codes) as one line of JSON |\n| `mz harness entry <name>` | one entry, e.g. `mz harness entry let` |\n\nExit status: **0** no errors (warnings do not fail), **1** errors or a failed contract\nassertion, **2** a usage or I/O problem. Branch on the status; parse the NDJSON for detail.\n\n`mz run` is the exception, because 1 belongs to the program it runs. It exits with the\nprogram's own status: 0; 1 when `main` returns an error (`MZ0992`); 101 for a trap (`MZ0991`)\nor a panic; 141 when standard output is closed; 128 plus the signal that ended it; **3** when\nthe check failed and nothing ran, or the lowered Rust did not compile (`MZ0990`); and 2 for a\nusage, environment or I/O problem (`--agent` is not accepted, a file that is not a `program`).\n\nOn a **service**, `mz contract` runs the service in process (no lowering, no socket), and the\nsummary adds `contract_tested`, the number of generated requests its `ensure` clauses were\ntested over. `mz outline`, `mz hash` and `mz ir` do not cover services yet and exit 2. `mz\nbuild` on a component exits 2 (\"components do not lower yet\"), and on a file with errors it\nprints them, builds nothing and exits 1.\n\n`check` and `contract` are deliberately separate: a compile error is not a defect, and a\ncontract failure is. Your file is done when **both** `mz check --agent` and\n`mz contract --agent` exit 0.\n\n## The loop\n\n1. Write the file.\n2. `mz check --agent file.mz`. If it exits 0, go to 5.\n3. `mz fix file.mz` to apply every `exact` fix. Read each remaining diagnostic's `say`; apply\n `guess` fixes only after checking them.\n4. Fix everything reported, then go to 2. Errors are independent, so fix them all before\n re-running; do not fix one and re-run to \"see the next\".\n5. `mz contract --agent file.mz`. `MZ0603` means an assertion is false (a defect in the\n component or the contract); `MZ0605` means it could not be evaluated, which **counts as a\n failure**.\n\n## Reading the NDJSON\n\nThis file is wrong on purpose. It is the example in `benchmarks/prompts/mzizi-guide.md`, which\n`benchmarks/prompts/verify-guide.sh` checks against the real compiler.\n\n```mz\n## WRONG ON PURPOSE: three mistakes.\ncomponent tag\n\n enum tag_size\n snug class \"h-13 px-3\"\n end\n\n prop onPick: event(none)\n prop size: tag_size = snug\n\n view\n row\n slot = \"tag\"\n class = \"rounded-full {size.class}\"\n tap = on_pick\n end\n end\n\n contract\n slot is \"tag\"\n tag_size.snug height 52\n end\n\nend\n```\n\n`mz check --agent tag.mz` exits 1 and prints exactly:\n\n```text\n{\"code\":\"MZ0101\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[8,8,8,14],\"say\":\"`onPick` is not snake_case — Mzizi names are snake_case, so write `on_pick`\",\"fix\":{\"span\":[8,8,8,14],\"replace\":\"on_pick\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0602\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[21,26,21,28],\"say\":\"a contract clause needs a predicate (is / in / contains / not_empty / at_least / uses / min_height / shows), found `52`\",\"fix\":{\"span\":[21,26,21,26],\"replace\":\"is \",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0208\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[24,1,24,4],\"say\":\"top-level blocks close with their name: write `end component tag`\",\"fix\":{\"span\":[24,1,24,4],\"replace\":\"end component tag\",\"confidence\":\"exact\"}}\n{\"summary\":true,\"errors\":3,\"warnings\":0,\"exact_fixable\":3,\"ms\":0}\n```\n\n`mz fix tag.mz` then prints `mz: applied 3 exact fixes; 0 errors (0 exact-fixable) remain`, and\n`mz contract --agent tag.mz` reports `\"contract_clauses\":2,\"contract_failures\":0`.\n\n- `code`, `severity` (`error` or `warning`), `file` (the path you passed), `span`\n `[line, col, end_line, end_col]` (1-indexed, end exclusive) and `say` are always present.\n- `say` quotes the offending source, so you need not re-read the file to understand it.\n- `fix` is optional: put `replace` at `fix.span`. An empty `replace` is a deletion. `exact`\n is safe to apply blind; `guess` has the right location and an inferred replacement.\n- The last line is always the summary.\n\n## Syntax essentials\n\nOne file holds one component, or one service (see \"Services\" below). There is one form per\nintent: if a construct is not shown here or in the RFCs, assume it does not exist. The order\ninside a component is doc lines, `use`, `enum` and `record`, `prop`, `view`, `fn`, `contract`.\n\nA complete primitive, `primitives/badge.mz`. Its `px-2 py-0.5` and `gap-1` are the corpus\nbadge's, copied unchanged, and are below the spacing floor (14px inset, 7px gaps, 1.5 leading;\nthe `mzizi-design` skill), so they are not a model for new code: a component you write keeps\nthe floor, or carries a `mzizi-spacing-exempt: <reason>` comment:\n\n```mz\n## A small status label. Not interactive, so no touch floor applies.\n## Corpus reference: n2-primitives/badge.tsx.\ncomponent badge\n\n enum badge_variant\n default class \"bg-primary text-primary-foreground\"\n secondary class \"bg-secondary text-secondary-foreground\"\n destructive class \"bg-destructive/10 text-destructive dark:bg-destructive/20\"\n outline class \"border-border text-foreground bg-input/30\"\n ghost class \"hover:bg-muted hover:text-muted-foreground\"\n link class \"text-primary underline-offset-4 hover:underline\"\n end\n\n prop variant: badge_variant = default\n prop label: text\n\n view\n mark\n slot = \"badge\"\n portal = \"https://mzizi.dev/components/badge\"\n variant = variant\n class = \"inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all {variant.class}\"\n text = label\n end\n end\n\n contract\n every badge_variant class not_empty\n badge_variant.default class contains \"bg-primary\"\n slot is \"badge\"\n end\n\nend component badge\n```\n\n- **Shape.** `component <name>` opens the file and `end component <name>` closes it, repeating\n the name. Every other block (`enum`, `record`, `view`, `contract`, each element, each `when`,\n each `for each`, each `fn`) closes with a bare `end`. Never `end enum` or `end view`.\n- **Comments.** `##` to end of line is the only comment form. `//` and `/* */` are errors.\n- **Names** are `snake_case`. `onRemove` is `MZ0101`, with the snake form as its `exact` fix.\n- **Enums are variant tables.** One row per variant: its name, then `column value` pairs. Every\n variant has every column (`MZ0303` otherwise). A column can carry meaning beyond styling, as in\n `primitives/alert.mz`, where `announce` ties the ARIA role to the severity:\n\n ```mz\n enum alert_variant\n default class \"bg-card text-card-foreground\" announce \"status\"\n destructive class \"bg-card text-destructive\" announce \"alert\"\n warning class \"bg-card text-[var(--status-warning,#FFD740)]\" announce \"alert\"\n info class \"bg-card text-[var(--status-info,#00B0FF)]\" announce \"status\"\n end\n ```\n\n- **Heights come from the class.** A variant's `height` is the pixels its `h-N` or `size-N`\n class renders (`N × 4`). You may leave the `height` column out. A written `height` that\n disagrees with the class is `MZ0313`, and its `exact` fix is the rendered number.\n- **Props.** `prop <name>: <type>`, optionally `= <default>`. The types are an enum name\n (`= default`), `text`, `bool`, `int`, `event(none)`, `event(<type>)`, `list(<type>)`\n (omitted means empty), `option(<type>)` (omitted means none) and a record name. Never\n `option(list(T))` (`MZ0703`).\n- **The view** holds exactly one element tree. An element is a bare word on its own line,\n closed by `end`. Inside it, each line is an attribute `name = value` (the `=` is required),\n a child element, a `when` block, a `for each`, or `nothing`. Strings interpolate with\n `{...}`; there is no `+`.\n- **Conditionals.** `when x` … `end`, `when not x`, `when v is <variant>`, and for an option or\n a list `when x is none` … `else` … `end`. There is no `if` (`MZ0407`, `exact` fix `when`)\n and no `match`. From `primitives/avatar.mz`:\n\n ```mz\n when image is none\n row\n slot = \"avatar-fallback\"\n class = \"flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground {size.text_class}\"\n text = initials\n end\n else\n picture\n slot = \"avatar-image\"\n source = image\n alt = alt\n class = \"aspect-square size-full rounded-full object-cover\"\n end\n end\n ```\n\n- **Lists** render with `for each item in items`, whose first line is its `key = item.<field>`.\n- **Capabilities.** `use motion` if the component animates (`primitives/spinner.mz`). Nothing\n else needs `use`.\n- **Events.** Bind with `tap = on_tap` / `change = on_change`. A parameterless `fn` can `emit`\n an event prop, as in `examples/connectivity_bar.mz`:\n\n ```mz\n fn retry\n emit on_state_change(syncing)\n end\n ```\n\n- **No React carry-overs.** `...props` spreads are `MZ0106` (`exact` fix deletes the line).\n An `as_child` prop is warning `MZ0312`: delete it, and keep what the `as_child = false`\n branch renders. `className` pass-through has no equivalent.\n\n`benchmarks/prompts/mzizi-guide.md` is the complete one-file guide the benchmark gives a model,\nincluding records, `for each` and a React-to-Mzizi table. Read it before writing a component.\n\n## Services (the backend slice, RFC-0011)\n\nA backend program is one file holding one `service`, closed by `end service <name>`. Every\ninner block (`record`, `route`, `fallback`, `when`, `contract`) closes with a bare `end`. The\norder is doc lines, service-level `header` lines, `record`s (and `enum`s), `route`s,\n`fallback`, `contract`. There are no imports, no `main`, no server setup and no port.\n\n`examples/registry.mz`, the example service (22 clauses, all holding; its `ensure` clauses\ntested over 61 generated requests):\n\n```mz\n## A registry of two components: routes, methods, redirects and a JSON 404.\n## RFC-0011's worked example, shaped like the API gateway's `/v1/ui` routes (RFC-0009 §2.4, B1).\nservice registry\n\n header \"x-mzizi-source\" \"fixture\"\n\n record problem\n field error: text\n end\n\n record health\n field status: text\n end\n\n record item\n field name: text\n end\n\n route health_check\n get \"/v1/health\"\n respond 200 json health status \"ok\"\n end\n\n route ui_list\n get \"/v1/ui\"\n respond 200 file \"fixtures/ui.json\"\n end\n\n route ui_item\n get \"/v1/ui/{name}\"\n when name in \"badge\" \"button\"\n respond 200 json item name name\n end\n header \"cache-control\" \"private, no-cache, no-store, max-age=0, must-revalidate\"\n respond 404 json problem error \"Not found\"\n end\n\n fallback\n header \"cache-control\" \"private, no-cache, no-store, max-age=0, must-revalidate\"\n respond 404 json problem error \"Not found\"\n end\n\n contract\n example get \"/v1/health\" body.status is \"ok\"\n example get \"/v1/ui\" status is 200\n example get \"/v1/ui\" header \"content-type\" is \"application/json\"\n example get \"/v1/ui/button\" status is 200\n example get \"/v1/ui/button\" body.name is \"button\"\n example get \"/v1/ui/does-not-exist\" status is 404\n example get \"/v1/ui/does-not-exist\" body.error is \"Not found\"\n example get \"/v1/ui/does-not-exist\" header \"cache-control\" is \"private, no-cache, no-store, max-age=0, must-revalidate\"\n example head \"/v1/ui/button\" status is 200\n example head \"/v1/ui/button\" body is \"\"\n example options \"/v1/ui\" status is 204\n example options \"/v1/ui\" header \"allow\" is \"GET, HEAD, OPTIONS\"\n example delete \"/v1/ui/button\" status is 405\n example delete \"/v1/ui/button\" body is \"\"\n example get \"/v1/ui/\" status is 308\n example get \"/v1/ui/?limit=2\" header \"location\" is \"/v1/ui?limit=2\"\n example get \"/nope\" status is 404\n example get \"/nope\" header \"content-type\" is \"application/json\"\n ensure status in 200 204 308 404 405\n ensure header \"x-mzizi-source\" is \"fixture\"\n ensure when status is 404 then body.error is \"Not found\"\n ensure when status is 405 then header \"allow\" is \"GET, HEAD, OPTIONS\"\n end\n\nend service registry\n```\n\n- **A route** is `route <name>`, then exactly one `<method> \"<pattern>\"` line, then zero or\n more `query <name>: option(<scalar>)` lines, then statements, then `end`. The methods are\n `get`, `head`, `post`, `put`, `patch`, `delete` and `options`, lower case; a second method on\n the same path is a second route.\n- **A pattern** is `/` or `/`-separated segments of lower-case letters, digits and `-._~`,\n or a whole-segment `{name}`, which binds `name` as `text`. No trailing slash, no empty\n segment, no `?`. A literal segment beats a parameter at the same position.\n- **A query parameter is always an option:** `query limit: option(int)`. `option(int)` is\n `-`? then ASCII digits, so `\"0x10\"`, `\"1e2\"`, `\" 7 \"` and `\"1.5\"` are `none`; `option(bool)`\n is exactly `true` or `false`. Bad input is absence, never an error.\n- **A handler has three statements**, and every path through it must end in `respond`:\n\n | Statement | Means |\n | -------------------------------- | --------------------------------------------------------- |\n | `when <cond>` … [`else` …] `end` | a branch; with no `else`, a false condition falls through |\n | `header \"<name>\" \"<value>\"` | set a header on the response this path sends |\n | `respond <status> [<body>]` | send the response; nothing after it on the path runs |\n\n A condition is `p` / `not p` (a `bool`), `p is <literal>`, `p in <literal> …`,\n `p at_least <n>` / `p at_most <n>` (an `int`), or `p is none` … `else` (an option, which is\n narrowed in the `else`). There is no `if`, `==`, `return`, assignment, loop or call.\n\n- **Bodies:** none; `json <record> <field> <value> …` (every field once, a literal or a\n parameter, no punctuation; `application/json`); `text \"<string>\"` (may interpolate a\n parameter as `{name}`); or `file \"<path>\"` (a fixture beside the `.mz` file, embedded at\n build time). A response record's fields are scalars or options of scalars. `204`, `304` and\n `1xx` take no body.\n- **Headers** are lower case. A service-level `header` applies to every response, runtime\n ones included; a route's own `header` for the same name wins. `content-length` and\n `transfer-encoding` belong to the server.\n- **The runtime answers what HTTP defines, so do not write it:** `HEAD` for a `get` route\n (its status and headers, no body); `OPTIONS` (`204` with `allow`, e.g. `GET, HEAD, OPTIONS`);\n any other undeclared method on a matched path (`405` with the same `allow`, no body); and a\n trailing slash or `//` (`308` to the canonical path, query kept, never off-site). A path no\n route matches runs `fallback`, or gets an empty `404` when there is none.\n- **The contract:** `example <method> \"<target>\" <facet> <predicate>` sends one request;\n `ensure [when <facet> <predicate> then] <facet> <predicate>` must hold for every generated\n request. A facet is `status`, `header \"<name>\"`, `body` or `body.<field>`; a predicate is\n `is <value>`, `in <value> …`, `not_empty`, `contains \"<text>\"`, `at_least <n>` or\n `at_most <n>`. `header \"<name>\" is none` asserts absence and `body is \"\"` an empty body.\n `ensure` is **tested** over the generated set, never proven.\n\nThis file is wrong on purpose. It is the example in `benchmarks/prompts/mzizi-be-guide.md`:\n\n```mz\n## WRONG ON PURPOSE: three mistakes.\nservice shop\n\n record problem\n field error: text\n end\n\n route item\n GET \"/v1/items/{id}/\"\n when id is \"1\"\n respond 200 text \"one\"\n end\n end\n\nend service shop\n```\n\n`mz check --agent candidate.mz` exits 1 and prints:\n\n```text\n{\"code\":\"MZ0613\",\"severity\":\"warning\",\"file\":\"candidate.mz\",\"span\":[2,9,2,13],\"say\":\"`service shop` has no `contract` block — its behaviour is unverified (RFC-0010 §8)\"}\n{\"code\":\"MZ0801\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[9,5,9,8],\"say\":\"`GET` is not a method — a route declares one of get, head, post, put, patch, delete, options\",\"fix\":{\"span\":[9,5,9,8],\"replace\":\"get\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0802\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[9,9,9,26],\"say\":\"`\\\"/v1/items/{id}/\\\"` ends with `/` — patterns have no trailing slash; the runtime redirects `…/` to the path without it\",\"fix\":{\"span\":[9,9,9,26],\"replace\":\"\\\"/v1/items/{id}\\\"\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0804\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[13,3,13,6],\"say\":\"a path through `route item` reaches `end` without a `respond` — when the `when` on line 10 does not respond on both branches; every path must respond\"}\n{\"summary\":true,\"errors\":3,\"warnings\":1,\"exact_fixable\":2,\"ms\":0}\n```\n\n`mz fix` applies the two `exact` fixes. `MZ0804` needs a decision: add\n`respond 404 json problem error \"Not found\"` after the `when`'s `end`, so the false path\nresponds too.\n\n**Building it.** `mz build registry.mz --out <dir>` checks the file and, with zero errors,\nwrites `Cargo.toml`, `src/main.rs` and the fixtures. The package is its own workspace root,\ngenerated output that is never committed, and depends on `axum =0.8.9` and `tokio =1.53.1`\n(tests add `tower`, `http-body-util` and `serde_json`). Every `example` becomes one\n`#[tokio::test]` (`cargo test` in the package); `ensure` clauses are evaluated by\n`mz contract` only. `cargo run --release --manifest-path <dir>/Cargo.toml` serves\n`127.0.0.1:$PORT` (8080 by default) and prints `listening on http://127.0.0.1:<port>`.\n\n## Contracts\n\nEvery component carries a `contract … end` block: one assertion per line,\n`<subject> <predicate>`, no colon. A file with no contract is warning `MZ0501`. It is checked\nagainst the file's own enums, view attributes and prop defaults (RFC-0006). From\n`primitives/button.mz`:\n\n```mz\n contract\n every button_size height at_least 48\n button_size.default height is 56\n button_size.sm height is 48\n button_variant.default class contains \"bg-primary\"\n slot is \"button\"\n end\n```\n\n| Subject | Means |\n| --------------------------- | -------------------------------------------------- |\n| `every <enum> <column>` | that column in every variant |\n| `<enum>.<variant> <column>` | one cell |\n| `<attribute>` | the root view element's attribute (`slot`, `role`) |\n| `<prop>` | the prop's default (only a prop that has one) |\n| `<element> \"<text>\"` | the element carrying that text |\n| `when <name>` | the `when` branch that mentions that name |\n\nPredicates: `is <value>` (exactly as written: `height is \"52\"` fails against `height 52`),\n`contains \"<s>\"`, `not_empty`, `at_least <n>`, `in \"<a>\" \"<b>\"`, `uses \"--token\"`,\n`uses <component>` (no subject), `shows <element> \"<t>\"` (after `when <name>`) and\n`min_height <n>`. `examples/connectivity_bar.mz` uses the last two:\n\n```mz\n when offline shows button \"Retry\"\n button \"Retry\" min_height 48\n```\n\nAssert what the source guarantees: every interactive size clears 48px, the `slot`, and the key\ncells. An assertion on a prop with no default is `MZ0605` (unevaluable), which fails.\n\n`mz contract` checks nothing rendered. The rendered half is `benchmarks/harness`, which diffs a\n`.mz` component's variants, defaults, touch heights and `data-slot` set against the Rust\nreference. Mzizi Roots components carry the same clause grammar as `pub const CONTRACT` and\ncheck it against their server-rendered markup (see the `mzizi-roots` skill).\n\nThe direction (RFC-0010, draft) is that **everything built carries a contract**: components,\nlanguage functions and backend handlers. The component form and the service form (\"Services\"\nabove) are evaluated today; functions have no contracts because they have no bodies.\n\n## Diagnostic codes you will meet\n\nThe ranges are `MZ01xx` lexing and names, `MZ02xx` blocks and closers, `MZ03xx` declarations,\n`MZ04xx` the view, `MZ05xx` lints, `MZ06xx` contracts, `MZ07xx` types and resolution, and\n`MZ08xx` services and handlers. The full tables are RFC-0001 §4.7, RFC-0006 §8, RFC-0008\n(types), RFC-0010 §6 and RFC-0011 §9. A service reuses `MZ0701`, `MZ0707`, `MZ0708` and\n`MZ0710` for unknown names and un-narrowed options.\n\n| Code | Severity | Written | Repair |\n| ------------------- | -------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |\n| `MZ0101` | error | a name that is not `snake_case` | `exact`: the snake form |\n| `MZ0105` | error | `list<T>`, `T[]`, `[T]` | `exact`: `list(T)` |\n| `MZ0106` | error | `...props`, `{...props}`, `..attributes` | `exact`: delete (the whole line when alone) |\n| `MZ0204` | error | a block never closed | `guess`: insert the `end`s |\n| `MZ0206`–`MZ0208` | error | a wrong closer (`end view`, a bare `end` for `component`) | `exact`: rewrite the whole closer line |\n| `MZ0303` | error | a variant missing a column another variant has | add the column |\n| `MZ0312` | warning | `prop as_child: bool` (React `asChild`) | `exact` when unread: delete the prop |\n| `MZ0313` | error | `icon class \"size-14\" height 48` | `exact`: the rendered px (`56`) |\n| `MZ0406` | error | `class \"flex\"` (attribute without `=`) | insert `=` (`exact` for known attribute words) |\n| `MZ0407` | error | `if open` | `exact`: `when open` |\n| `MZ0408` | error | an attribute directly under `view` | move it inside the root element |\n| `MZ0409` | error | `span class = \"x\"` (more after an element word) | put the element word on its own line |\n| `MZ0501` | warning | no `contract` block | write one |\n| `MZ0502` | warning | `<other>_enum.variant` from another component | none until modules |\n| `MZ0602` | error | a clause with no predicate (`height 52`) | `exact`: insert `is` (needs an operand) |\n| `MZ0603` | error | **an assertion that does not hold** | fix the component or the clause |\n| `MZ0605` | error | an assertion that cannot be evaluated | assert something the file defines |\n| `MZ0606` | error | an `example` status that an `ensure status in …` rules out | fix one of the two |\n| `MZ0611` / `MZ0612` | error | a service `ensure` / `example` that does not hold | `say` quotes the request that broke it |\n| `MZ0613` | warning | a service with no or an empty `contract` block | write one |\n| `MZ0701` | error | an unknown type (`strng`) | alias or nearest-name fix |\n| `MZ0706` | error | a default that does not fit its type | varies |\n| `MZ0707` / `MZ0708` | error | an unknown name / variant, column or field | nearest-name fix |\n| `MZ0710` | error | an option used without `when … is none … else` | narrow it first |\n| `MZ0711` / `MZ0712` | error | a wrong kind of value / a `when` that does not fit its type | see `say` |\n| `MZ0801` | error | a route not opening with `<method> \"<pattern>\"`, `GET`, a second method line | `exact` for `GET`; else a `guess` |\n| `MZ0802` | error | a bad pattern: trailing `/`, empty segment, `?`, no leading `/` | `exact` where the repair is certain |\n| `MZ0803` | error | two routes with one method and the same pattern | delete or change one |\n| `MZ0804` | error | a handler path that reaches `end` without `respond` | add the `respond` |\n| `MZ0805` | error | a line after `respond` that can never run | `exact`: delete it |\n| `MZ0806` | error | a bad `respond`: no status, not 100–599, a body on `204` | `exact` deletes a body that is not allowed |\n| `MZ0807` | error | a bad `header`: `\"Cache-Control\"`, set twice, server-owned | `exact`: the lower-case name |\n| `MZ0808` | error | a `json` literal with a missing, repeated or ill-typed field (unknown is `MZ0708`) | give every field once |\n| `MZ0809` | error | `query limit: int` | `exact`: `option(int)` |\n| `MZ0810` | error | a `file \"…\"` fixture that does not exist | add the file or fix the path |\n| `MZ0811` | error | a line a service or handler cannot hold (`return`, `=`, `view`) | see `say` (`is` for `=` is `exact`) |\n| `MZ0812` | error | a second `fallback`, or a route or parameter name used twice | rename or delete one |\n\nTwo real outputs, from pilot-2 candidates written by the 7B model and re-checked on `main`\n(`benchmarks/results/2026-09-27-pilot-2/raw/scored/…/mzizi/{button,badge}/seed-1/iter-01/candidate.mz`):\n\n```text\n{\"code\":\"MZ0313\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[16,35,16,37],\"say\":\"`icon` declares `height 48` but its class `size-14` renders 56px (N × 4) — write `height 56`, or drop the column and let the class say it once\",\"fix\":{\"span\":[16,35,16,37],\"replace\":\"56\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0106\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[15,8,15,16],\"say\":\"`...props` is a spread, and Mzizi has none: a component names each prop it reads and each attribute it sets. `props` has no equivalent — delete the line\",\"fix\":{\"span\":[15,1,16,1],\"replace\":\"\",\"confidence\":\"exact\"}}\n```\n\n## Contributing to the language\n\n- A language change needs an RFC in `design/`. Take the next number not used on `main` or\n claimed by an open PR: RFC-0005 is reserved, and RFC-0011 (handlers), RFC-0012 (the language\n harness), RFC-0013 (the core language) and RFC-0014 (Tier 2, draft) exist, so the next is\n RFC-0015.\n- A PR that adds or changes a language feature adds or updates its language-harness entry in\n `compiler/src/harness.rs` in the same PR: one entry in `FEATURES`, one `Code` per diagnostic\n code with a `trigger`, and tested examples. `cargo test --test harness` is the check.\n- A PR that changes what the language can do updates its row in `LANGUAGE-TRACKER.md` in the\n same PR, with evidence, and adds a `CHANGELOG.md` entry.\n- CI runs `cargo fmt -- --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test`,\n and `mz check` and `mz contract` over every file in `examples/` and `primitives/`, one file\n per call. Its `lowering` job runs `mz build` on `examples/registry.mz`, then `cargo test` and\n one request over a socket against the package, `mzprobe verify` of task B1, and `mz run` of\n every example program with its output diffed against `examples/<name>.expected`. `AGENTS.md`\n in the repo has the exact script.\n- Pull requests are squash-merged through the merge queue, and the squash commit keeps every\n commit message. State what you measured in the commit message.\n- The repo consumes nothing else in the org: no build step, test or script may reach into\n `mzizi-registry` or `agent-tools`.\n\n## See also\n\n- `LANGUAGE-TRACKER.md` (what Mzizi can and cannot do), `CHARTER.md` (v0.4),\n `design/RFC-0001` (syntax and the agent protocol), `RFC-0013` (the core language: programs),\n `RFC-0014` (Tier 2, draft), `RFC-0006` (contracts), `RFC-0008`\n (types, collections, records), `RFC-0009` (the comparison benchmark and the kill\n criterion), `RFC-0010` (contracts everywhere, draft), `RFC-0011` (handlers, the backend\n slice), `RFC-0012` (the language harness, draft), `benchmarks/prompts/mzizi-be-guide.md` (the\n one-file service guide) and `benchmarks/READINESS.md`.\n- The `mzizi-backend` skill: where a Mzizi service stands against the live TypeScript Workers.\n- The `mzizi-roots` skill: the Rust components built to support the language, its component\n model and where a component lowering would target.",
16
+ "body_mdx": "# Writing and checking Mzizi\n\n**Mzizi is a general-purpose programming language, built to make Rust better, the way\nTypeScript makes JavaScript better** (`CHARTER.md` v0.4, \"Mzizi: a general-purpose\nprogramming language\"). How: no borrows, lifetimes or ownership in the language you write\n(RFC-0001 §1.8), with the harness at the core, the layer an agent reads. \"Makes Rust better\"\nis the goal Phase 0 measures, not a result. Only a backend `service` lowers to Rust today; no\ncomponent does.\n\nRust is its platform, the way JavaScript is TypeScript's: Mzizi is designed to lower to Rust (RFC-0001 §5), and Mzizi\nRoots is its component model, the way React is JavaScript's. Its goal is to be the language\npeople choose instead of TypeScript, Python and C++: write Mzizi, get Rust underneath. That\nis a goal, not a result. Phase 0 has one goal: to build Mzizi as a programming language,\nmeasured against the best existing language for each kind of task (RFC-0009). Nothing has\nbeen measured against that yet.\nIts syntax, type system and diagnostics are designed for machine authorship.\n\nThe language lives in [`mzizi-dev/mzizi`](https://github.com/mzizi-dev/mzizi) (public). Say\nwhich thing is which when you describe it:\n\n- **The language:** its syntax, type system, semantics, and contracts as a language\n feature.\n- **The harness, the core of Mzizi:** the layer an agent reads and works through. It covers\n the language as an agent sees it (grammar, types, contracts, canonical form), the agent\n protocol (`mz check --agent`, its diagnostics and fixes; RFC-0001 §4), and the plugin\n host that the toolchain, the CLI, the MCP server and plugins attach to natively. Part of\n it exists today, in the toolchain: the `mz check --agent` output this skill teaches. The\n harness as a whole is designed, not built: RFC-0012 (a draft) specifies it, and there is no\n `mz harness` command and no plugin host, so never say it works. It lives in\n `mzizi-dev/mzizi`, the language repository, and the `agent-tools`\n packages (`@nyuchi/mzizi-cli`, `mzizi-mcp`, `fundi`) are its clients. It is not\n `benchmarks/harness/`, which is the Phase 0 benchmark scorer.\n- **The toolchain, which implements and supports the language:** `mz` (the compiler, its\n IR and its NDJSON diagnostics, `mz fix`, `mz contract`, `mz build`), `@nyuchi/mzizi-cli`, the MCP\n server at `mcp.mzizi.dev`, these skills, and the Phase 0 benchmark harness. They are\n meant to attach to the harness. Never call the compiler \"the language\".\n- **The components, which support the language:** Mzizi Roots and the registry\n (`mzizi-dev/mzizi-registry`), the language's UI layer. That is the companion\n `mzizi-roots` skill. Supplying the benchmark's UI tasks is one of their jobs, not what\n they are.\n\nThis skill describes `main` at `cf3a44e` (2026-11-01): 765 tests in the workspace\n(`cargo test --workspace`, 634 of them in the compiler crate) and 33,540 lines in\n`compiler/src`. Where this skill and the code disagree, **the code is the fact** (RFC-0003\n§7.1). Every example below is copied from a file in the repository that passes `mz check`,\n`mz contract` and, for a program, `mz run` there.\n\n## Check the tracker before you claim anything\n\n[`LANGUAGE-TRACKER.md`](https://github.com/mzizi-dev/mzizi/blob/main/LANGUAGE-TRACKER.md) in\nthe language repository is **the one list of what Mzizi can and cannot do** (owner, 2026-09-30).\nIf a capability is not ✅ there, Mzizi does not have it, whatever any other page says, this\nskill included. Its status column is what the code on `main` does, never what an RFC designs.\nRead it before you tell anyone Mzizi can do something, and before you write Mzizi that relies\non a construct this skill does not show.\n\n**In a `program` (RFC-0013, rows C1–C10, all ✅ on `main` since the 2026-10-08 releases):**\nexpressions and operators (arithmetic, comparison with `is`, `and` / `or`, text interpolation\n`\"{x}\"`), bindings (`let`, `var`, assignment), functions with parameters, return types and\nrecursion, control flow (`when` / `else when` / `else`, exhaustive `match`, `when` and `match`\nas values, `for each` over `range(a, to = b)` or a list, `while`, `break`, `continue`, early\n`return`), `int` and `float` with the numeric methods, text methods (`s[i]`, `slice`, `find`,\n`split`, `chars`, `parse_int`, `parse_float`), collections (`list(T)`, `map(K, V)`, `set(K)`,\nbracket literals, indexing that returns an option read with `otherwise`, `in`, the named\nfolds), records with methods, enums with columns, and errors (`result(T, E)`,\n`return error(e)`, prefix `try`, a `match` on a result, and `main` returning a result).\n`mz run` lowers a program to a dependency-free Rust package, builds it with Cargo and runs it.\n\n**Still missing (❌ or partial in the tracker):** modules and imports (P1; one top-level\ndeclaration per file), a standard library (P2), lowering of components (P3 is partial: a\n`service` and a `program` lower, no component does), Rust interop (P6), state and I/O (P8),\nconcurrency (P9), generics and interfaces (P12), a formatter, a language server, `mz test`,\npackage management and a published release (`mzizi-lang-compiler` 0.0.0, `publish = false`).\nErrors are not yet mapped back to `.mz` lines (P5, designed). Do not write Mzizi that needs\nany of these, and do not tell anyone it exists.\n\nFour pilots have run (two on 2026-09-27, two on 2026-10-29) and none showed an advantage for\nMzizi. The run that tests the charter's kill criterion has not happened, so never write that\nMzizi is faster or better.\n\n## What exists, and what does not\n\n**Built and tested, in the toolchain:** the lexer, the recovering parser, the name and type\nresolver, the agent diagnostic protocol (`mz check --agent`), `mz fix`, the content-addressed\nIR, `mz outline`, contract evaluation (`mz contract`), the language harness (`mz harness`,\nRFC-0012: one machine-readable definition of every feature, operator and diagnostic code),\nnine primitives written in Mzizi (`primitives/*.mz`), component, service and program examples\n(`examples/*.mz`, each program with the output `mz run` must print in `examples/<name>.expected`),\nand the Phase 0 benchmark harness and runner. All of it is gated in CI.\n\n**Programs (RFC-0013), built and tested.** A `program` file holds one program with `fn main`.\n`mz check` checks it, and `mz run` lowers it to Rust, builds it and runs it:\n\n```mz\n## Fibonacci numbers by recursion, in RFC-0013's foundation slice (§18.1): functions,\n## calls, `let`, `var`, `when`, integer arithmetic and `print`, and no loops yet.\n## `mz run examples/fib.mz` prints what fib.expected holds; CI compares the two.\nprogram fib\n\n fn main\n print(\"fib(9) is {fib(9)}\")\n let n = 20\n let f = fib(n)\n print(\"fib({n}) is {f}\")\n var total = 0\n total = total + fib(10)\n total = total + fib(11)\n print(\"fib(10) + fib(11) is {total}, and that is fib(12): {total is fib(12)}\")\n print(parity(f))\n end fn main\n\n fn fib(n: int): int\n when n < 2\n return n\n end\n return fib(n - 1) + fib(n - 2)\n end fn fib\n\n fn parity(n: int): text\n when n % 2 is 0 and n > 0\n return \"{n} is even\"\n else\n return \"{n} is odd, or not positive\"\n end\n end fn parity\n\nend program fib\n```\n\n`mz run examples/fib.mz` prints:\n\n```text\nfib(9) is 34\nfib(20) is 6765\nfib(10) + fib(11) is 144, and that is fib(12): true\n6765 is odd, or not positive\n```\n\nErrors are values. A function returns `result(T, E)`, fails with `return error(e)`, and a\ncaller handles the result with `match` or propagates it with prefix `try`:\n\n```mz\n## Checks ages: one function fails with an error, its callers handle it with `match` or\n## propagate it with `try`, and `main` itself returns a result (RFC-0013 §12).\nprogram errors\n\n enum age_problem\n negative say \"is below zero\"\n too_old say \"is above 150\"\n end\n\n fn main: result(none, age_problem)\n print(describe(42))\n print(describe(-3))\n print(describe(200))\n let total = try total_age(20, 22)\n print(\"20 and 22 make {total}\")\n match total_age(20, 151)\n case ok sum\n print(\"20 and 151 make {sum}\")\n case error problem\n print(\"20 and 151 were rejected: {problem} {problem.say}\")\n end\n end fn main\n\n fn check_age(n: int): result(int, age_problem)\n when n < 0\n return error(negative)\n end\n when n > 150\n return error(too_old)\n end\n return n\n end fn check_age\n\n fn describe(n: int): text\n match check_age(n)\n case ok age\n return \"age {age} is fine\"\n case error problem\n return \"{n} is rejected: {problem} {problem.say}\"\n end\n end fn describe\n\n fn total_age(a: int, b: int): result(int, age_problem)\n return try check_age(a) + try check_age(b)\n end fn total_age\n\nend program errors\n```\n\n**The backend slice (RFC-0011), built and tested:** a `service` with HTTP routes and handlers\n(`when`, `header`, `respond`), checked by `mz check`. `mz contract` runs a service in process\nand evaluates its `example` and `ensure` clauses. `mz build` lowers a service to a local\nRust + axum package, which CI compiles, tests and serves. That is **the whole of the lowering\ntoday:** no component lowers, and the package is a local server, with no Workers, WebAssembly\nor Containers target and no deployment. The `mzizi-be` benchmark arm, the probe crate\n`mzprobe` and backend task B1 exist; no backend episode has run.\n\n**Not built:** everything in the \"still missing\" list above, lowering of components,\nrendering, a release and a published binary. A component `.mz` file today is checked and its contract\nevaluated; **nothing runs it.** Do not tell anyone Mzizi \"compiles to Rust\" or is \"production\nready\"; say it is designed to lower to Rust, and that only a service does so today.\n\n**In progress, as design only:**\n\n- **The harness.** RFC-0012 (draft). Only the agent protocol exists (`mz check --agent`,\n `mz fix`, `mz contract`).\n- **Contracts everywhere.** RFC-0010 (draft) extends contracts to functions, handlers,\n services and records. Component contracts (RFC-0006) and service contracts (RFC-0011 §7) are\n evaluated. Of RFC-0010's codes, `MZ0606`, `MZ0611`, `MZ0612` and `MZ0613` exist, for\n services; `MZ0607`–`MZ0610` do not exist in the compiler.\n- **Request bodies** for a service (`body input: <record>` and `reject` blocks, RFC-0011 §4.3)\n are designed and not built. So is state (task B4).\n- **Modules.** There are no imports. Components are referenced by bare name\n (`primitives/confirm_bar.mz` uses `button` and `alert` with no import line). A reference to\n another component's enum (`button_variant.ghost`) is warning `MZ0502`, because it cannot be\n checked until modules exist.\n\n**Results, as they fell.** The first two Phase 0 pilots ran on 2026-09-27 and neither showed an\nadvantage for Mzizi (two more ran on 2026-10-29, also with no advantage). On the frontier model the Mzizi arm and the Rust UI-framework arm tied, with Mzizi about 8%\nfewer transcript tokens. On the ~7B open-weight model, the design target, Mzizi did worse on\nall three metrics. Neither pilot is the kill-criterion run (RFC-0009 §6), and that run has not\nhappened. The pilots were tests inside Phase 0 on a few registry UI components against\na Rust UI framework, not its goal. \"Designed for agents\" and \"aims to replace TypeScript, Python and C++\"\nare accurate; \"faster\" or \"better\" is not.\n\n## Getting `mz`\n\nThere is no release. Build it from source; the compiler crate has zero dependencies:\n\n```bash\ngit clone https://github.com/mzizi-dev/mzizi.git && cd mzizi/compiler\ncargo build --bin mz # ../target/debug/mz\ncargo run --bin mz -- check ../primitives/button.mz\n```\n\n**Never `cargo install mz`.** The `mz` crate on crates.io is an unrelated project, and the\ncompiler crate (`mzizi-lang-compiler`) is `publish = false`.\n\n## The commands\n\n`mz` takes exactly one file per call. `mz check ../primitives/*.mz` is a usage error (exit 2).\nLoop over files instead.\n\n| Command | Does |\n| ----------------------------------- | -------------------------------------------------------------------------------- |\n| `mz check <file>` | human-readable diagnostics |\n| `mz check --agent <file>` | NDJSON: one diagnostic per line, then one summary line. **Target this.** |\n| `mz fix <file>` | applies every `exact` fix in place, in one pass, then re-checks the file |\n| `mz contract <file>` | evaluates the file's `contract` block (only on a file that checks clean) |\n| `mz contract --agent <file>` | the same, as NDJSON; the summary adds `contract_clauses` and `contract_failures` |\n| `mz outline <file>` | the interface only (name, enums, props, contract subjects), as valid Mzizi |\n| `mz hash <file>` / `mz ir <file>` | the content-addressed root hash / every IR node with its hash and path |\n| `mz build <service.mz> --out <dir>` | lowers a service to a Rust + axum Cargo package in `<dir>` (only a clean file) |\n| `mz build <program.mz> --out <dir>` | lowers a program to a dependency-free Rust package in `<dir>` |\n| `mz run <program.mz>` | lowers a program, builds it with Cargo and runs it |\n| `mz harness version` | protocol, language version and the definition's SHA-256 |\n| `mz harness definition --agent` | every language-harness entry (features, operators, codes) as one line of JSON |\n| `mz harness entry <name>` | one entry, e.g. `mz harness entry let` |\n\nExit status: **0** no errors (warnings do not fail), **1** errors or a failed contract\nassertion, **2** a usage or I/O problem. Branch on the status; parse the NDJSON for detail.\n\n`mz run` is the exception, because 1 belongs to the program it runs. It exits with the\nprogram's own status: 0; 1 when `main` returns an error (`MZ0992`); 101 for a trap (`MZ0991`)\nor a panic; 141 when standard output is closed; 128 plus the signal that ended it; **3** when\nthe check failed and nothing ran, or the lowered Rust did not compile (`MZ0990`); and 2 for a\nusage, environment or I/O problem (`--agent` is not accepted, a file that is not a `program`).\n\nOn a **service**, `mz contract` runs the service in process (no lowering, no socket), and the\nsummary adds `contract_tested`, the number of generated requests its `ensure` clauses were\ntested over. `mz outline`, `mz hash` and `mz ir` do not cover services yet and exit 2. `mz\nbuild` on a component exits 2 (\"components do not lower yet\"), and on a file with errors it\nprints them, builds nothing and exits 1.\n\n`check` and `contract` are deliberately separate: a compile error is not a defect, and a\ncontract failure is. Your file is done when **both** `mz check --agent` and\n`mz contract --agent` exit 0.\n\n## The loop\n\n1. Write the file.\n2. `mz check --agent file.mz`. If it exits 0, go to 5.\n3. `mz fix file.mz` to apply every `exact` fix. Read each remaining diagnostic's `say`; apply\n `guess` fixes only after checking them.\n4. Fix everything reported, then go to 2. Errors are independent, so fix them all before\n re-running; do not fix one and re-run to \"see the next\".\n5. `mz contract --agent file.mz`. `MZ0603` means an assertion is false (a defect in the\n component or the contract); `MZ0605` means it could not be evaluated, which **counts as a\n failure**.\n\n## Reading the NDJSON\n\nThis file is wrong on purpose. It is the example in `benchmarks/prompts/mzizi-guide.md`, which\n`benchmarks/prompts/verify-guide.sh` checks against the real compiler.\n\n```mz\n## WRONG ON PURPOSE: three mistakes.\ncomponent tag\n\n enum tag_size\n snug class \"h-13 px-3\"\n end\n\n prop onPick: event(none)\n prop size: tag_size = snug\n\n view\n row\n slot = \"tag\"\n class = \"rounded-full {size.class}\"\n tap = on_pick\n end\n end\n\n contract\n slot is \"tag\"\n tag_size.snug height 52\n end\n\nend\n```\n\n`mz check --agent tag.mz` exits 1 and prints exactly:\n\n```text\n{\"code\":\"MZ0101\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[8,8,8,14],\"say\":\"`onPick` is not snake_case — Mzizi names are snake_case, so write `on_pick`\",\"fix\":{\"span\":[8,8,8,14],\"replace\":\"on_pick\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0602\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[21,26,21,28],\"say\":\"a contract clause needs a predicate (is / in / contains / not_empty / at_least / uses / min_height / shows), found `52`\",\"fix\":{\"span\":[21,26,21,26],\"replace\":\"is \",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0208\",\"severity\":\"error\",\"file\":\"tag.mz\",\"span\":[24,1,24,4],\"say\":\"top-level blocks close with their name: write `end component tag`\",\"fix\":{\"span\":[24,1,24,4],\"replace\":\"end component tag\",\"confidence\":\"exact\"}}\n{\"summary\":true,\"errors\":3,\"warnings\":0,\"exact_fixable\":3,\"ms\":0}\n```\n\n`mz fix tag.mz` then prints `mz: applied 3 exact fixes; 0 errors (0 exact-fixable) remain`, and\n`mz contract --agent tag.mz` reports `\"contract_clauses\":2,\"contract_failures\":0`.\n\n- `code`, `severity` (`error` or `warning`), `file` (the path you passed), `span`\n `[line, col, end_line, end_col]` (1-indexed, end exclusive) and `say` are always present.\n- `say` quotes the offending source, so you need not re-read the file to understand it.\n- `fix` is optional: put `replace` at `fix.span`. An empty `replace` is a deletion. `exact`\n is safe to apply blind; `guess` has the right location and an inferred replacement.\n- The last line is always the summary.\n\n## Syntax essentials\n\nOne file holds one component, or one service (see \"Services\" below). There is one form per\nintent: if a construct is not shown here or in the RFCs, assume it does not exist. The order\ninside a component is doc lines, `use`, `enum` and `record`, `prop`, `view`, `fn`, `contract`.\n\nA complete primitive, after `components/registry/n2-primitives/badge.mz` in mzizi-registry (the\n`.mz` syntax and the `contract` are that file's, unchanged). The `class` string is **not** that\nfile's: the registry's `badge.mz` still carries the older `h-5`, `px-2 py-0.5` and `gap-1`\n(checked on registry `main` and `staging`, 4 November 2026), which are under the spacing floor\n(14px inset, 7px gaps, 1.5 leading; the `mzizi-design` skill). Here the class uses the floor-clean\nvalues of the registry's `badge.tsx` on `main` (`h-6`, `gap-2`, `px-3.5`; `py-0.5` is unchanged from\nthat file, and the registry holds a chip's block axis by its height), so the example is a model for new code. Do not copy the\nregistry's `badge.mz` classes; a component you write keeps the floor, or carries a\n`mzizi-spacing-exempt: <reason>` comment:\n\n```mz\n## A small status label. Not interactive, so no touch floor applies.\n## Corpus reference: n2-primitives/badge.tsx.\ncomponent badge\n\n enum badge_variant\n default class \"bg-primary text-primary-foreground\"\n secondary class \"bg-secondary text-secondary-foreground\"\n destructive class \"bg-destructive/10 text-destructive dark:bg-destructive/20\"\n outline class \"border-border text-foreground bg-input/30\"\n ghost class \"hover:bg-muted hover:text-muted-foreground\"\n link class \"text-primary underline-offset-4 hover:underline\"\n end\n\n prop variant: badge_variant = default\n prop label: text\n\n view\n mark\n slot = \"badge\"\n portal = \"https://mzizi.dev/components/badge\"\n variant = variant\n class = \"inline-flex h-6 w-fit shrink-0 items-center justify-center gap-2 overflow-hidden rounded-md border border-transparent px-3.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all {variant.class}\"\n text = label\n end\n end\n\n contract\n every badge_variant class not_empty\n badge_variant.default class contains \"bg-primary\"\n slot is \"badge\"\n end\n\nend component badge\n```\n\n- **Shape.** `component <name>` opens the file and `end component <name>` closes it, repeating\n the name. Every other block (`enum`, `record`, `view`, `contract`, each element, each `when`,\n each `for each`, each `fn`) closes with a bare `end`. Never `end enum` or `end view`.\n- **Comments.** `##` to end of line is the only comment form. `//` and `/* */` are errors.\n- **Names** are `snake_case`. `onRemove` is `MZ0101`, with the snake form as its `exact` fix.\n- **Enums are variant tables.** One row per variant: its name, then `column value` pairs. Every\n variant has every column (`MZ0303` otherwise). A column can carry meaning beyond styling, as in\n `primitives/alert.mz`, where `announce` ties the ARIA role to the severity:\n\n ```mz\n enum alert_variant\n default class \"bg-card text-card-foreground\" announce \"status\"\n destructive class \"bg-card text-destructive\" announce \"alert\"\n warning class \"bg-card text-[var(--status-warning,#FFD740)]\" announce \"alert\"\n info class \"bg-card text-[var(--status-info,#00B0FF)]\" announce \"status\"\n end\n ```\n\n- **Heights come from the class.** A variant's `height` is the pixels its `h-N` or `size-N`\n class renders (`N × 4`). You may leave the `height` column out. A written `height` that\n disagrees with the class is `MZ0313`, and its `exact` fix is the rendered number.\n- **Props.** `prop <name>: <type>`, optionally `= <default>`. The types are an enum name\n (`= default`), `text`, `bool`, `int`, `event(none)`, `event(<type>)`, `list(<type>)`\n (omitted means empty), `option(<type>)` (omitted means none) and a record name. Never\n `option(list(T))` (`MZ0703`).\n- **The view** holds exactly one element tree. An element is a bare word on its own line,\n closed by `end`. Inside it, each line is an attribute `name = value` (the `=` is required),\n a child element, a `when` block, a `for each`, or `nothing`. Strings interpolate with\n `{...}`; there is no `+`.\n- **Conditionals.** `when x` … `end`, `when not x`, `when v is <variant>`, and for an option or\n a list `when x is none` … `else` … `end`. There is no `if` (`MZ0407`, `exact` fix `when`)\n and no `match`. From `primitives/avatar.mz`:\n\n ```mz\n when image is none\n row\n slot = \"avatar-fallback\"\n class = \"flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground {size.text_class}\"\n text = initials\n end\n else\n picture\n slot = \"avatar-image\"\n source = image\n alt = alt\n class = \"aspect-square size-full rounded-full object-cover\"\n end\n end\n ```\n\n- **Lists** render with `for each item in items`, whose first line is its `key = item.<field>`.\n- **Capabilities.** `use motion` if the component animates (`primitives/spinner.mz`). Nothing\n else needs `use`.\n- **Events.** Bind with `tap = on_tap` / `change = on_change`. A parameterless `fn` can `emit`\n an event prop, as in `examples/connectivity_bar.mz`:\n\n ```mz\n fn retry\n emit on_state_change(syncing)\n end\n ```\n\n- **No React carry-overs.** `...props` spreads are `MZ0106` (`exact` fix deletes the line).\n An `as_child` prop is warning `MZ0312`: delete it, and keep what the `as_child = false`\n branch renders. `className` pass-through has no equivalent.\n\n`benchmarks/prompts/mzizi-guide.md` is the complete one-file guide the benchmark gives a model,\nincluding records, `for each` and a React-to-Mzizi table. Read it before writing a component.\n\n## Services (the backend slice, RFC-0011)\n\nA backend program is one file holding one `service`, closed by `end service <name>`. Every\ninner block (`record`, `route`, `fallback`, `when`, `contract`) closes with a bare `end`. The\norder is doc lines, service-level `header` lines, `record`s (and `enum`s), `route`s,\n`fallback`, `contract`. There are no imports, no `main`, no server setup and no port.\n\n`examples/registry.mz`, the example service (22 clauses, all holding; its `ensure` clauses\ntested over 61 generated requests):\n\n```mz\n## A registry of two components: routes, methods, redirects and a JSON 404.\n## RFC-0011's worked example, shaped like the API gateway's `/v1/ui` routes (RFC-0009 §2.4, B1).\nservice registry\n\n header \"x-mzizi-source\" \"fixture\"\n\n record problem\n field error: text\n end\n\n record health\n field status: text\n end\n\n record item\n field name: text\n end\n\n route health_check\n get \"/v1/health\"\n respond 200 json health status \"ok\"\n end\n\n route ui_list\n get \"/v1/ui\"\n respond 200 file \"fixtures/ui.json\"\n end\n\n route ui_item\n get \"/v1/ui/{name}\"\n when name in \"badge\" \"button\"\n respond 200 json item name name\n end\n header \"cache-control\" \"private, no-cache, no-store, max-age=0, must-revalidate\"\n respond 404 json problem error \"Not found\"\n end\n\n fallback\n header \"cache-control\" \"private, no-cache, no-store, max-age=0, must-revalidate\"\n respond 404 json problem error \"Not found\"\n end\n\n contract\n example get \"/v1/health\" body.status is \"ok\"\n example get \"/v1/ui\" status is 200\n example get \"/v1/ui\" header \"content-type\" is \"application/json\"\n example get \"/v1/ui/button\" status is 200\n example get \"/v1/ui/button\" body.name is \"button\"\n example get \"/v1/ui/does-not-exist\" status is 404\n example get \"/v1/ui/does-not-exist\" body.error is \"Not found\"\n example get \"/v1/ui/does-not-exist\" header \"cache-control\" is \"private, no-cache, no-store, max-age=0, must-revalidate\"\n example head \"/v1/ui/button\" status is 200\n example head \"/v1/ui/button\" body is \"\"\n example options \"/v1/ui\" status is 204\n example options \"/v1/ui\" header \"allow\" is \"GET, HEAD, OPTIONS\"\n example delete \"/v1/ui/button\" status is 405\n example delete \"/v1/ui/button\" body is \"\"\n example get \"/v1/ui/\" status is 308\n example get \"/v1/ui/?limit=2\" header \"location\" is \"/v1/ui?limit=2\"\n example get \"/nope\" status is 404\n example get \"/nope\" header \"content-type\" is \"application/json\"\n ensure status in 200 204 308 404 405\n ensure header \"x-mzizi-source\" is \"fixture\"\n ensure when status is 404 then body.error is \"Not found\"\n ensure when status is 405 then header \"allow\" is \"GET, HEAD, OPTIONS\"\n end\n\nend service registry\n```\n\n- **A route** is `route <name>`, then exactly one `<method> \"<pattern>\"` line, then zero or\n more `query <name>: option(<scalar>)` lines, then statements, then `end`. The methods are\n `get`, `head`, `post`, `put`, `patch`, `delete` and `options`, lower case; a second method on\n the same path is a second route.\n- **A pattern** is `/` or `/`-separated segments of lower-case letters, digits and `-._~`,\n or a whole-segment `{name}`, which binds `name` as `text`. No trailing slash, no empty\n segment, no `?`. A literal segment beats a parameter at the same position.\n- **A query parameter is always an option:** `query limit: option(int)`. `option(int)` is\n `-`? then ASCII digits, so `\"0x10\"`, `\"1e2\"`, `\" 7 \"` and `\"1.5\"` are `none`; `option(bool)`\n is exactly `true` or `false`. Bad input is absence, never an error.\n- **A handler has three statements**, and every path through it must end in `respond`:\n\n | Statement | Means |\n | -------------------------------- | --------------------------------------------------------- |\n | `when <cond>` … [`else` …] `end` | a branch; with no `else`, a false condition falls through |\n | `header \"<name>\" \"<value>\"` | set a header on the response this path sends |\n | `respond <status> [<body>]` | send the response; nothing after it on the path runs |\n\n A condition is `p` / `not p` (a `bool`), `p is <literal>`, `p in <literal> …`,\n `p at_least <n>` / `p at_most <n>` (an `int`), or `p is none` … `else` (an option, which is\n narrowed in the `else`). There is no `if`, `==`, `return`, assignment, loop or call.\n\n- **Bodies:** none; `json <record> <field> <value> …` (every field once, a literal or a\n parameter, no punctuation; `application/json`); `text \"<string>\"` (may interpolate a\n parameter as `{name}`); or `file \"<path>\"` (a fixture beside the `.mz` file, embedded at\n build time). A response record's fields are scalars or options of scalars. `204`, `304` and\n `1xx` take no body.\n- **Headers** are lower case. A service-level `header` applies to every response, runtime\n ones included; a route's own `header` for the same name wins. `content-length` and\n `transfer-encoding` belong to the server.\n- **The runtime answers what HTTP defines, so do not write it:** `HEAD` for a `get` route\n (its status and headers, no body); `OPTIONS` (`204` with `allow`, e.g. `GET, HEAD, OPTIONS`);\n any other undeclared method on a matched path (`405` with the same `allow`, no body); and a\n trailing slash or `//` (`308` to the canonical path, query kept, never off-site). A path no\n route matches runs `fallback`, or gets an empty `404` when there is none.\n- **The contract:** `example <method> \"<target>\" <facet> <predicate>` sends one request;\n `ensure [when <facet> <predicate> then] <facet> <predicate>` must hold for every generated\n request. A facet is `status`, `header \"<name>\"`, `body` or `body.<field>`; a predicate is\n `is <value>`, `in <value> …`, `not_empty`, `contains \"<text>\"`, `at_least <n>` or\n `at_most <n>`. `header \"<name>\" is none` asserts absence and `body is \"\"` an empty body.\n `ensure` is **tested** over the generated set, never proven.\n\nThis file is wrong on purpose. It is the example in `benchmarks/prompts/mzizi-be-guide.md`:\n\n```mz\n## WRONG ON PURPOSE: three mistakes.\nservice shop\n\n record problem\n field error: text\n end\n\n route item\n GET \"/v1/items/{id}/\"\n when id is \"1\"\n respond 200 text \"one\"\n end\n end\n\nend service shop\n```\n\n`mz check --agent candidate.mz` exits 1 and prints:\n\n```text\n{\"code\":\"MZ0613\",\"severity\":\"warning\",\"file\":\"candidate.mz\",\"span\":[2,9,2,13],\"say\":\"`service shop` has no `contract` block — its behaviour is unverified (RFC-0010 §8)\"}\n{\"code\":\"MZ0801\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[9,5,9,8],\"say\":\"`GET` is not a method — a route declares one of get, head, post, put, patch, delete, options\",\"fix\":{\"span\":[9,5,9,8],\"replace\":\"get\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0802\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[9,9,9,26],\"say\":\"`\\\"/v1/items/{id}/\\\"` ends with `/` — patterns have no trailing slash; the runtime redirects `…/` to the path without it\",\"fix\":{\"span\":[9,9,9,26],\"replace\":\"\\\"/v1/items/{id}\\\"\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0804\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[13,3,13,6],\"say\":\"a path through `route item` reaches `end` without a `respond` — when the `when` on line 10 does not respond on both branches; every path must respond\"}\n{\"summary\":true,\"errors\":3,\"warnings\":1,\"exact_fixable\":2,\"ms\":0}\n```\n\n`mz fix` applies the two `exact` fixes. `MZ0804` needs a decision: add\n`respond 404 json problem error \"Not found\"` after the `when`'s `end`, so the false path\nresponds too.\n\n**Building it.** `mz build registry.mz --out <dir>` checks the file and, with zero errors,\nwrites `Cargo.toml`, `src/main.rs` and the fixtures. The package is its own workspace root,\ngenerated output that is never committed, and depends on `axum =0.8.9` and `tokio =1.53.1`\n(tests add `tower`, `http-body-util` and `serde_json`). Every `example` becomes one\n`#[tokio::test]` (`cargo test` in the package); `ensure` clauses are evaluated by\n`mz contract` only. `cargo run --release --manifest-path <dir>/Cargo.toml` serves\n`127.0.0.1:$PORT` (8080 by default) and prints `listening on http://127.0.0.1:<port>`.\n\n## Contracts\n\nEvery component carries a `contract … end` block: one assertion per line,\n`<subject> <predicate>`, no colon. A file with no contract is warning `MZ0501`. It is checked\nagainst the file's own enums, view attributes and prop defaults (RFC-0006). From\n`primitives/button.mz`:\n\n```mz\n contract\n every button_size height at_least 48\n button_size.default height is 56\n button_size.sm height is 48\n button_variant.default class contains \"bg-primary\"\n slot is \"button\"\n end\n```\n\n| Subject | Means |\n| --------------------------- | -------------------------------------------------- |\n| `every <enum> <column>` | that column in every variant |\n| `<enum>.<variant> <column>` | one cell |\n| `<attribute>` | the root view element's attribute (`slot`, `role`) |\n| `<prop>` | the prop's default (only a prop that has one) |\n| `<element> \"<text>\"` | the element carrying that text |\n| `when <name>` | the `when` branch that mentions that name |\n\nPredicates: `is <value>` (exactly as written: `height is \"52\"` fails against `height 52`),\n`contains \"<s>\"`, `not_empty`, `at_least <n>`, `in \"<a>\" \"<b>\"`, `uses \"--token\"`,\n`uses <component>` (no subject), `shows <element> \"<t>\"` (after `when <name>`) and\n`min_height <n>`. `examples/connectivity_bar.mz` uses the last two:\n\n```mz\n when offline shows button \"Retry\"\n button \"Retry\" min_height 48\n```\n\nAssert what the source guarantees: every interactive size clears 48px, the `slot`, and the key\ncells. An assertion on a prop with no default is `MZ0605` (unevaluable), which fails.\n\n`mz contract` checks nothing rendered. The rendered half is `benchmarks/harness`, which diffs a\n`.mz` component's variants, defaults, touch heights and `data-slot` set against the Rust\nreference. Mzizi Roots components carry the same clause grammar as `pub const CONTRACT` and\ncheck it against their server-rendered markup (see the `mzizi-roots` skill).\n\nThe direction (RFC-0010, draft) is that **everything built carries a contract**: components,\nlanguage functions and backend handlers. The component form and the service form (\"Services\"\nabove) are evaluated today; functions have no contracts because they have no bodies.\n\n## Diagnostic codes you will meet\n\nThe ranges are `MZ01xx` lexing and names, `MZ02xx` blocks and closers, `MZ03xx` declarations,\n`MZ04xx` the view, `MZ05xx` lints, `MZ06xx` contracts, `MZ07xx` types and resolution, and\n`MZ08xx` services and handlers. The full tables are RFC-0001 §4.7, RFC-0006 §8, RFC-0008\n(types), RFC-0010 §6 and RFC-0011 §9. A service reuses `MZ0701`, `MZ0707`, `MZ0708` and\n`MZ0710` for unknown names and un-narrowed options.\n\n| Code | Severity | Written | Repair |\n| ------------------- | -------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |\n| `MZ0101` | error | a name that is not `snake_case` | `exact`: the snake form |\n| `MZ0105` | error | `list<T>`, `T[]`, `[T]` | `exact`: `list(T)` |\n| `MZ0106` | error | `...props`, `{...props}`, `..attributes` | `exact`: delete (the whole line when alone) |\n| `MZ0204` | error | a block never closed | `guess`: insert the `end`s |\n| `MZ0206`–`MZ0208` | error | a wrong closer (`end view`, a bare `end` for `component`) | `exact`: rewrite the whole closer line |\n| `MZ0303` | error | a variant missing a column another variant has | add the column |\n| `MZ0312` | warning | `prop as_child: bool` (React `asChild`) | `exact` when unread: delete the prop |\n| `MZ0313` | error | `icon class \"size-14\" height 48` | `exact`: the rendered px (`56`) |\n| `MZ0406` | error | `class \"flex\"` (attribute without `=`) | insert `=` (`exact` for known attribute words) |\n| `MZ0407` | error | `if open` | `exact`: `when open` |\n| `MZ0408` | error | an attribute directly under `view` | move it inside the root element |\n| `MZ0409` | error | `span class = \"x\"` (more after an element word) | put the element word on its own line |\n| `MZ0501` | warning | no `contract` block | write one |\n| `MZ0502` | warning | `<other>_enum.variant` from another component | none until modules |\n| `MZ0602` | error | a clause with no predicate (`height 52`) | `exact`: insert `is` (needs an operand) |\n| `MZ0603` | error | **an assertion that does not hold** | fix the component or the clause |\n| `MZ0605` | error | an assertion that cannot be evaluated | assert something the file defines |\n| `MZ0606` | error | an `example` status that an `ensure status in …` rules out | fix one of the two |\n| `MZ0611` / `MZ0612` | error | a service `ensure` / `example` that does not hold | `say` quotes the request that broke it |\n| `MZ0613` | warning | a service with no or an empty `contract` block | write one |\n| `MZ0701` | error | an unknown type (`strng`) | alias or nearest-name fix |\n| `MZ0706` | error | a default that does not fit its type | varies |\n| `MZ0707` / `MZ0708` | error | an unknown name / variant, column or field | nearest-name fix |\n| `MZ0710` | error | an option used without `when … is none … else` | narrow it first |\n| `MZ0711` / `MZ0712` | error | a wrong kind of value / a `when` that does not fit its type | see `say` |\n| `MZ0801` | error | a route not opening with `<method> \"<pattern>\"`, `GET`, a second method line | `exact` for `GET`; else a `guess` |\n| `MZ0802` | error | a bad pattern: trailing `/`, empty segment, `?`, no leading `/` | `exact` where the repair is certain |\n| `MZ0803` | error | two routes with one method and the same pattern | delete or change one |\n| `MZ0804` | error | a handler path that reaches `end` without `respond` | add the `respond` |\n| `MZ0805` | error | a line after `respond` that can never run | `exact`: delete it |\n| `MZ0806` | error | a bad `respond`: no status, not 100–599, a body on `204` | `exact` deletes a body that is not allowed |\n| `MZ0807` | error | a bad `header`: `\"Cache-Control\"`, set twice, server-owned | `exact`: the lower-case name |\n| `MZ0808` | error | a `json` literal with a missing, repeated or ill-typed field (unknown is `MZ0708`) | give every field once |\n| `MZ0809` | error | `query limit: int` | `exact`: `option(int)` |\n| `MZ0810` | error | a `file \"…\"` fixture that does not exist | add the file or fix the path |\n| `MZ0811` | error | a line a service or handler cannot hold (`return`, `=`, `view`) | see `say` (`is` for `=` is `exact`) |\n| `MZ0812` | error | a second `fallback`, or a route or parameter name used twice | rename or delete one |\n\nTwo real outputs, from pilot-2 candidates written by the 7B model and re-checked on `main`\n(`benchmarks/results/2026-09-27-pilot-2/raw/scored/…/mzizi/{button,badge}/seed-1/iter-01/candidate.mz`):\n\n```text\n{\"code\":\"MZ0313\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[16,35,16,37],\"say\":\"`icon` declares `height 48` but its class `size-14` renders 56px (N × 4) — write `height 56`, or drop the column and let the class say it once\",\"fix\":{\"span\":[16,35,16,37],\"replace\":\"56\",\"confidence\":\"exact\"}}\n{\"code\":\"MZ0106\",\"severity\":\"error\",\"file\":\"candidate.mz\",\"span\":[15,8,15,16],\"say\":\"`...props` is a spread, and Mzizi has none: a component names each prop it reads and each attribute it sets. `props` has no equivalent — delete the line\",\"fix\":{\"span\":[15,1,16,1],\"replace\":\"\",\"confidence\":\"exact\"}}\n```\n\n## Contributing to the language\n\n- A language change needs an RFC in `design/`. Take the next number not used on `main` or\n claimed by an open PR: RFC-0005 is reserved, and RFC-0011 (handlers), RFC-0012 (the language\n harness), RFC-0013 (the core language) and RFC-0014 (Tier 2, draft) exist, so the next is\n RFC-0015.\n- A PR that adds or changes a language feature adds or updates its language-harness entry in\n `compiler/src/harness.rs` in the same PR: one entry in `FEATURES`, one `Code` per diagnostic\n code with a `trigger`, and tested examples. `cargo test --test harness` is the check.\n- A PR that changes what the language can do updates its row in `LANGUAGE-TRACKER.md` in the\n same PR, with evidence, and adds a `CHANGELOG.md` entry.\n- CI runs `cargo fmt -- --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test`,\n and `mz check` and `mz contract` over every file in `examples/` and `primitives/`, one file\n per call. Its `lowering` job runs `mz build` on `examples/registry.mz`, then `cargo test` and\n one request over a socket against the package, `mzprobe verify` of task B1, and `mz run` of\n every example program with its output diffed against `examples/<name>.expected`. `AGENTS.md`\n in the repo has the exact script.\n- Pull requests are squash-merged through the merge queue, and the squash commit keeps every\n commit message. State what you measured in the commit message.\n- The repo consumes nothing else in the org: no build step, test or script may reach into\n `mzizi-registry` or `agent-tools`.\n\n## See also\n\n- `LANGUAGE-TRACKER.md` (what Mzizi can and cannot do), `CHARTER.md` (v0.4),\n `design/RFC-0001` (syntax and the agent protocol), `RFC-0013` (the core language: programs),\n `RFC-0014` (Tier 2, draft), `RFC-0006` (contracts), `RFC-0008`\n (types, collections, records), `RFC-0009` (the comparison benchmark and the kill\n criterion), `RFC-0010` (contracts everywhere, draft), `RFC-0011` (handlers, the backend\n slice), `RFC-0012` (the language harness, draft), `benchmarks/prompts/mzizi-be-guide.md` (the\n one-file service guide) and `benchmarks/READINESS.md`.\n- The `mzizi-backend` skill: where a Mzizi service stands against the live TypeScript Workers.\n- The `mzizi-roots` skill: the Rust components built to support the language, its component\n model and where a component lowering would target.",
17
17
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/mzizi-language",
18
18
  "files": []
19
19
  },
20
20
  {
21
21
  "name": "mzizi-roots",
22
22
  "category": "build",
23
- "description": "Use this skill for anything involving Mzizi's components: using them, starting an app on them, or contributing one. Mzizi Roots is Mzizi's own components in Rust (UI on Dioxus today, moving to Mzizi's own render(props); server components) from mzizi-dev/mzizi-registry. Covers `cargo add mzizi-roots` / `mzizi-roots-server`, what mzizi_get_component returns (Rust first), the node-to-crate map, `pub const CONTRACT`, the contracts in the registry's contracts/ for the Dashboard Standard (app/, 35 components) and the Discover Standard (discover/, 16 components; no script, server-filled shells): read one before building or changing a @bundu/ui component. Also no-JS fallback, density and floors (49px hit region, 17px body, 14px inset, 7px gaps) and brand-overlay rules, the frontend paths (pure .astro via `mzizi add --target astro`, Astro with Roots, pure Rust; Next.js adopts the .tsx), reuse before build, and adding a component (node, .rs, .tsx, registry.json, the gates). Files are the data layer: no database.",
24
- "body_mdx": "# Mzizi Roots — the Rust build of Mzizi's components\n\n**Mzizi Roots** is Mzizi's own components in Rust: UI components, and server components as\nhost-agnostic Rust. The UI crates are built on Dioxus 0.7 today and are moving to Mzizi's own\nRust renderer (a pure `render(props) -> String`, with behaviour attached in the browser through\n`web-sys`). Do not write new Dioxus code for a Roots component. They are built to support the Mzizi language (the\n`mzizi-language` skill) as its component model, the way React is JavaScript's; they are not\nthe language. They live in\n[`mzizi-dev/mzizi-registry`](https://github.com/mzizi-dev/mzizi-registry) and are compiled by\nthe crates in its `mzizi-rs/` workspace. The plan is `docs/roots/RFC-roots.md` there.\n\n**Rust first.** Where a Rust implementation exists, present it and use it first. The React/TSX\ncomponents are **the React build**: they keep working for existing React apps, and they are\ndeprioritised, so no new work goes into them.\n\nThis skill describes registry `main` at v4.18.0 (`0d17818`, 2026-11-01) and its `contracts/`\n(77 component contracts in four families, and the `lib/` family), the crates at **0.7.0**\n(registry v4.18.0, on crates.io since 2026-11-01; 0.7.0 adds two crates, `mzizi-resilience`,\nthe N5 core, and `mzizi-chaos-engine`, the N8 seeded chaos engine, from registry PRs #584\nand #586; 0.6.0 before it carried the `--assurance-*` and `--attestation-*` roles in the generated\n`mzizi-tokens` pack, from mzizi-registry #578, and 0.5.0 the `mzizi-mongodb` security fixes from mzizi-registry #567; the UI crates\notherwise match 0.4.0, on crates.io since 2026-10-20 with `markdown-renderer` 1.2.0 and 1.2.1\nfrom mzizi-registry #483 and #517), and `mzizi-mcp` 0.13.0. It replaces three 0.7.0 skills:\n`scaffold-component` (now \"Contributing a component\"), `simplify` (now \"Reuse before build\")\nand the frontend half of `ecosystem-app-setup` (now \"Starting an app\"). Tokens, brand and the\nUbuntu constraints are in the `mzizi-design` skill.\n\n## Install\n\nTwo umbrella crates with no code of their own re-export the node crates behind features:\n\n```sh\ncargo add mzizi-roots # UI: tokens + ui + brand + shell (all default features)\ncargo add mzizi-roots-server # server: assurance + fundi + docs + discovery\n```\n\nTake only what you use:\n\n```toml\n[dependencies]\nmzizi-roots = { version = \"0.7\", default-features = false, features = [\"brand\"] }\n```\n\nTo follow `main`, or pin a commit, install from git. Cargo finds a crate by name anywhere in the\nrepository's workspace:\n\n```sh\ncargo add mzizi-roots --git https://github.com/mzizi-dev/mzizi-registry\n```\n\n```toml\nmzizi-brand = { git = \"https://github.com/mzizi-dev/mzizi-registry\", rev = \"<commit>\" }\n```\n\nA single node crate works too (`cargo add mzizi-ui`). The crates depend on `dioxus` with\n`macro`, `signals`, `hooks` and `html` only, so the app that mounts them chooses web, desktop,\nmobile or server rendering.\n\n`mzizi add <name> --target rust` (`@nyuchi/mzizi-cli`) records the crate and prints the\n`cargo add` rather than copying a `.rs` file into your project: a copied component is a fork\nof a crate module.\n\n## What the MCP returns\n\n`mzizi_get_component` on `https://mcp.mzizi.dev/mcp` (free, no account; `mzizi-mcp` 0.11.0) is\nRust first. It returns `{name, renamedFrom?, lead, note, rust, react, docs?, versions?}`:\n\n- `lead` is `\"rust\"` when the component has a Roots implementation, else `\"react\"`; `note`\n says which build to use.\n- `rust` is the registry's `/v1/rs/<name>` answer with the install first: `crate`\n (`{name, registry, git}`), `install.cargo` (`cargo add <crate>`), `install.pinned`\n (`cargo add <crate> --git https://github.com/mzizi-dev/mzizi-registry --rev <pin>`, which\n builds exactly the source shown), `module` (e.g. `mzizi_brand`), `contract` (the\n `contract … end` text, or `null`), and `files[]` with the `.rs` source inline. With no Rust\n it is `null`.\n- `react` is the React build: `build: \"react\"`, `status: \"deprioritised\"`, `install` (the\n `npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>` line), then the `/v1/ui/<name>`\n body. It is no longer under a `component` key (that was 0.10.x).\n\n`mzizi_list_components` rows carry `rustCrate` when a component has Rust, the response carries\n`withRust`, and `rust: true` lists only those. `mzizi_search` hits carry `rustCrate` too. For\n`mzizi-hero-stat` the answer is `lead: \"rust\"`, crate `mzizi-brand`, and the four-clause\ncontract shown under \"Contracts\" below.\n\n## The crates\n\nOne crate per registry node that has Rust. The map is `mzizi-rs/crate-for-node.json`, and\n`pnpm rust:generate` refuses a node with Rust in it that no crate claims.\n\n| Node directory | Crate | Umbrella | What it holds |\n| ------------------- | ----------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `n1-tokens` | `mzizi-tokens` | `mzizi-roots` (always on) | the 21-family palette and scales as Rust consts, generated from `lib/tokens/palette.source.ts` |\n| `n2-primitives` | `mzizi-ui` | `mzizi-roots` (`ui`) | alert, avatar, badge, button, card, chart, input, label, markdown renderer, progress, safe-area frame, separator, skeleton, status badge |\n| `n3-brand` | `mzizi-brand` | `mzizi-roots` (`brand`) | alert banner, avatar stack, cover header, empty state, escalation card, gauge card, hero stat, meta tile, stats row, success screen, suitability card, user card (all `mzizi-*`) |\n| `n7-shell` | `mzizi-shell` | `mzizi-roots` (`shell`) | bottom nav, command palette, connectivity bar, deep-link handler, footer, mini-app runtime, notification center, persistent player, route guard, theme provider, toast provider, update prompt |\n| `n8-assurance` | `mzizi-assurance` | `mzizi-roots-server` (`assurance`) | probes, telemetry, the OTLP exporter, a11y and RTL conformity checks, alert and incident engines |\n| `n9-fundi` | `mzizi-fundi` | `mzizi-roots-server` (`fundi`) | the Fundi reporter and learning loop (client side) |\n| `n10-documentation` | `mzizi-docs` | `mzizi-roots-server` (`docs`) | the docs API and routing, AI context, the changelog and docs renderers (brings in `dioxus`) |\n| `n11-discovery` | `mzizi-discovery` | `mzizi-roots-server` (`discovery`) | page metadata and Schema.org JSON-LD |\n\nN5 resilience has a crate since 0.7.0: `mzizi-resilience` (circuit breaker, retry backoff,\nbulkhead, rate limiter, fallback chain and deadline, each a sans-IO state machine with a\n`contracts/lib/` contract). Depend on it directly; no umbrella re-exports it yet, and\n`crate-for-node.json` still maps `n5-resilience` to `mzizi-data`. `mzizi-chaos-engine` (N8, a\nseeded deterministic fault schedule for chaos tests that refuses to run in production) is the\nsame. N4 safety and N6 pages have no crate (N6 never will: a page implements nothing). A Workers adapter\n(`mzizi-worker`) is planned for `mzizi-roots-server` and has not landed.\n\nThe registry has 577 items and 55 `.rs` files. Cite `GET https://api.mzizi.dev/v1/ui` or\n`/v1/stats` for a current count rather than repeating these. To find a component's crate, read\n`rust.crate` from `mzizi_get_component`, or take its node directory and read the table above.\n`https://api.mzizi.dev/v1/rs/<name>` also serves a `crate` field, derived from the same map on\nregistry `main`; the API serves the registry at a pinned commit, and until the gateway's pin\nmoves past the map it names `mzizi-ui` for every component. Trust the map, or the MCP.\n\n## The renderer\n\nMzizi components are built in-house in Rust, not borrowed from another framework.\n\n- **Built so far.** Seven components render with Mzizi's own `render(props) -> String`: the\n skeleton and six static primitives, merged in `mzizi-dev/mzizi-registry`\n ([#506](https://github.com/mzizi-dev/mzizi-registry/pull/506) and\n [#510](https://github.com/mzizi-dev/mzizi-registry/pull/510)).\n- **Not yet.** Dioxus is still a dependency of `mzizi-ui`, `mzizi-brand`, `mzizi-shell`,\n `mzizi-docs` and `mzizi-roots` until the migration in the registry's `RFC-roots-renderer`\n finishes. The examples in this skill use Dioxus because the published crates need it today.\n- **Direction.** No Dioxus, Leptos or Yew in new work. Astro renders HTML on the server, static\n components ship zero WASM, there is at most one WASM module per page, and Tokio runs\n server-side only.\n\n## Using a UI component\n\nThis compiles against `mzizi-roots` 0.7.0 from crates.io and renders the component to a static\nHTML fragment with `dioxus-ssr`:\n\n```toml\n[dependencies]\nmzizi-roots = { version = \"0.7\", default-features = false, features = [\"brand\"] }\ndioxus = { version = \"0.7\", default-features = false, features = [\"macro\", \"signals\", \"hooks\", \"html\"] }\ndioxus-ssr = \"0.7\"\n```\n\n```rust\nuse dioxus::prelude::*;\nuse mzizi_roots::brand::{AlertSeverity, MziziAlertBanner};\n\nfn banner() -> Element {\n rsx! {\n MziziAlertBanner {\n r#type: \"Thunderstorm\",\n severity: AlertSeverity::Severe,\n headline: \"Severe storms this afternoon\",\n }\n }\n}\n\nfn main() {\n let mut dom = VirtualDom::new(banner);\n dom.rebuild_in_place();\n let html = dioxus_ssr::render(&dom); // a static HTML fragment for Astro to place\n println!(\"{html}\");\n}\n```\n\nThe output is a `<div data-slot=\"mzizi-alert-banner\" data-portal=\"https://mzizi.dev/components/mzizi-alert-banner\" role=\"alert\" …>`\nwith the same Tailwind classes and CSS custom properties as the React build, so one stylesheet\nstyles both.\n\nWhat a Roots UI component looks like:\n\n- **Same contract as the React build:** the same registry name, `data-slot`, `data-portal`,\n variant names and Tailwind classes over the same `var(--…)` tokens. No colour literals.\n- **Variants are enums**, and a `match` replaces `class-variance-authority`\n (`n2-primitives/badge.rs`, `button.rs`).\n- **Browser facilities are props.** Where the `.tsx` reads `useMziziHarness`, the Rust takes the\n one bit it branches on, usually `prefers_reduced_motion: bool`. Logging is the host's.\n- **Icons are slots:** an `Element` the host renders, where the `.tsx` takes a Lucide component.\n- **Each file is self-contained.** No helpers shared between components.\n\n## The two frontend paths\n\nThe frontend is either **Astro** (pure `.astro` components, or Roots rendered underneath) or\n**pure Rust end to end**.\n\n- **Pure Rust.** A Dioxus app depends on the crates and mounts the components on web (wasm),\n desktop or mobile. Every component supports this today.\n- **Astro, static.** Render with `dioxus-ssr` at build time or in a Worker, and let Astro place\n the fragment, as in the example above. It ships no JavaScript and suits display components.\n The brand crate's contract suite does this for every component on every run.\n- **Astro, interactive islands.** Compile the behaviour to one wasm module per page and wrap it\n as a custom element. **Not packaged yet**: the wrapper crate (`mzizi-islands`) is proposed for\n batch 2 of the RFC.\n\n- **Astro, pure `.astro` components** (mzizi-registry #397, owner decision 2026-10-04). The\n registry is the single source of every component in every format: `.tsx`, `.rs`, `.astro`\n (and `.mz`, the Mzizi language, as it lands). A component's `.astro` sits beside its `.tsx`\n and `.rs` (`components/registry/n<N>-*/<name>.astro`), pure Astro with no React or\n Vue under it and no client JavaScript unless its contract allows one enhancement script.\n Install with `mzizi add <name> --target astro` (inferred in an Astro project, even one with\n React islands): it reads `api.mzizi.dev/v1/astro/<name>` and writes the component,\n its flat-imported siblings (`./button.astro`, `./ui-utils`, `./server-table`) and any brand\n asset into `src/components/mzizi/`. A component with no `.astro` is refused by name, never\n replaced by its `.tsx`. `mzizi_get_component` returns the Astro block too.\n\nThe Mzizi Dashboard Standard's app components (`app-*`, plus the primitives `button`, `badge`,\n`card`, `alert`, `input`, `label`, `skeleton`), the Mzizi Discover Standard's components\n(`discover-*`, including the detail pattern) and the marketing-site components (`site-*`) all\nhave an `.astro` AND a `.tsx` in the registry (the `ui/` family's Astro ports keep the\nregistry's own `.tsx`, which share only what their contract's `identity` says), each held to\nits contract (\"The Dashboard and Discover Standards' contracts\" below). `@bundu/ui`\n(`@bundu/ui/app/*`, `@bundu/ui/discover/*`) is BUILT from these files at a pinned registry\ncommit, and its CI fails on drift; a Next.js app adopts the same design through the registry's\n`.tsx` (`mzizi add app-data-table --target tsx` or\n`npx shadcn add` of `api.mzizi.dev/v1/ui/<name>`). Use them as they are: do not\nfork, restyle or wrap the shell or a component; fix it upstream in the registry (contract,\n`.astro` and `.tsx` together), in the same change.\n\n## Starting an app\n\n**Install the shell first.** Before the first page, mount the app's one shell: `AppShell` for a\nsigned-in app or console, `DiscoverShell` for a public discover site, both from `@bundu/ui`.\nThen check that every page renders inside it. The `mzizi-architecture` skill has the choice, the\ninstall and the check. Apps built page by page without a shell are the failure it exists to\nstop. Neither shell has a Rust build yet (`rs: null`), so a Rust or Astro-with-Roots app still\ntakes the shell from `@bundu/ui`.\n\nFor a new Mzizi-ecosystem app, or any app that consumes the design system. Pick the path\nfirst (above); these steps are the Astro one, with the pure-Rust differences noted.\n\n### 1. Create the project (Astro)\n\n```bash\npnpm create astro@latest my-app -- --template minimal --typescript strict\ncd my-app\npnpm astro add tailwind cloudflare\n```\n\nAstro renders HTML by default, which is what makes a new app indexable by N11 and paintable on\na low-end device without opting in to either. For a pure-Rust app, start from\n`cargo new my-app` and skip to step 3.\n\n### 2. Wire the tokens\n\nCopy the generated stylesheet rather than retyping hexes: `skills/mzizi-design/tokens/palette.css`\nin `@nyuchi/mzizi-skills` (all 21 families, light and dark, generated from\n`palette.canonical.json` at the package root). Import it from your global stylesheet (in Astro,\n`src/styles/global.css`) and register the properties in the Tailwind 4 `@theme` block so\nutilities generate. In Rust the same palette is the `mzizi-tokens` crate, always on in\n`mzizi-roots`. How the 21 families are meant to be used, the radius scale and the Bundu floor (17px body, 14px captions, a 49px hit region on every control, a 14px inset and 7px gaps)\nare in the `mzizi-design` skill.\n\nPick the app's accent through `--brand-accent` rather than hardcoding a mineral hex: nyuchi =\ngold, mukoko = tanzanite, and a Mzizi surface = hematite (a Heritage tone, not a mineral; see the\nconstellation in `mzizi-design`).\n\n### 3. Add the Roots components\n\n```bash\ncargo add mzizi-roots # tokens, primitives, brand components, shell parts (no AppShell yet)\n```\n\nFor Astro, a small Rust crate (a build step, or a `workers-rs` Worker for request-time\nrendering) renders each component to HTML with `dioxus-ssr`, and Astro places the fragment.\nThe fragment carries the same Tailwind classes as the React build, so the stylesheet from step 2\nstyles it, and it ships no JavaScript. Interactive islands (Roots compiled to wasm and wrapped\nas custom elements) are proposed and not packaged yet. \"Using a UI component\" above has a\ncompiling example.\n\nNever copy component source between apps. Depend on the crate, so every app inherits fixes.\n\n### 4. Give the app's AI assistants the doctrine\n\nInstall the published skills bundle so any assistant working in the repo has the design system,\nthe language and the components on hand:\n\n```bash\nnpm install -D @nyuchi/mzizi-skills\nnpx skills experimental_sync # links the bundled skills into .agents/skills/\n```\n\n`npx skills add @nyuchi/mzizi-skills` does **not** work: the `skills` CLI reads that argument\nas a GitHub repository and fails to clone it. `experimental_sync` is the CLI's route for skills\nthat ship in `node_modules`, and it is marked experimental. The same skills are served with no\ninstall at all by the MCP's `mzizi_get_skills`.\n\nThe public Claude Code plugin installs these skills and the MCP in one step:\n\n```\n/plugin marketplace add mzizi-dev/mzizi-registry\n/plugin install mzizi@mzizi\n```\n\nTo wire the MCP directly instead, add it to `.mcp.json` or your client config. It is free with\nno account; only the Fundi tools need sign-in:\n\n```json\n{\n \"mcpServers\": {\n \"mzizi\": { \"type\": \"http\", \"url\": \"https://mcp.mzizi.dev/mcp\" }\n }\n}\n```\n\n### 5. Let fundi read the project\n\n`@nyuchi/mzizi-cli` ships the **fundi** agent, which reads the project off disk and plans the\nwiring against the registry:\n\n```bash\npnpm add -D @nyuchi/mzizi-cli\npnpm exec fundi explore # offline — snapshot the project\npnpm exec fundi plan \"wire up Mzizi tokens\"\n```\n\nRun the bins from the installed package (`pnpm exec`, or `npx` inside the project). A bare\n`npx fundi` outside it fetches an unrelated npm package called `fundi`. `explore` runs without\ncredentials; `plan` and `chat` need `ANTHROPIC_API_KEY`. A plan is\ninert until you apply it, so read it before letting it write.\n\n### 6. Verify\n\n```bash\npnpm dev\n```\n\nConfirm the tokens resolve in light and dark, a rendered Roots fragment picks up the styles,\nand the page is readable with JavaScript off.\n\n### 7. An existing React app\n\nIf the app is already React, the React build installs with the CLI or shadcn, by absolute URL:\n\n```bash\npnpm add -D @nyuchi/mzizi-cli && pnpm exec mzizi add button # infers the tsx target\nnpx shadcn@latest add https://api.mzizi.dev/v1/ui/mzizi-theme-provider\n```\n\nPoint `components.json` `registries.mzizi.url` at `https://api.mzizi.dev/v1/ui`. A bare name\nresolves against shadcn's own registry and installs a different component. Do not start a new\napp this way.\n\n## Server components\n\nA Roots server component is a **sans-IO core**: typed request in, typed response out, every\nexternal facility (a store, a clock, an outbound fetch, a secret) behind a trait. The host owns\nI/O and credentials. `n10-documentation/mzizi-docs-api.rs` is the model: it parses a path into a\ntyped `Route`, asks a `DocsStore` trait for data and returns an `ApiResponse`. `mzizi-assurance`,\n`mzizi-fundi` and `mzizi-discovery` have no dependencies at all. The RFC records a passing\n`wasm32-unknown-unknown` `cargo check` for those three and `mzizi-docs`; CI does not run a\nwasm32 job yet.\n\nA host adapter mounts the core: a `workers-rs` adapter for Cloudflare Workers, an axum adapter\nfor containers. Neither adapter crate exists yet. Until then the host writes the few lines that\nturn its request into the core's request (see the `mzizi-backend` skill). Dioxus\n`#[server]` functions are one more possible adapter, not the building block.\n\n## Contracts\n\nEvery Roots component carries a contract, in two parts:\n\n1. **The registry contract:** one registry name, description, dependencies and variants shared\n by both builds, and the same `data-slot`, `data-portal` and tokens. Each crate's\n `tests/contract.rs` reads the `.tsx` sibling and asserts that the slot, the portal and every\n variant value the Rust emits appear there too. A deliberate divergence, such as a touch\n target raised to the 49px hit region, is asserted from both sides, so the test fails when the\n React build is fixed.\n2. **Checkable clauses** in the Mzizi language's `contract … end` grammar (RFC-0006; the\n `mzizi-language` skill), exported as `pub const CONTRACT: &str`. From\n `n3-brand/mzizi-hero-stat.rs`:\n\n ```rust\n pub const CONTRACT: &str = r#\"contract\n slot is \"mzizi-hero-stat\"\n role is \"region\"\n label not_empty\n button \"Share\" min_height 48\n end\"#;\n ```\n\n `mzizi-brand`'s contract suite renders each component with `dioxus-ssr` in named states and\n evaluates every clause against the markup it emits. **A clause it cannot evaluate fails.**\n `mzizi_roots::brand::CONTRACTS` lists every brand component with its contract.\n\nToday the twelve `mzizi-brand` components export `CONTRACT`. The other crates check the\nregistry contract against the `.tsx`, and their `CONTRACT` clauses arrive batch by batch.\nServer-component clauses (status per route, CORS and cache headers, no credential in the core)\nwait on the language's contracts-everywhere RFC (RFC-0010). Until then their tests assert those\nproperties against an in-memory implementation of the traits. The language's own `service`\ncontracts (`example` and `ensure`, RFC-0011) apply to a Mzizi `service`, not to these Rust\ncrates. No component lowers to Rust yet (only a Mzizi `service` does, to a local axum\npackage), so `mz contract` does not read a Roots `CONTRACT`.\n\n### The Dashboard and Discover Standards' contracts\n\nThe registry's\n[`contracts/`](https://github.com/mzizi-dev/mzizi-registry/tree/main/contracts) holds a\nmachine-readable, versioned contract for every component of `@bundu/ui` (built in\n`mzizi-dev/packages-npm`), in two families of one format:\n\n- **`app/`, the Mzizi Dashboard Standard** (since registry `a298a4e`, 2026-10-04,\n mzizi-registry #404): the one dashboard design for every product in the Bundu ecosystem,\n with each brand's mineral as an overlay (docs.mzizi.dev/patterns/dashboard-standard). Its 35\n contracts are the server-rendered app components (`@bundu/ui/app/*.astro`): the shell, the\n page and data patterns, and seven primitives.\n- **`discover/`, the Mzizi Discover Standard** (since registry `0bea10c`, 2026-10-04,\n mzizi-registry #415, tracking issue #413): one design for every public discover and browse\n page in the Mukoko family (docs.mzizi.dev/patterns/discover-standard). Its 16 contracts are\n the Discover components (`@bundu/ui/discover/*.astro`). \"The Discover Standard\" below says\n what is different.\n\n| File | What it is |\n| ------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| `contracts/schema/component-contract.schema.json` | the format, one for both families |\n| `contracts/app/<name>.contract.json` | one Dashboard Standard component's contract |\n| `contracts/discover/<name>.contract.json` | one Discover Standard component's contract |\n| `contracts/site/<name>.contract.json` | one marketing-site component's contract |\n| `contracts/ui/<name>.contract.json` | one Astro port of a registry primitive (native select, segmented control, …) |\n| `contracts/index.json` | all 77 component contracts with their version and `.astro`, `.tsx`, `.rs` items |\n| `contracts/README.md` | the coverage tables, the recorded gaps, the shared rules, how to change a contract |\n\n**Read the contract before you build or change a `@bundu/ui` component, in any language.** It is\nthe specification: `props` and `slots` (types, required, defaults, closed `values`; the\n`.astro` file's props and slots must be exactly these), `behaviour`, `states` (named fixtures,\n`default` required; every runner renders every state), `accessibility` (roles, keyboard,\nfocus, ARIA, WCAG 2.2 criteria), `density`, `theming`, `noJs`, `responsive`, the `contract`\nblock and `checks` (selector assertions on the rendered markup). `implementations` names the\nAstro export and the `.tsx` and `.rs` siblings, with what they share (`identity`: `slot`,\n`slot+role` or `slot+variants`) and their `divergences`; items under `related` look similar\nand have a different contract, so they are not implementations. `implementations.astro`\ngives the package, the export and `since`: the `@bundu/ui` release that first shipped it, 0.3.0\nfor the original two families (mzizi-registry #419) and 0.6.0 for the components added since\n(money, plan summary, tier status, confirm action, the markdown renderer, and the trust and\naddress primitives).\n`@bundu/ui` is 0.7.0 on npm. `gaps` records where the current build falls short.\n\n**Every new build starts from its contract, library or component** (owner, 2026-10-31). Write or\nchange the contract first, then the builds. A fifth family, `lib/`, holds\ncontracts for non-visual libraries: the N5 resilience primitives (circuit breaker, retry,\nbulkhead, rate limiter, fallback chain, deadline) and the N8 chaos engine, in their own format\n(`schema/lib-contract.schema.json`), each invariant held by a test in the implementing crate\n(mzizi-registry #585, released in v4.18.0).\n\nFour fields are requirements. packages-npm's contract test evaluates the first three on the\nrendered Astro build; the responsive rules are prose. The values below are the Dashboard\nStandard's; the Discover Standard's differ (below):\n\n- **No-JS fallback.** `noJs.script` is `none` (no script) or `enhancement` (one script, and\n everything in `noJs.without` works when it does not run). Only AppShell and CommandPalette\n are `enhancement`; every other app component ships no script.\n- **Density.** Each row gives a part's height in px for a fine pointer and a coarse one\n (touch), read from the rendered classes (`h-N`, `min-h-N`, `size-N` = N×4;\n `pointer-coarse:` for touch). Dashboards are drawn dense on a mouse and at the floor on touch:\n the app Button's `md` row reads 36px fine and 48 coarse with the small radius (a button is a\n `hit-area` element, so its hit region is 49px; its `sm` link row is a pinned gap), and nav rows\n are drawn 32 on a mouse. The registry primitives read the same density from the size roles\n (`h-control`: 49px coarse, 36px drawn at desktop density, hit region 49px either way), which the\n runner and `mzizi-rs/contract-eval` understand (`ROLE_HEIGHTS`). Since the Bundu floor (registry\n 4.23.0) both fail a density row whose interactive control has a hit region under 49px on either\n pointer, or a button-like control drawn under 28px; they count the 49px hit region only where the\n token layer's `hit-area` rule provably applies (`contracts/hit-region-cases.json`). Rows that\n predate the floor are pinned in `HIT_AREA_GAPS` and listed in `gaps`, not hidden.\n- **Theming.** A brand overlay changes colour only: `--primary`, `--ring`, AppShell's\n `--app-accent` and the brand mark. Layout never varies by brand. A mineral may be named only\n as one of the component's declared `statusColours`; any other mineral class, or any colour\n value, fails (the `mzizi-design` skill).\n- **Responsive.** Each contract's rules from 320px up.\n\nIn both families the tests also fail **any inline `style` attribute** on a rendered element, so\na page's Content-Security-Policy can keep `style-src 'self'` with no\n`style-src-attr 'unsafe-inline'`: custom properties go through classes or data attributes\n(AppShell's `data-accent`), sizes through classes or SVG geometry (BarChart). JSON-LD\n(`type=\"application/ld+json\"`) is data and does not count as a script.\n\n**How they relate to `CONTRACT` and the language.** The `contract` field is a\n`contract … end` block in the same clause grammar as a Roots `pub const CONTRACT` (RFC-0006\nand RFC-0010), restricted to what applies to rendered markup: subjects `slot`, `role`,\n`label`, `class`, `portal`, `<element> \"<text>\"`, `uses <data-slot>` and `when <state> …`;\npredicates `is`, `contains`, `not_empty`, `in`, `uses \"--token\"`, `min_height` and `shows`. As\nin the language, **a clause, check or density row a runner cannot evaluate fails** (FM-12).\nThe registry's own suites evaluate them: `__tests__/astro/contracts.test.ts` renders every\n`.astro` in every state (Astro's container, no renderer), `__tests__/contracts/\ntsx-contracts.test.tsx` renders every `contract`-identity `.tsx` with the same runner\n(`contracts/runner.ts`), and `__tests__/contracts/contracts.test.tsx` validates every contract\nagainst the schema and the grammar and checks each identity-only sibling. packages-npm's\n`src/app/contracts.test.ts` runs the same contracts again on the built package. A crate's own `CONTRACT`\nand `tests/contract.rs` still run as before.\n\nEvery contract has an `.astro` (`implementations.astro.registry`). The seven app primitives'\n`.tsx` are the registry's own `button`, `badge`, `card`, `alert`, `input`, `label` and\n`skeleton`, which share only their identity (the contract lists the divergences), as do the\n`ui/` family's; every other `.tsx` (`app-*`, `discover-*`, `site-*`) has\n`identity: \"contract\"`, held to the whole contract exactly as the Astro build is. A `.rs`\nexists in `mzizi-ui` for `button`, `badge`, `card`, `input`, `label` and `safe-area-frame`;\nmzizi-registry #401 tracks Rust ports of the rest, done when each passes the contract.\n\n### The Discover Standard\n\nThe 11 Discover Standard list-page contracts in `discover/` and the node each declares (the detail pattern below adds five more). DiscoverShell's `kind` is `shell`;\nthe other ten are `pattern`s.\n\n| Contract | Component | Node | What it is |\n| --------------------------- | --------------- | ---- | --------------------------------------------------------------------------------------------- |\n| `discover/discover-shell` | DiscoverShell | N7 | the frame: skip link, mineral strip, header (brand, \"Main\" nav, one action), `<main>`, footer |\n| `discover/discover-meta` | DiscoverMeta | N6 | the `<head>` tags: title, description, robots, canonical, Open Graph, the X card, JSON-LD |\n| `discover/discover-hero` | DiscoverHero | N6 | breadcrumbs, eyebrow, the page's one `<h1>`, lead, the search box and actions |\n| `discover/discover-search` | DiscoverSearch | N6 | the one search box: a `role=\"search\"` GET form, so every search is a URL |\n| `discover/category-chips` | CategoryChips | N6 | the category links as a named navigation landmark; chips wrap |\n| `discover/category-chip` | CategoryChip | N6 | one category link, with an optional count; a link, never a button |\n| `discover/discover-section` | DiscoverSection | N6 | one band: eyebrow, heading, description and \"See all\" over its content |\n| `discover/result-grid` | ResultGrid | N6 | a polite status line, the cards, the empty state and \"load more\"; `grid`, `list` or `rail` |\n| `discover/discover-card` | DiscoverCard | N6 | one card for every page, variants `article`, `event`, `circle`, `place` |\n| `discover/load-more` | LoadMore | N6 | cursor pagination: one `rel=\"next\"` link to `?cursor=…` |\n| `discover/open-in-app` | OpenInApp | N6 | \"Open in Mukoko\": an https link into the super-app, with one line saying what happens |\n\nSame format, schema, clause grammar and runners as the Dashboard Standard. What the Discover\ncontracts state differently:\n\n- **What it is for.** Public discover and browse pages in the Mukoko family (circles, news,\n events, weather, and the super-app on the web), not admin or operator dashboards. It reuses\n the Dashboard Standard where they overlap: ResultGrid's `empty` slot takes `app/empty-state`\n or `app/state-message`, and `related` names `app/filter-bar` and `app/pagination` as similar\n components with a different contract.\n- **No script at all.** Every Discover contract's `noJs.script` is `none`; there is no\n `enhancement` like AppShell's or CommandPalette's. Views are URLs, not client state: search\n is a GET form (`?q=`, with no `cursor`, so a new search starts at the first page), a category\n is a link, and paging is LoadMore's `rel=\"next\"` link to `?cursor=`.\n- **Sizes come from the size roles, with gaps still pinned.** Discover's search field is 49px on\n both pointers and its submit button 36px drawn on a mouse and 49px on touch (the size roles,\n mzizi-registry#597). \"See all\", LoadMore, OpenInApp, DiscoverCard's app link, the category\n chips, the navigation links and the home link are pinned in the registry's `HIT_AREA_GAPS`\n (links, or literal `h-11`/`h-12` classes) until they move onto a size role or take `hit-area`;\n do not copy their heights. Until 2026-11-01 Discover was the same size on every pointer; the\n Bundu floor replaced that.\n- **Theming.** The brand-overlay rule is the same (colour only, no colour values, a mineral\n only as a declared status colour), but there is no AppShell `--app-accent`. A brand shows in\n DiscoverShell's `brand` slot (its mark) and in whatever primary action its `action` slot\n holds; the frame, header, navigation and footer are identical in every brand. `--primary` reaches only\n DiscoverHero's wash (`gradient-showcase`), DiscoverCard's monogram and `brand` badge tone,\n and OpenInApp's `primary` variant. Chips, sections and LoadMore are neutral in every brand,\n and DiscoverSearch follows the brand only in its focus ring (`--ring`). Only DiscoverCard\n declares status colours (`cobalt`, `malachite`, `gold`, `terracotta`), for its badge tones.\n- **Server-filled shells.** Every Discover component also works in an Astro page built once\n with `{{placeholders}}` and filled by a server, as circles.mukoko.com's Rust Worker does:\n text props are plain strings, text that fills to `\"\"` hides itself, and state switches are\n attributes styled by classes (ResultGrid, DiscoverSection and LoadMore `state`, DiscoverCard\n `badgeTone`, CategoryChip `current`). CategoryChip, DiscoverCard, DiscoverHero, LoadMore and\n ResultGrid have a `template` state with placeholders, rendered like every other state.\n DiscoverCard and CategoryChip render their own `<li>`, so they repeat as fragments.\n- **One card.** DiscoverCard's `variant` changes emphasis, never structure. Its title is the\n one primary link, stretched over the card; other actions stay separately focusable.\n- **OpenInApp's `href` is https** (a universal link or a server redirect), never a bare\n `mukoko://` link, which does nothing without the app.\n- **No siblings and no gaps yet.** No `.tsx` or `.rs` implements a Discover contract, and none\n records `gaps`. A React or Rust port is done when it passes the same contract.\n\n**Upstream first** (registry `CONTRIBUTING.md`, from #415, for both families): a component an\napp needs that Mzizi does not have, or a change to one it has, goes upstream immediately: its\ncontract in the registry, its build in `@bundu/ui` and its page on docs.mzizi.dev. An app keeps\na local copy only while that upstream pull request is open, marked at the top of the file with\n`TODO(mzizi): <upstream PR URL>`.\n\n### Changing a contract\n\n1. Edit `contracts/<family>/<name>.contract.json` in the registry and bump its `version`:\n major when a clause, prop, slot or state is removed or narrowed, minor when one is added,\n patch for prose. A new family also needs its directory in `FAMILIES` in\n `__tests__/contracts/contracts.test.tsx` and in packages-npm's `scripts/contract-paths.mjs`.\n2. Update `contracts/index.json` and the README's coverage table for that family. The test\n fails if either disagrees.\n3. Run `pnpm test` in the registry, then `pnpm contracts:fetch <branch>` and `pnpm test` in\n packages-npm. The Astro build must pass the new contract before either side merges. Never\n edit the copy in packages-npm.\n\n**The detail pattern** (mzizi-registry #429; docs.mzizi.dev/patterns/discover-detail): the\npage for one item reached from a discover page uses five more `discover/` contracts, each an\n`.astro` and a `.tsx`: `detail-hero` (the page's only `<h1>`, when/where, lead, image or media\nslot), `breadcrumb` (named `<nav>`, `aria-current=\"page\"`, BreadcrumbList JSON-LD), `meta-list`\n(a `<dl>` of facts), `detail-actions` (Open in Mukoko first, then secondary and share links; no\nscript) and `related-rail` (DiscoverCards; a scroll-snap row on phones, a grid from 48rem;\nhidden when empty).\n\n**Open in Mukoko** is one canonical https universal link, `mukoko.com/open/<service>/<id>`\n(`openInMukokoUrl` in `discover-open-link.ts`; `@bundu/ui/discover/open`). `open-in-app` 1.1.0\nand `detail-actions` take `service` (`news`, `events`, `weather`, `circles`) and `id`; `href` is\nonly an override. The apps claim `mukoko.com/open/*` as universal and app links; on the web a\nresolver (mukoko-dev/super-app-web `/open/*`) sends the reader to the service's page. Never a\nbare `mukoko://` link.\n\n## Reuse before build\n\n**The simplest change is the one you don't write.** Reuse before you build, compose before you\nimplement, and let one source own each decision. Use this before adding a component, a style or\na page, and during any refactor.\n\n1. **Reuse before build.** Search the registry first: `mzizi_search` or\n `mzizi_list_components` over the MCP (`https://mcp.mzizi.dev/mcp`, free), or\n `GET https://api.mzizi.dev/v1/ui`. Where a Rust implementation exists, depend on its crate\n (`cargo add mzizi-roots`). For the React build, install with\n `mzizi add <name>` or `npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>`. Never\n reimplement a primitive that already exists.\n2. **Styles are tokens.** Never hardcode a colour, radius, spacing, or motion value —\n consume `var(--…)` (or the `mzizi-tokens` consts in Rust). N1 is the only layer allowed to\n define values, so a rebrand is one edit. A new colour goes in the registry's\n `lib/tokens/palette.source.ts`, and `pnpm tokens:sync` regenerates `styles/globals.css`, the Rust\n tokens and the other emitters from it.\n3. **Compose, don't implement.** A page (N6) is a composition of N2 primitives and N3 brand\n components — never inline a `<button>`, a card, or an SVG. Pull spinners, skeletons, and\n badges from N2.\n4. **Variants over copies.** Collapse near-duplicate components into one variant table: an\n enum with a `match` in Rust, an `enum` table in Mzizi source, one `cva()` in the React\n build. Express differences as `variant` / `size` props, not new files.\n5. **One composition path.** In the React build, compose class names through `cn()` and\n extract shared logic into `lib/` or a hook. In Rust, a pure function beside the component.\n No logic duplicated across files.\n6. **Right altitude.** Keep each component self-contained, importing only from the layer\n below on its strand. A primitive that reaches up into page code can't be installed on its\n own.\n7. **Delete first.** Prefer removing over adding — dead code, unused props, redundant\n wrappers, commented-out blocks. Less code is less to break, read, and keep true.\n8. **Live data, not copies.** Read counts, tokens and doctrine from their source\n (`https://api.mzizi.dev/v1/stats`, `palette.canonical.json`, `mzizi_get_tokens`,\n `mzizi_get_doctrine`). A baked-in number or duplicated token guarantees drift.\n\n### Checklist\n\n- [ ] Searched the registry before building, and used the Rust build where one exists?\n- [ ] All styles are tokens (`var(--…)`), zero hardcoded values outside N1?\n- [ ] Page is pure composition of N2/N3, no inlined primitives?\n- [ ] Variation expressed as one variant table, not near-duplicate files?\n- [ ] One composition path, shared logic extracted once?\n- [ ] Each file self-contained and installable, importing only downward?\n- [ ] Deleted more than you added where you could?\n- [ ] Counts, tokens, and doctrine read live, not hardcoded?\n\n## Contributing a component\n\n**A component is a real file.** `components/registry/n<N>-<label>/<name>.rs` (Rust, compiled by\nthe node's crate in `mzizi-rs/`) and, for the React build, `<name>.tsx` beside it, which\n`pnpm typecheck` (`tsc --noEmit`) and `pnpm lint` check. An error in either fails CI. The\nregistry has no app to build: CI's `Rust` job runs `cargo fmt`, `check`, `clippy` and the\ncontract tests, and its `Build` job runs `pnpm build`, which runs every generator in write mode\nand fails if that changes a committed file.\n\n| File | What it is |\n| --------------------------------------------- | ----------------------------------------------------------------- |\n| `components/registry/n<N>-<label>/<name>.rs` | the Roots component (Dioxus UI, or a sans-IO server core) |\n| `components/registry/n<N>-<label>/<name>.tsx` | the React build of the same contract |\n| `registry.json` | the manifest `/v1/ui/<name>` and `npx shadcn add` resolve against |\n\n`registry.json` carries the install contract (description, dependencies,\n`registryDependencies`, target paths) and **never the source**. The node is derived from the\ndirectory the component lives in, so it cannot disagree with where the file is.\n\n**There is no database.** The registry's files are its data layer (mzizi-registry PR #368), and\na test fails the build if a Supabase import or a `SUPABASE_*` read comes back. Nothing about a\ncomponent, including its source, status or history, is written to a table. If you find\ninstructions anywhere telling you to `INSERT` a component or read a `component_documents` row,\nthey are stale.\n\n### Step 1 — Decide the node\n\nThe architecture is the **Mzizi DNA double helix**: eight nodes (N1–N8) on the engineering\nbackbone, four rungs (N9–N12) that bridge both backbones, and six strands. The N-numbers are\n**labels, not a sequence**.\n\nThe node decides the **directory**, the directory decides the **crate** that compiles the Rust\n(`mzizi-rs/crate-for-node.json`), and the directory is what the build reads. Choosing the node\nand choosing where the file goes are the same decision.\n\n| Node | Directory | Crate | What belongs there |\n| ----------------- | -------------------- | ----------------- | --------------------------------------------------------------------------- |\n| N1 tokens | `n1-tokens/` | `mzizi-tokens` | A token value or emitter (colour, spacing, motion, radius), not a component |\n| N2 primitive | `n2-primitives/` | `mzizi-ui` | Generic UI: button, card, input, dialog, toolbar, bento-grid |\n| N3 brand | `n3-brand/` | `mzizi-brand` | A branded composition with the mineral palette |\n| N4 safety | `n4-safety/` | none yet | A conditional-rendering gate: permission, geo, rate limit |\n| N5 resilience | `n5-resilience/` | none yet | Error boundary, skeleton, offline banner, fallback chain |\n| N6 pages | `n6-pages/` | none yet | Full-screen layout composition |\n| N7 shell | `n7-shell/` | `mzizi-shell` | App container: navigation, routing, lifecycle |\n| N8 assurance | `n8-assurance/` | `mzizi-assurance` | Instrumentation, a11y audit, RTL conformity, probes |\n| N9 fundi | `n9-fundi/` | `mzizi-fundi` | Automated healing (a rung) |\n| N10 documentation | `n10-documentation/` | `mzizi-docs` | Docs, AI instructions, docs infrastructure (a rung) |\n| N11 discovery | `n11-discovery/` | `mzizi-discovery` | SEO / AIO surfaces: if the machine can't see it, it doesn't exist (a rung) |\n| N12 skills | none in the registry | none | Agent skills (a rung), authored in `mzizi-dev/agent-tools/mzizi-skills/` |\n\nA node with no crate yet (N4, N5, N6) takes a `.tsx` today. Adding its first `.rs` means adding\nits crate and its `crate-for-node.json` entry in the same pull request; `pnpm rust:generate`\nrefuses Rust that no crate claims.\n\nIf you are unsure where something belongs, ask the MCP (`https://mcp.mzizi.dev/mcp`, free, no\naccount): `mzizi_list_components` with `node: N` shows what already lives in a node, and\n`mzizi_get_architecture` returns the node, rung and strand model with live counts. Never\nhardcode counts.\n\n### Step 2 — Write the file\n\nCreate it in the directory the node names:\n\n```\ncomponents/registry/n3-brand/mzizi-your-component.rs # the Roots component\ncomponents/registry/n3-brand/mzizi-your-component.tsx # the React build, same contract\n```\n\nThe Rust file carries `pub const CONTRACT: &str` and follows the rules in \"Using a UI component\" above: variants as enums, tokens by reference, browser facilities as props, icons as slots,\nno helpers shared between components. `.ts` is for a non-rendering module.\n\nThat is the authoring step. There is no row to insert and no registration to perform before\nwriting code: the file _is_ the component, and `cargo test` (for the `.rs`), `pnpm typecheck`\nand `pnpm lint` (for the `.tsx`) and `pnpm build` (every derived artifact regenerated, with no\ndiff) are what tell you it is real.\n\nNaming: N3 brand components take a `mzizi-` prefix. (Components formerly named `nyuchi-*` are\n`mzizi-*`, and the old names redirect.) Everything else is the bare name, matching what a\nconsumer will type in `mzizi add` or `npx shadcn add`.\n\n### Wiring the Rust into its crate\n\n`pnpm rust:generate` writes the committed copy to `mzizi-rs/crates/<crate>/src/generated/`. Add\nthe `#[path = \"generated/<name>.rs\"] pub mod <name_snake>;` line and the `pub use` to the\ncrate's `src/lib.rs` by hand, and add the component to the crate's `tests/contract.rs`. Edit\nthe registry file, never the copy. The workspace lints forbid `unsafe_code` and warn on\n`missing_docs`; clippy turns the warning into a failure. Cap parallel builds on a small machine\n(`CARGO_BUILD_JOBS=2`).\n\nA release is a version bump in `mzizi-rs/Cargo.toml`. On merge to `main`,\n`.github/workflows/publish-crates.yml` re-runs the Rust gate and publishes each crate in\ndependency order, skipping any version already on crates.io. A published crate name is\npermanent. Say in the PR body that components changed, so the site, docs and skills are\nupdated to match.\n\n### Step 3 — Declare it in `registry.json`\n\nOne item, carrying the install contract. `meta` is the part you author; four other fields are\n**derived from it** and must not be hand-written.\n\n```jsonc\n{\n \"name\": \"your-component-name\",\n \"type\": \"registry:ui\",\n \"description\": \"Short description of what it does.\",\n \"dependencies\": [],\n \"registryDependencies\": [],\n \"files\": [\n {\n \"path\": \"components/registry/n2-primitives/your-component-name.tsx\",\n \"target\": \"components/ui/your-component-name.tsx\",\n \"type\": \"registry:ui\"\n }\n ],\n \"meta\": {\n \"collection\": \"primitives\",\n \"owner\": \"mzizi\",\n \"useCases\": [\"Use case 1\", \"Use case 2\"],\n \"variants\": [\"default\", \"sm\", \"lg\"],\n \"sizes\": [\"default\", \"sm\", \"lg\"],\n \"features\": [\"cn() for className composition\", \"data-slot attribute\"],\n \"a11y\": [\"ARIA attributes\", \"Keyboard accessible\"],\n \"hasDemo\": true\n }\n}\n```\n\n`files[]` lists the `.tsx`: it is the shadcn install contract for the React build. The `.rs`\nbeside it is not listed; the node's crate compiles it, and `/v1/rs/<name>` serves it with the\ncrate's name.\n\n**Do not write `title`, `categories`, `docs` or `author`.** `pnpm registry:metadata` derives all\nfour from `meta` and the repo. `docs` in particular is what the shadcn CLI prints _after_\ninstalling, so it is generated from your `useCases` / `variants` / `sizes` / `features` / `a11y`\nrather than duplicated by hand — and an item with no `meta` deliberately gets no `docs`, because\na stub reads as documentation and says nothing.\n\nThen run the generators and let the gates check you:\n\n```bash\npnpm registry:paths # fill file paths from what is on disk\npnpm registry:metadata # derive title, categories, docs, author\npnpm registry:normalize # canonical ordering\npnpm registry:validate # every item resolves on disk; every dependency addressable\npnpm registry:verify # the CI drift gate\npnpm typecheck && pnpm lint && pnpm build # build: every generator; commit what it writes\n```\n\nFollow the criteria for the chosen node.\n\n#### Every node: the Rust file\n\n- `pub const CONTRACT: &str` with the clauses the rendered markup must meet (slot, role, touch\n floors, variant tables), evaluated by the crate's `tests/contract.rs` on `dioxus-ssr` output\n- The same `data-slot`, `data-portal` and variant values as the `.tsx`, asserted by the same\n suite; a deliberate divergence is asserted from both sides\n- `prefers_reduced_motion: bool` as a prop where the `.tsx` reads the harness's motion\n- Icons as `Element` slots, not an icon crate\n- Doc comments on every public item (`missing_docs` is a warning, and clippy runs with\n `-D warnings`); no `unsafe`\n\n#### N2 primitive checklist\n\n- No `useMziziHarness` import in the `.tsx`\n- `data-slot` attribute on the root element\n- `data-portal` attribute pointing at `https://mzizi.dev/components/<name>`\n- `cn()` for className composition in the `.tsx`\n- No raw Tailwind colours: use semantic tokens (`bg-primary`, `text-foreground`, …)\n- Icons from `@/lib/icons` in the `.tsx`, never `lucide-react` directly\n- **The Bundu floor: every interactive control has a 49px hit region on every device** (owner,\n 2026-11-02: 7 x 7, above Apple's 44 pt; desktop included). Body text is 17px and captions 14px\n everywhere; nothing readable is under 14px, so use `text-sm` (14px), never `text-[10px]`.\n Buttons and inputs take their height from the size roles, never `h-12`, `h-14` or\n `min-h-[44px]`: `h-control` (49px touch, drawn 36px at desktop density), `h-control-sm`,\n `h-control-lg` (56px touch), `size-control-icon`, `h-field` (49px drawn everywhere, because a\n native field cannot carry a pseudo-element), as `button.tsx`, `button.astro` and `button.rs`\n state, and `/v1/brand` `.componentSpecs` serves (`minTouchTarget: 49`). At desktop density a\n button-like control may be drawn 28 to 40px: the token layer's `hit-area` rule gives it a\n transparent `::after` of `max(100%, var(--target-floor))`. The rule steps aside for an element\n with its own `after:` utility, for menu and listbox rows (they take `min-height: var(--target-floor)`),\n for a calendar day and for an element with `overflow: hidden`; a standalone link that is a target\n is drawn at 49px or takes `hit-area`. `primitives/button.mz` still says 56 until it is updated in\n its own repository. A smaller visual box, such as a badge, is not a touch target; if it becomes\n one, give it `min-h-touch-min`. `data-mzizi-sizes=\"legacy\"` on the root is the compatibility\n alias that restores the pre-2026-11-01 sizes (and steps out of the floor); it is planned for removal\n in the next major, so set it only to buy time. Contrast floors beside it: body 7:1, non-body\n 4.5:1, UI 3:1, with five button-label roles held at 4.5:1 (`primary-foreground`,\n `brand-accent-foreground`, `app-accent-foreground`, `sidebar-primary-foreground`,\n `destructive-foreground`). `text-base` can collide with `--color-base`: write `text-body`\n (details in `mzizi-design`).\n- **The spacing floor: padding, gaps and leading have minimums too** (owner, 2026-11-04, mzizi-registry#617; `BUNDU_FLOOR.spacing`). **Inset 14px** between text and its container edge (`--inset-floor`), **7px** between adjacent controls, between a label and its field and between sibling items (`--gap-floor`; visible edge to visible edge, hit regions may touch but never overlap), and **1.5** body line height (`--leading-floor`). The base layer gives every element the floor even with no padding or gap set; the token test fails a component spec below it; every component contract asserts its spacing minimums, and the TypeScript runner and `mzizi-rs/contract-eval` must agree; a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress. A component you add or change keeps at or above these numbers in its spec and its contract, in the `.tsx`, the `.astro` and the `.rs` alike, and nothing may lower them (no brand pack, no density layer); `data-mzizi-sizes=\"legacy\"` is the only opt-out. Use the components and tokens, and never `p-0`, `px-0`, `gap-0`, a negative margin, `leading-none`, `leading-tight` or an arbitrary `p-[4px]` on a text container or a row of controls without an exception comment (`mzizi-spacing-exempt: <reason>`); `mzizi check` (`@nyuchi/mzizi-cli` 0.8.0 or later) fails the project otherwise. A registry or `@bundu/ui` older than the spacing floor has none of the custom properties. Details in `mzizi-design`.\n- Pill-shaped categories (button, input, avatar, badge, toggle) use `rounded-full`\n\n#### N3 brand checklist\n\n- `mzizi-` prefix on the name\n- `useMziziHarness(\"<name>\")` in the `.tsx` for the motion preference (and `log` or\n `LiveRegion` where the component uses them); `prefers_reduced_motion` as a prop in the `.rs`\n- The entrance animation off when motion is reduced\n- An ARIA role or `aria-label`, and a region that is named in every state, the loading\n skeleton included\n- `data-slot` and `data-portal` attributes, in every state\n- `focus-visible` ring on interactive elements, and interactive elements with a 49px hit region\n- I18N via `Intl` formatters for dates, numbers and currency\n- Semantic tokens for status-bearing colours\n\n#### N6 page checklist\n\n- Pure composition: no inline buttons, cards or SVGs\n- Accept children/slots for content\n- Semantic CSS vars only (`bg-card`, `text-foreground`, `bg-primary`)\n- A loading-state prop\n- `role=\"main\"` and an `aria-label`\n\n#### N8 assurance checklist\n\n- A sans-IO Rust core (typed input, typed result, every facility behind a trait) in\n `mzizi-assurance`, with the `.ts` module as the React build\n- A banner comment identifying the node and purpose\n- A configurable rules array\n- Optional `onViolation` / `onComplete` callbacks\n\n### Step 4 — Docs and demos\n\nThere is nothing extra to write. Docs come from the `meta` you already authored in Step 3\n(`useCases`, `variants`, `sizes`, `features`, `a11y`), and `pnpm registry:metadata` turns them\ninto the `docs` string the CLI prints after install. Set `meta.hasDemo` when a demo exists.\n\nIf a prop table is expected, `pnpm props:extract` derives it from the component's own types and\n`pnpm props:verify` is the gate. Props are read from the code, never restated beside it.\n\n### Step 5 — Versions and history\n\nThere is no status row and no database. A component's version history is the registry's\nchangelog (`content/changelog/`, generated, checked by `pnpm changelog:generate:check`) and the\nrename map. `mzizi_get_component` with `include: [\"versions\"]` returns it. Read what a component\nis through the MCP or `https://api.mzizi.dev/v1/ui/<name>` rather than assuming.\n\n### Step 6 — Validate accessibility\n\nIf your component introduces a new colour pair, validate it before shipping. The MCP tool is\ncomputed locally and needs no account:\n\n```\nmzizi_check_accessibility(foreground: \"#FFFFFF\", background: \"#0047AB\")\n```\n\nIt returns the WCAG contrast ratio with AA/AAA verdicts and whether the pair clears 7:1, the\nbody-text floor. Non-body text needs 4.5:1 and UI parts 3:1 (five button-label roles hold 4.5:1);\nthe tool does not compute APCA, so read the role values in `mzizi_get_tokens` for those.\n\n### Step 7 — Before you open the PR\n\n`pnpm check` runs every registry gate CI runs, in order, including `registry:validate`,\n`registry:verify`, `tokens:verify`, `changelog:generate:check`, `rust:generate:check`, the\ntests and the build. Then the Rust workspace gate:\n\n```bash\npnpm check\ncargo fmt --manifest-path mzizi-rs/Cargo.toml --all -- --check\ncargo clippy --manifest-path mzizi-rs/Cargo.toml --workspace --all-targets -- -D warnings\ncargo test --manifest-path mzizi-rs/Cargo.toml --workspace\ncargo package --manifest-path mzizi-rs/Cargo.toml --workspace\n```\n\nIf you touched `files[]` or `meta`, run `pnpm registry:paths`, `pnpm registry:metadata` and\n`pnpm registry:normalize` first; their `--check` twins fail otherwise.\n\nA green run means the item resolves to real files, every dependency is addressable, the derived\nfields match `meta`, the Rust compiles, and its contract holds on rendered markup. Say in the PR\nbody that a component changed, so the site, the docs and the skills are updated to match.\n\n### What to avoid\n\n- **Never put component source in a database row, a JSON document, or any second place.** The\n file under `components/registry/` is the component; everything else is a representation of\n it, and a representation can drift from the code while still looking correct. That is not\n hypothetical here: a `source_code` column once served stale copies for 571 components.\n- Never edit `mzizi-rs/crates/*/src/generated/`. It is overwritten by `pnpm rust:generate`.\n- Never hand-write `title`, `categories`, `docs` or `author` in `registry.json`. They are\n derived, and a hand-written value is a second source that the next generator run silently\n overwrites.\n- Never hardcode hex colour values (except documented third-party brands like Ethereum,\n Google, EcoCash).\n- Never use `lucide-react` directly in a `.tsx`; go through `@/lib/icons`.\n- Never use `margin-left` / `padding-right` / `left`; use logical properties\n (`margin-inline-start`, …) for RTL support.\n- Never start new work in the React build alone. If a component has a Rust implementation,\n change the Rust and keep the `.tsx` in step.\n- Never bump a major version on your own initiative. That is a maintainers' call.\n\n## The React build\n\n`npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>` installs the `.tsx` for an existing\nReact app, for all 577 items. Always pass the absolute URL: a bare name resolves against\nshadcn's own registry and installs a different component. `mzizi add <name>` installs the React\nbuild too. Neither is where new work goes.\n\n## See also\n\n- `docs/roots/RFC-roots.md` in the registry: the inventory, the conversion batches and what\n the API and site still need for Rust-first serving (`/v1/roots` is proposed, not live).\n- The `mzizi-language` skill for the clause grammar and `mz contract`.\n- The `mzizi-design` skill for the palette, radius, type and the Ubuntu constraints.\n- The `mzizi-backend` skill for mounting a server core on Workers.",
23
+ "description": "Use this skill for anything involving Mzizi's components: using them, starting an app on them, or contributing one. Mzizi Roots is Mzizi's own components in Rust (UI and server components) from mzizi-dev/mzizi-registry. Covers `cargo add mzizi-roots` / `mzizi-roots-server`, what mzizi_get_component returns (Rust first), the node-to-crate map, `pub const CONTRACT`, the contracts in the registry's contracts/ for the Dashboard Standard (app/, 35 components) and the Discover Standard (discover/, 16 components; no script, server-filled shells): read one before building or changing a @bundu/ui component. Also no-JS fallback, density and floors (49px hit region, 17px body, 14px inset, 7px gaps) and brand-overlay rules, the frontend paths (pure .astro via `mzizi add --type astro`, Astro with Roots, pure Rust; Next.js adopts the .tsx), reuse before build, and adding a component (node, .rs, .tsx, registry.json, the gates). Files are the data layer: no database.",
24
+ "body_mdx": "# Mzizi Roots — the Rust build of Mzizi's components\n\n**Mzizi Roots** is Mzizi's own components in Rust: UI components, and server components as\nhost-agnostic Rust. Write no new code against a third-party UI framework (owner, 2026-11-04,\nmzizi-registry#635). What renders a Roots UI component is the\nregistry's decision, so this skill names no rendering target. They are built to support the Mzizi language (the\n`mzizi-language` skill) as its component model, the way React is JavaScript's; they are not\nthe language. They live in\n[`mzizi-dev/mzizi-registry`](https://github.com/mzizi-dev/mzizi-registry) and are compiled by\nthe crates in its `mzizi-rs/` workspace. The plan is `docs/roots/RFC-roots.md` there.\n\n**Rust first.** Where a Rust implementation exists, present it and use it first. The React/TSX\ncomponents are **the React build**: they keep working for existing React apps, and they are\ndeprioritised, so no new work goes into them.\n\nThis skill describes registry `main` at v4.18.0 (`0d17818`, 2026-11-01) and its `contracts/`\n(77 component contracts in four families, and the `lib/` family), the crates at **0.7.0**\n(registry v4.18.0, on crates.io since 2026-11-01; 0.7.0 adds two crates, `mzizi-resilience`,\nthe N5 core, and `mzizi-chaos-engine`, the N8 seeded chaos engine, from registry PRs #584\nand #586; 0.6.0 before it carried the `--assurance-*` and `--attestation-*` roles in the generated\n`mzizi-tokens` pack, from mzizi-registry #578, and 0.5.0 the `mzizi-mongodb` security fixes from mzizi-registry #567; the UI crates\notherwise match 0.4.0, on crates.io since 2026-10-20 with `markdown-renderer` 1.2.0 and 1.2.1\nfrom mzizi-registry #483 and #517), and `mzizi-mcp` 0.13.0. It replaces three 0.7.0 skills:\n`scaffold-component` (now \"Contributing a component\"), `simplify` (now \"Reuse before build\")\nand the frontend half of `ecosystem-app-setup` (now \"Starting an app\"). Tokens, brand and the\nUbuntu constraints are in the `mzizi-design` skill.\n\n## Install\n\nTwo umbrella crates with no code of their own re-export the node crates behind features:\n\n```sh\ncargo add mzizi-roots # UI: tokens + ui + brand + shell (all default features)\ncargo add mzizi-roots-server # server: assurance + fundi + docs + discovery\n```\n\nTake only what you use:\n\n```toml\n[dependencies]\nmzizi-roots = { version = \"0.7\", default-features = false, features = [\"brand\"] }\n```\n\nTo follow `main`, or pin a commit, install from git. Cargo finds a crate by name anywhere in the\nrepository's workspace:\n\n```sh\ncargo add mzizi-roots --git https://github.com/mzizi-dev/mzizi-registry\n```\n\n```toml\nmzizi-brand = { git = \"https://github.com/mzizi-dev/mzizi-registry\", rev = \"<commit>\" }\n```\n\nA single node crate works too (`cargo add mzizi-ui`). Read a crate's own `Cargo.toml` at the\ncommit you pin for what it depends on; this skill does not restate it.\n\n`mzizi add <name> --type mzizi` (`@nyuchi/mzizi-cli` 0.9.0 or later; `--target rust` on 0.8.0) records the crate and prints the\n`cargo add` rather than copying a `.rs` file into your project: a copied component is a fork\nof a crate module.\n\n## What the MCP returns\n\n`mzizi_get_component` on `https://mcp.mzizi.dev/mcp` (free, no account; `mzizi-mcp` 0.11.0) is\nRust first. It returns `{name, renamedFrom?, lead, note, rust, react, docs?, versions?}`:\n\n- `lead` is `\"rust\"` when the component has a Roots implementation, else `\"react\"`; `note`\n says which build to use.\n- `rust` is the registry's `/v1/rs/<name>` answer with the install first: `crate`\n (`{name, registry, git}`), `install.cargo` (`cargo add <crate>`), `install.pinned`\n (`cargo add <crate> --git https://github.com/mzizi-dev/mzizi-registry --rev <pin>`, which\n builds exactly the source shown), `module` (e.g. `mzizi_brand`), `contract` (the\n `contract … end` text, or `null`), and `files[]` with the `.rs` source inline. With no Rust\n it is `null`.\n- `react` is the React build: `build: \"react\"`, `status: \"deprioritised\"`, `install` (the\n `npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>` line), then the `/v1/ui/<name>`\n body. It is no longer under a `component` key (that was 0.10.x).\n\n`mzizi_list_components` rows carry `rustCrate` when a component has Rust, the response carries\n`withRust`, and `rust: true` lists only those. `mzizi_search` hits carry `rustCrate` too. For\n`mzizi-hero-stat` the answer is `lead: \"rust\"`, crate `mzizi-brand`, and the four-clause\ncontract shown under \"Contracts\" below.\n\n## The crates\n\nOne crate per registry node that has Rust. The map is `mzizi-rs/crate-for-node.json`, and\n`pnpm rust:generate` refuses a node with Rust in it that no crate claims.\n\n| Node directory | Crate | Umbrella | What it holds |\n| ------------------- | ----------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `n1-tokens` | `mzizi-tokens` | `mzizi-roots` (always on) | the 21-family palette and scales as Rust consts, generated from `lib/tokens/palette.source.ts` |\n| `n2-primitives` | `mzizi-ui` | `mzizi-roots` (`ui`) | alert, avatar, badge, button, card, chart, input, label, markdown renderer, progress, safe-area frame, separator, skeleton, status badge |\n| `n3-brand` | `mzizi-brand` | `mzizi-roots` (`brand`) | alert banner, avatar stack, cover header, empty state, escalation card, gauge card, hero stat, meta tile, stats row, success screen, suitability card, user card (all `mzizi-*`) |\n| `n7-shell` | `mzizi-shell` | `mzizi-roots` (`shell`) | bottom nav, command palette, connectivity bar, deep-link handler, footer, mini-app runtime, notification center, persistent player, route guard, theme provider, toast provider, update prompt |\n| `n8-assurance` | `mzizi-assurance` | `mzizi-roots-server` (`assurance`) | probes, telemetry, the OTLP exporter, a11y and RTL conformity checks, alert and incident engines |\n| `n9-fundi` | `mzizi-fundi` | `mzizi-roots-server` (`fundi`) | the Fundi reporter and learning loop (client side) |\n| `n10-documentation` | `mzizi-docs` | `mzizi-roots-server` (`docs`) | the docs API and routing, AI context, the changelog and docs renderers |\n| `n11-discovery` | `mzizi-discovery` | `mzizi-roots-server` (`discovery`) | page metadata and Schema.org JSON-LD |\n\nN5 resilience has a crate since 0.7.0: `mzizi-resilience` (circuit breaker, retry backoff,\nbulkhead, rate limiter, fallback chain and deadline, each a sans-IO state machine with a\n`contracts/lib/` contract). Depend on it directly; no umbrella re-exports it yet, and\n`crate-for-node.json` still maps `n5-resilience` to `mzizi-data`. `mzizi-chaos-engine` (N8, a\nseeded deterministic fault schedule for chaos tests that refuses to run in production) is the\nsame. N4 safety and N6 pages have no crate (N6 never will: a page implements nothing). A Workers adapter\n(`mzizi-worker`) is planned for `mzizi-roots-server` and has not landed.\n\nThe registry has 577 items and 55 `.rs` files. Cite `GET https://api.mzizi.dev/v1/ui` or\n`/v1/stats` for a current count rather than repeating these. To find a component's crate, read\n`rust.crate` from `mzizi_get_component`, or take its node directory and read the table above.\n`https://api.mzizi.dev/v1/rs/<name>` also serves a `crate` field, derived from the same map on\nregistry `main`; the API serves the registry at a pinned commit, and until the gateway's pin\nmoves past the map it names `mzizi-ui` for every component. Trust the map, or the MCP.\n\n## The renderer\n\nMzizi components are built in-house, not borrowed from another framework. The owner removed the\nframework the Roots UI crates were first built on (2026-11-04, across every Mzizi repository,\n[mzizi-registry#635](https://github.com/mzizi-dev/mzizi-registry/issues/635)).\n\n- **Built so far.** Seven components render with Mzizi's own `render(props) -> String`: the\n skeleton and six static primitives, merged in `mzizi-dev/mzizi-registry`\n ([#506](https://github.com/mzizi-dev/mzizi-registry/pull/506) and\n [#510](https://github.com/mzizi-dev/mzizi-registry/pull/510)).\n- **Not yet.** Some published crate versions still depend on a framework that is being\n removed; the registry tracks that migration in #635. The rendering target that replaces it is\n not decided, and this skill does not name one.\n- **Direction.** No third-party UI framework in new work. Astro renders HTML on the server, static\n components ship zero WASM, there is at most one WASM module per page, and Tokio runs\n server-side only.\n- **The order components are served in** (owner, 2026-11-04): Mzizi first, TypeScript second,\n native (Swift, Kotlin) where the registry has them. `mzizi add` serves every type.\n\n## Using a UI component\n\nAsk the registry for the component and read what it returns: `mzizi_get_component` gives the\ncrate, the `cargo add` line, the contract and the `.rs` source, and `mzizi add <name> --type mzizi`\nrecords the crate. How a host mounts or renders the component depends on the rendering target\nthe registry has not yet decided, so there is no host example here: do not copy one from an\nolder version of this skill.\n\nThe same component also exists as pure `.astro` (`mzizi add <name> --type astro`) and as the\n`.tsx`. Its Rust build and those carry the same `data-slot`, `data-portal` and classes, so one\nstylesheet styles all of them.\n\nWhat a Roots UI component looks like:\n\n- **Same contract as the React build:** the same registry name, `data-slot`, `data-portal`,\n variant names and Tailwind classes over the same `var(--…)` tokens. No colour literals.\n- **Variants are enums**, and a `match` replaces `class-variance-authority`\n (`n2-primitives/badge.rs`, `button.rs`).\n- **Browser facilities are props.** Where the `.tsx` reads `useMziziHarness`, the Rust takes the\n one bit it branches on, usually `prefers_reduced_motion: bool`. Logging is the host's.\n- **Icons are slots:** an `Element` the host renders, where the `.tsx` takes a Lucide component.\n- **Each file is self-contained.** No helpers shared between components.\n\n## The two frontend paths\n\nThe frontend is either **Astro** (pure `.astro` components, or Roots rendered underneath) or\n**pure Rust end to end**.\n\n- **Pure Rust.** An app depends on the crates and mounts the components itself. The rendering\n target is not decided (mzizi-registry#635), so this skill gives no mounting example.\n- **Astro, static.** Render the Roots component to HTML at build time or in a Worker, and let\n Astro place the fragment. It ships no JavaScript and suits display components. The renderer\n that produces the fragment is the registry's decision; use the pure `.astro` component\n where you need a working path today.\n- **Astro, interactive islands.** Compile the behaviour to one wasm module per page and wrap it\n as a custom element. **Not packaged yet**: the wrapper crate (`mzizi-islands`) is proposed for\n batch 2 of the RFC.\n\n- **Astro, pure `.astro` components** (mzizi-registry #397, owner decision 2026-10-04). The\n registry is the single source of every component in every format: `.tsx`, `.rs`, `.astro`\n (and `.mz`, the Mzizi language, as it lands). A component's `.astro` sits beside its `.tsx`\n and `.rs` (`components/registry/n<N>-*/<name>.astro`), pure Astro with no React or\n Vue under it and no client JavaScript unless its contract allows one enhancement script.\n Install with `mzizi add <name> --type astro` (inferred in an Astro project, even one with\n React islands): it reads `api.mzizi.dev/v1/astro/<name>` and writes the component,\n its flat-imported siblings (`./button.astro`, `./ui-utils`, `./server-table`) and any brand\n asset into `src/components/mzizi/`. A component with no `.astro` is refused by name, never\n replaced by its `.tsx`. `mzizi_get_component` returns the Astro block too.\n\nThe Mzizi Dashboard Standard's app components (`app-*`, plus the primitives `button`, `badge`,\n`card`, `alert`, `input`, `label`, `skeleton`), the Mzizi Discover Standard's components\n(`discover-*`, including the detail pattern) and the marketing-site components (`site-*`) all\nhave an `.astro` AND a `.tsx` in the registry (the `ui/` family's Astro ports keep the\nregistry's own `.tsx`, which share only what their contract's `identity` says), each held to\nits contract (\"The Dashboard and Discover Standards' contracts\" below). `@bundu/ui`\n(`@bundu/ui/app/*`, `@bundu/ui/discover/*`) is BUILT from these files at a pinned registry\ncommit, and its CI fails on drift; a Next.js app adopts the same design through the registry's\n`.tsx` (`mzizi add app-data-table --type tsx` or\n`npx shadcn add` of `api.mzizi.dev/v1/ui/<name>`). Use them as they are: do not\nfork, restyle or wrap the shell or a component; fix it upstream in the registry (contract,\n`.astro` and `.tsx` together), in the same change.\n\n## Starting an app\n\n**Install the shell first.** Before the first page, mount the app's one shell: `AppShell` for a\nsigned-in app or console, `DiscoverShell` for a public discover site, both from `@bundu/ui`.\nThen check that every page renders inside it. The `mzizi-architecture` skill has the choice, the\ninstall and the check. Apps built page by page without a shell are the failure it exists to\nstop. Neither shell has a Rust build yet (`rs: null`), so a Rust or Astro-with-Roots app still\ntakes the shell from `@bundu/ui`.\n\nFor a new Mzizi-ecosystem app, or any app that consumes the design system. Pick the path\nfirst (above); these steps are the Astro one, with the pure-Rust differences noted.\n\n### 1. Create the project (Astro)\n\n```bash\npnpm create astro@latest my-app -- --template minimal --typescript strict\ncd my-app\npnpm astro add tailwind cloudflare\n```\n\nAstro renders HTML by default, which is what makes a new app indexable by N11 and paintable on\na low-end device without opting in to either. For a pure-Rust app, start from\n`cargo new my-app` and skip to step 3.\n\n### 2. Wire the tokens\n\nCopy the generated stylesheet rather than retyping hexes: `skills/mzizi-design/tokens/palette.css`\nin `@nyuchi/mzizi-skills` (all 21 families, light and dark, generated from\n`palette.canonical.json` at the package root). Import it from your global stylesheet (in Astro,\n`src/styles/global.css`) and register the properties in the Tailwind 4 `@theme` block so\nutilities generate. In Rust the same palette is the `mzizi-tokens` crate, always on in\n`mzizi-roots`. How the 21 families are meant to be used, the radius scale and the Bundu floor (17px body, 14px captions, a 49px hit region on every control, a 14px inset and 7px gaps)\nare in the `mzizi-design` skill.\n\nPick the app's accent through `--brand-accent` rather than hardcoding a mineral hex: nyuchi =\ngold, mukoko = tanzanite, and a Mzizi surface = hematite (a Heritage tone, not a mineral; see the\nconstellation in `mzizi-design`).\n\n### 3. Add the Roots components\n\n```bash\ncargo add mzizi-roots # tokens, primitives, brand components, shell parts (no AppShell yet)\n```\n\nFor Astro, a small Rust crate (a build step, or a `workers-rs` Worker for request-time\nrendering) renders each component to HTML, and Astro places the fragment. The fragment carries\nthe same Tailwind classes as the React build, so the stylesheet from step 2 styles it, and it\nships no JavaScript. Which renderer that crate calls is undecided (mzizi-registry#635), so use\nthe pure `.astro` components for a working path. Interactive islands (Roots compiled to wasm\nand wrapped as custom elements) are proposed and not packaged yet.\n\nNever copy component source between apps. Depend on the crate, so every app inherits fixes.\n\n### 4. Give the app's AI assistants the doctrine\n\nInstall the published skills bundle so any assistant working in the repo has the design system,\nthe language and the components on hand:\n\n```bash\nnpm install -D @nyuchi/mzizi-skills\nnpx skills experimental_sync # links the bundled skills into .agents/skills/\n```\n\n`npx skills add @nyuchi/mzizi-skills` does **not** work: the `skills` CLI reads that argument\nas a GitHub repository and fails to clone it. `experimental_sync` is the CLI's route for skills\nthat ship in `node_modules`, and it is marked experimental. The same skills are served with no\ninstall at all by the MCP's `mzizi_get_skills`.\n\nThe public Claude Code plugin installs these skills and the MCP in one step:\n\n```\n/plugin marketplace add mzizi-dev/mzizi-registry\n/plugin install mzizi@mzizi\n```\n\nTo wire the MCP directly instead, add it to `.mcp.json` or your client config. It is free with\nno account; only the Fundi tools need sign-in:\n\n```json\n{\n \"mcpServers\": {\n \"mzizi\": { \"type\": \"http\", \"url\": \"https://mcp.mzizi.dev/mcp\" }\n }\n}\n```\n\n### 5. Let fundi read the project\n\n`@nyuchi/mzizi-cli` ships the **fundi** agent, which reads the project off disk and plans the\nwiring against the registry:\n\n```bash\npnpm add -D @nyuchi/mzizi-cli\npnpm exec fundi explore # offline — snapshot the project\npnpm exec fundi plan \"wire up Mzizi tokens\"\n```\n\nRun the bins from the installed package (`pnpm exec`, or `npx` inside the project). A bare\n`npx fundi` outside it fetches an unrelated npm package called `fundi`. `explore` runs without\ncredentials; `plan` and `chat` need `ANTHROPIC_API_KEY`. A plan is\ninert until you apply it, so read it before letting it write.\n\n### 6. Verify\n\n```bash\npnpm dev\n```\n\nConfirm the tokens resolve in light and dark, a rendered Roots fragment picks up the styles,\nand the page is readable with JavaScript off.\n\n### 7. An existing React app\n\nIf the app is already React, the React build installs with the CLI or shadcn, by absolute URL:\n\n```bash\npnpm add -D @nyuchi/mzizi-cli && pnpm exec mzizi add button # infers the type (tsx here)\nnpx shadcn@latest add https://api.mzizi.dev/v1/ui/mzizi-theme-provider\n```\n\nPoint `components.json` `registries.mzizi.url` at `https://api.mzizi.dev/v1/ui`. A bare name\nresolves against shadcn's own registry and installs a different component. Do not start a new\napp this way.\n\n## Server components\n\nA Roots server component is a **sans-IO core**: typed request in, typed response out, every\nexternal facility (a store, a clock, an outbound fetch, a secret) behind a trait. The host owns\nI/O and credentials. `n10-documentation/mzizi-docs-api.rs` is the model: it parses a path into a\ntyped `Route`, asks a `DocsStore` trait for data and returns an `ApiResponse`. `mzizi-assurance`,\n`mzizi-fundi` and `mzizi-discovery` have no dependencies at all. The RFC records a passing\n`wasm32-unknown-unknown` `cargo check` for those three and `mzizi-docs`; CI does not run a\nwasm32 job yet.\n\nA host adapter mounts the core: a `workers-rs` adapter for Cloudflare Workers, an axum adapter\nfor containers. Neither adapter crate exists yet. Until then the host writes the few lines that\nturn its request into the core's request (see the `mzizi-backend` skill).\n\n## Contracts\n\nEvery Roots component carries a contract, in two parts:\n\n1. **The registry contract:** one registry name, description, dependencies and variants shared\n by both builds, and the same `data-slot`, `data-portal` and tokens. Each crate's\n `tests/contract.rs` reads the `.tsx` sibling and asserts that the slot, the portal and every\n variant value the Rust emits appear there too. A deliberate divergence, such as a touch\n target raised to the 49px hit region, is asserted from both sides, so the test fails when the\n React build is fixed.\n2. **Checkable clauses** in the Mzizi language's `contract … end` grammar (RFC-0006; the\n `mzizi-language` skill), exported as `pub const CONTRACT: &str`. From\n `n3-brand/mzizi-hero-stat.rs`:\n\n ```rust\n pub const CONTRACT: &str = r#\"contract\n slot is \"mzizi-hero-stat\"\n role is \"region\"\n label not_empty\n button \"Share\" min_height 48\n end\"#;\n ```\n\n `min_height 48` is that component's own clause, quoted as the registry has it: a minimum\n **drawn** height. It is not the floor. The floor is the **49px hit region**\n (`BUNDU_FLOOR.hitAreaPx`, `lib/tokens/roles.source.ts` in mzizi-registry), which a control keeps\n however tall it is drawn.\n\n `mzizi-brand`'s contract suite renders each component in named states and\n evaluates every clause against the markup it emits. **A clause it cannot evaluate fails.**\n `mzizi_roots::brand::CONTRACTS` lists every brand component with its contract.\n\nToday the twelve `mzizi-brand` components export `CONTRACT`. The other crates check the\nregistry contract against the `.tsx`, and their `CONTRACT` clauses arrive batch by batch.\nServer-component clauses (status per route, CORS and cache headers, no credential in the core)\nwait on the language's contracts-everywhere RFC (RFC-0010). Until then their tests assert those\nproperties against an in-memory implementation of the traits. The language's own `service`\ncontracts (`example` and `ensure`, RFC-0011) apply to a Mzizi `service`, not to these Rust\ncrates. No component lowers to Rust yet (only a Mzizi `service` does, to a local axum\npackage), so `mz contract` does not read a Roots `CONTRACT`.\n\n### The Dashboard and Discover Standards' contracts\n\nThe registry's\n[`contracts/`](https://github.com/mzizi-dev/mzizi-registry/tree/main/contracts) holds a\nmachine-readable, versioned contract for every component of `@bundu/ui` (built in\n`mzizi-dev/packages-npm`), in two families of one format:\n\n- **`app/`, the Mzizi Dashboard Standard** (since registry `a298a4e`, 2026-10-04,\n mzizi-registry #404): the one dashboard design for every product in the Bundu ecosystem,\n with each brand's mineral as an overlay (docs.mzizi.dev/patterns/dashboard-standard). Its 35\n contracts are the server-rendered app components (`@bundu/ui/app/*.astro`): the shell, the\n page and data patterns, and seven primitives.\n- **`discover/`, the Mzizi Discover Standard** (since registry `0bea10c`, 2026-10-04,\n mzizi-registry #415, tracking issue #413): one design for every public discover and browse\n page in the Mukoko family (docs.mzizi.dev/patterns/discover-standard). Its 16 contracts are\n the Discover components (`@bundu/ui/discover/*.astro`). \"The Discover Standard\" below says\n what is different.\n\n| File | What it is |\n| ------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| `contracts/schema/component-contract.schema.json` | the format, one for both families |\n| `contracts/app/<name>.contract.json` | one Dashboard Standard component's contract |\n| `contracts/discover/<name>.contract.json` | one Discover Standard component's contract |\n| `contracts/site/<name>.contract.json` | one marketing-site component's contract |\n| `contracts/ui/<name>.contract.json` | one Astro port of a registry primitive (native select, segmented control, …) |\n| `contracts/index.json` | all 77 component contracts with their version and `.astro`, `.tsx`, `.rs` items |\n| `contracts/README.md` | the coverage tables, the recorded gaps, the shared rules, how to change a contract |\n\n**Read the contract before you build or change a `@bundu/ui` component, in any language.** It is\nthe specification: `props` and `slots` (types, required, defaults, closed `values`; the\n`.astro` file's props and slots must be exactly these), `behaviour`, `states` (named fixtures,\n`default` required; every runner renders every state), `accessibility` (roles, keyboard,\nfocus, ARIA, WCAG 2.2 criteria), `density`, `theming`, `noJs`, `responsive`, the `contract`\nblock and `checks` (selector assertions on the rendered markup). `implementations` names the\nAstro export and the `.tsx` and `.rs` siblings, with what they share (`identity`: `slot`,\n`slot+role` or `slot+variants`) and their `divergences`; items under `related` look similar\nand have a different contract, so they are not implementations. `implementations.astro`\ngives the package, the export and `since`: the `@bundu/ui` release that first shipped it, 0.3.0\nfor the original two families (mzizi-registry #419) and 0.6.0 for the components added since\n(money, plan summary, tier status, confirm action, the markdown renderer, and the trust and\naddress primitives).\n`@bundu/ui` is 0.7.0 on npm. `gaps` records where the current build falls short.\n\n**Every new build starts from its contract, library or component** (owner, 2026-10-31). Write or\nchange the contract first, then the builds. A fifth family, `lib/`, holds\ncontracts for non-visual libraries: the N5 resilience primitives (circuit breaker, retry,\nbulkhead, rate limiter, fallback chain, deadline) and the N8 chaos engine, in their own format\n(`schema/lib-contract.schema.json`), each invariant held by a test in the implementing crate\n(mzizi-registry #585, released in v4.18.0).\n\nFour fields are requirements. packages-npm's contract test evaluates the first three on the\nrendered Astro build; the responsive rules are prose. The values below are the Dashboard\nStandard's; the Discover Standard's differ (below):\n\n- **No-JS fallback.** `noJs.script` is `none` (no script) or `enhancement` (one script, and\n everything in `noJs.without` works when it does not run). Only AppShell and CommandPalette\n are `enhancement`; every other app component ships no script.\n- **Density.** Each row gives a part's height in px for a fine pointer and a coarse one\n (touch), read from the rendered classes (`h-N`, `min-h-N`, `size-N` = N×4;\n `pointer-coarse:` for touch). Dashboards are drawn dense on a mouse and at the floor on touch:\n the app Button's `md` row reads 36px fine and 48px coarse as the **drawn** height (its\n `button.contract.json` density rows), with the small radius; a button is a `hit-area` element,\n so its **hit region is 49px**, the floor, on both pointers, and its `sm` link row is a pinned gap, and nav rows\n are drawn 32 on a mouse. The registry primitives read the same density from the size roles\n (`h-control`: 49px coarse, 36px drawn at desktop density, hit region 49px either way), which the\n runner and `mzizi-rs/contract-eval` understand (`ROLE_HEIGHTS`). Since the Bundu floor (registry\n 4.23.0) both fail a density row whose interactive control has a hit region under 49px on either\n pointer, or a button-like control drawn under 28px; they count the 49px hit region only where the\n token layer's `hit-area` rule provably applies (`contracts/hit-region-cases.json`). Rows that\n predate the floor are pinned in `HIT_AREA_GAPS` and listed in `gaps`, not hidden.\n- **Theming.** A brand overlay changes colour only: `--primary`, `--ring`, AppShell's\n `--app-accent` and the brand mark. Layout never varies by brand. A mineral may be named only\n as one of the component's declared `statusColours`; any other mineral class, or any colour\n value, fails (the `mzizi-design` skill).\n- **Responsive.** Each contract's rules from 320px up.\n\nIn both families the tests also fail **any inline `style` attribute** on a rendered element, so\na page's Content-Security-Policy can keep `style-src 'self'` with no\n`style-src-attr 'unsafe-inline'`: custom properties go through classes or data attributes\n(AppShell's `data-accent`), sizes through classes or SVG geometry (BarChart). JSON-LD\n(`type=\"application/ld+json\"`) is data and does not count as a script.\n\n**How they relate to `CONTRACT` and the language.** The `contract` field is a\n`contract … end` block in the same clause grammar as a Roots `pub const CONTRACT` (RFC-0006\nand RFC-0010), restricted to what applies to rendered markup: subjects `slot`, `role`,\n`label`, `class`, `portal`, `<element> \"<text>\"`, `uses <data-slot>` and `when <state> …`;\npredicates `is`, `contains`, `not_empty`, `in`, `uses \"--token\"`, `min_height` and `shows`. As\nin the language, **a clause, check or density row a runner cannot evaluate fails** (FM-12).\nThe registry's own suites evaluate them: `__tests__/astro/contracts.test.ts` renders every\n`.astro` in every state (Astro's container, no renderer), `__tests__/contracts/\ntsx-contracts.test.tsx` renders every `contract`-identity `.tsx` with the same runner\n(`contracts/runner.ts`), and `__tests__/contracts/contracts.test.tsx` validates every contract\nagainst the schema and the grammar and checks each identity-only sibling. packages-npm's\n`src/app/contracts.test.ts` runs the same contracts again on the built package. A crate's own `CONTRACT`\nand `tests/contract.rs` still run as before.\n\nEvery contract has an `.astro` (`implementations.astro.registry`). The seven app primitives'\n`.tsx` are the registry's own `button`, `badge`, `card`, `alert`, `input`, `label` and\n`skeleton`, which share only their identity (the contract lists the divergences), as do the\n`ui/` family's; every other `.tsx` (`app-*`, `discover-*`, `site-*`) has\n`identity: \"contract\"`, held to the whole contract exactly as the Astro build is. A `.rs`\nexists in `mzizi-ui` for `button`, `badge`, `card`, `input`, `label` and `safe-area-frame`;\nmzizi-registry #401 tracks Rust ports of the rest, done when each passes the contract.\n\n### The Discover Standard\n\nThe 11 Discover Standard list-page contracts in `discover/` and the node each declares (the detail pattern below adds five more). DiscoverShell's `kind` is `shell`;\nthe other ten are `pattern`s.\n\n| Contract | Component | Node | What it is |\n| --------------------------- | --------------- | ---- | --------------------------------------------------------------------------------------------- |\n| `discover/discover-shell` | DiscoverShell | N7 | the frame: skip link, mineral strip, header (brand, \"Main\" nav, one action), `<main>`, footer |\n| `discover/discover-meta` | DiscoverMeta | N6 | the `<head>` tags: title, description, robots, canonical, Open Graph, the X card, JSON-LD |\n| `discover/discover-hero` | DiscoverHero | N6 | breadcrumbs, eyebrow, the page's one `<h1>`, lead, the search box and actions |\n| `discover/discover-search` | DiscoverSearch | N6 | the one search box: a `role=\"search\"` GET form, so every search is a URL |\n| `discover/category-chips` | CategoryChips | N6 | the category links as a named navigation landmark; chips wrap |\n| `discover/category-chip` | CategoryChip | N6 | one category link, with an optional count; a link, never a button |\n| `discover/discover-section` | DiscoverSection | N6 | one band: eyebrow, heading, description and \"See all\" over its content |\n| `discover/result-grid` | ResultGrid | N6 | a polite status line, the cards, the empty state and \"load more\"; `grid`, `list` or `rail` |\n| `discover/discover-card` | DiscoverCard | N6 | one card for every page, variants `article`, `event`, `circle`, `place` |\n| `discover/load-more` | LoadMore | N6 | cursor pagination: one `rel=\"next\"` link to `?cursor=…` |\n| `discover/open-in-app` | OpenInApp | N6 | \"Open in Mukoko\": an https link into the super-app, with one line saying what happens |\n\nSame format, schema, clause grammar and runners as the Dashboard Standard. What the Discover\ncontracts state differently:\n\n- **What it is for.** Public discover and browse pages in the Mukoko family (circles, news,\n events, weather, and the super-app on the web), not admin or operator dashboards. It reuses\n the Dashboard Standard where they overlap: ResultGrid's `empty` slot takes `app/empty-state`\n or `app/state-message`, and `related` names `app/filter-bar` and `app/pagination` as similar\n components with a different contract.\n- **No script at all.** Every Discover contract's `noJs.script` is `none`; there is no\n `enhancement` like AppShell's or CommandPalette's. Views are URLs, not client state: search\n is a GET form (`?q=`, with no `cursor`, so a new search starts at the first page), a category\n is a link, and paging is LoadMore's `rel=\"next\"` link to `?cursor=`.\n- **Sizes come from the size roles, with gaps still pinned.** Discover's search field is 49px on\n both pointers and its submit button 36px drawn on a mouse and 49px on touch (the size roles,\n mzizi-registry#597). \"See all\", LoadMore, OpenInApp, DiscoverCard's app link, the category\n chips, the navigation links and the home link are pinned in the registry's `HIT_AREA_GAPS`\n (links, or literal `h-11`/`h-12` classes) until they move onto a size role or take `hit-area`;\n do not copy their heights. Until 2026-11-01 Discover was the same size on every pointer; the\n Bundu floor replaced that.\n- **Theming.** The brand-overlay rule is the same (colour only, no colour values, a mineral\n only as a declared status colour), but there is no AppShell `--app-accent`. A brand shows in\n DiscoverShell's `brand` slot (its mark) and in whatever primary action its `action` slot\n holds; the frame, header, navigation and footer are identical in every brand. `--primary` reaches only\n DiscoverHero's wash (`gradient-showcase`), DiscoverCard's monogram and `brand` badge tone,\n and OpenInApp's `primary` variant. Chips, sections and LoadMore are neutral in every brand,\n and DiscoverSearch follows the brand only in its focus ring (`--ring`). Only DiscoverCard\n declares status colours (`cobalt`, `malachite`, `gold`, `terracotta`), for its badge tones.\n- **Server-filled shells.** Every Discover component also works in an Astro page built once\n with `{{placeholders}}` and filled by a server, as circles.mukoko.com's Rust Worker does:\n text props are plain strings, text that fills to `\"\"` hides itself, and state switches are\n attributes styled by classes (ResultGrid, DiscoverSection and LoadMore `state`, DiscoverCard\n `badgeTone`, CategoryChip `current`). CategoryChip, DiscoverCard, DiscoverHero, LoadMore and\n ResultGrid have a `template` state with placeholders, rendered like every other state.\n DiscoverCard and CategoryChip render their own `<li>`, so they repeat as fragments.\n- **One card.** DiscoverCard's `variant` changes emphasis, never structure. Its title is the\n one primary link, stretched over the card; other actions stay separately focusable.\n- **OpenInApp's `href` is https** (a universal link or a server redirect), never a bare\n `mukoko://` link, which does nothing without the app.\n- **No siblings and no gaps yet.** No `.tsx` or `.rs` implements a Discover contract, and none\n records `gaps`. A React or Rust port is done when it passes the same contract.\n\n**Upstream first** (registry `CONTRIBUTING.md`, from #415, for both families): a component an\napp needs that Mzizi does not have, or a change to one it has, goes upstream immediately: its\ncontract in the registry, its build in `@bundu/ui` and its page on docs.mzizi.dev. An app keeps\na local copy only while that upstream pull request is open, marked at the top of the file with\n`TODO(mzizi): <upstream PR URL>`.\n\n### Changing a contract\n\n1. Edit `contracts/<family>/<name>.contract.json` in the registry and bump its `version`:\n major when a clause, prop, slot or state is removed or narrowed, minor when one is added,\n patch for prose. A new family also needs its directory in `FAMILIES` in\n `__tests__/contracts/contracts.test.tsx` and in packages-npm's `scripts/contract-paths.mjs`.\n2. Update `contracts/index.json` and the README's coverage table for that family. The test\n fails if either disagrees.\n3. Run `pnpm test` in the registry, then `pnpm contracts:fetch <branch>` and `pnpm test` in\n packages-npm. The Astro build must pass the new contract before either side merges. Never\n edit the copy in packages-npm.\n\n**The detail pattern** (mzizi-registry #429; docs.mzizi.dev/patterns/discover-detail): the\npage for one item reached from a discover page uses five more `discover/` contracts, each an\n`.astro` and a `.tsx`: `detail-hero` (the page's only `<h1>`, when/where, lead, image or media\nslot), `breadcrumb` (named `<nav>`, `aria-current=\"page\"`, BreadcrumbList JSON-LD), `meta-list`\n(a `<dl>` of facts), `detail-actions` (Open in Mukoko first, then secondary and share links; no\nscript) and `related-rail` (DiscoverCards; a scroll-snap row on phones, a grid from 48rem;\nhidden when empty).\n\n**Open in Mukoko** is one canonical https universal link, `mukoko.com/open/<service>/<id>`\n(`openInMukokoUrl` in `discover-open-link.ts`; `@bundu/ui/discover/open`). `open-in-app` 1.1.0\nand `detail-actions` take `service` (`news`, `events`, `weather`, `circles`) and `id`; `href` is\nonly an override. The apps claim `mukoko.com/open/*` as universal and app links; on the web a\nresolver (mukoko-dev/super-app-web `/open/*`) sends the reader to the service's page. Never a\nbare `mukoko://` link.\n\n## Reuse before build\n\n**The simplest change is the one you don't write.** Reuse before you build, compose before you\nimplement, and let one source own each decision. Use this before adding a component, a style or\na page, and during any refactor.\n\n1. **Reuse before build.** Search the registry first: `mzizi_search` or\n `mzizi_list_components` over the MCP (`https://mcp.mzizi.dev/mcp`, free), or\n `GET https://api.mzizi.dev/v1/ui`. Where a Rust implementation exists, depend on its crate\n (`cargo add mzizi-roots`). For the React build, install with\n `mzizi add <name>` or `npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>`. Never\n reimplement a primitive that already exists.\n2. **Styles are tokens.** Never hardcode a colour, radius, spacing, or motion value —\n consume `var(--…)` (or the `mzizi-tokens` consts in Rust). N1 is the only layer allowed to\n define values, so a rebrand is one edit. A new colour goes in the registry's\n `lib/tokens/palette.source.ts`, and `pnpm tokens:sync` regenerates `styles/globals.css`, the Rust\n tokens and the other emitters from it.\n3. **Compose, don't implement.** A page (N6) is a composition of N2 primitives and N3 brand\n components — never inline a `<button>`, a card, or an SVG. Pull spinners, skeletons, and\n badges from N2.\n4. **Variants over copies.** Collapse near-duplicate components into one variant table: an\n enum with a `match` in Rust, an `enum` table in Mzizi source, one `cva()` in the React\n build. Express differences as `variant` / `size` props, not new files.\n5. **One composition path.** In the React build, compose class names through `cn()` and\n extract shared logic into `lib/` or a hook. In Rust, a pure function beside the component.\n No logic duplicated across files.\n6. **Right altitude.** Keep each component self-contained, importing only from the layer\n below on its strand. A primitive that reaches up into page code can't be installed on its\n own.\n7. **Delete first.** Prefer removing over adding — dead code, unused props, redundant\n wrappers, commented-out blocks. Less code is less to break, read, and keep true.\n8. **Live data, not copies.** Read counts, tokens and doctrine from their source\n (`https://api.mzizi.dev/v1/stats`, `palette.canonical.json`, `mzizi_get_tokens`,\n `mzizi_get_doctrine`). A baked-in number or duplicated token guarantees drift.\n\n### Checklist\n\n- [ ] Searched the registry before building, and used the Rust build where one exists?\n- [ ] All styles are tokens (`var(--…)`), zero hardcoded values outside N1?\n- [ ] Page is pure composition of N2/N3, no inlined primitives?\n- [ ] Variation expressed as one variant table, not near-duplicate files?\n- [ ] One composition path, shared logic extracted once?\n- [ ] Each file self-contained and installable, importing only downward?\n- [ ] Deleted more than you added where you could?\n- [ ] Counts, tokens, and doctrine read live, not hardcoded?\n\n## Contributing a component\n\n**A component is a real file.** `components/registry/n<N>-<label>/<name>.rs` (Rust, compiled by\nthe node's crate in `mzizi-rs/`) and, for the React build, `<name>.tsx` beside it, which\n`pnpm typecheck` (`tsc --noEmit`) and `pnpm lint` check. An error in either fails CI. The\nregistry has no app to build: CI's `Rust` job runs `cargo fmt`, `check`, `clippy` and the\ncontract tests, and its `Build` job runs `pnpm build`, which runs every generator in write mode\nand fails if that changes a committed file.\n\n| File | What it is |\n| --------------------------------------------- | ----------------------------------------------------------------- |\n| `components/registry/n<N>-<label>/<name>.rs` | the Roots component (a UI component, or a sans-IO server core) |\n| `components/registry/n<N>-<label>/<name>.tsx` | the React build of the same contract |\n| `registry.json` | the manifest `/v1/ui/<name>` and `npx shadcn add` resolve against |\n\n`registry.json` carries the install contract (description, dependencies,\n`registryDependencies`, target paths) and **never the source**. The node is derived from the\ndirectory the component lives in, so it cannot disagree with where the file is.\n\n**There is no database.** The registry's files are its data layer (mzizi-registry PR #368), and\na test fails the build if a Supabase import or a `SUPABASE_*` read comes back. Nothing about a\ncomponent, including its source, status or history, is written to a table. If you find\ninstructions anywhere telling you to `INSERT` a component or read a `component_documents` row,\nthey are stale.\n\n### Step 1 — Decide the node\n\nThe architecture is the **Mzizi DNA double helix**: eight nodes (N1–N8) on the engineering\nbackbone, four rungs (N9–N12) that bridge both backbones, and six strands. The N-numbers are\n**labels, not a sequence**.\n\nThe node decides the **directory**, the directory decides the **crate** that compiles the Rust\n(`mzizi-rs/crate-for-node.json`), and the directory is what the build reads. Choosing the node\nand choosing where the file goes are the same decision.\n\n| Node | Directory | Crate | What belongs there |\n| ----------------- | -------------------- | ----------------- | --------------------------------------------------------------------------- |\n| N1 tokens | `n1-tokens/` | `mzizi-tokens` | A token value or emitter (colour, spacing, motion, radius), not a component |\n| N2 primitive | `n2-primitives/` | `mzizi-ui` | Generic UI: button, card, input, dialog, toolbar, bento-grid |\n| N3 brand | `n3-brand/` | `mzizi-brand` | A branded composition with the mineral palette |\n| N4 safety | `n4-safety/` | none yet | A conditional-rendering gate: permission, geo, rate limit |\n| N5 resilience | `n5-resilience/` | none yet | Error boundary, skeleton, offline banner, fallback chain |\n| N6 pages | `n6-pages/` | none yet | Full-screen layout composition |\n| N7 shell | `n7-shell/` | `mzizi-shell` | App container: navigation, routing, lifecycle |\n| N8 assurance | `n8-assurance/` | `mzizi-assurance` | Instrumentation, a11y audit, RTL conformity, probes |\n| N9 fundi | `n9-fundi/` | `mzizi-fundi` | Automated healing (a rung) |\n| N10 documentation | `n10-documentation/` | `mzizi-docs` | Docs, AI instructions, docs infrastructure (a rung) |\n| N11 discovery | `n11-discovery/` | `mzizi-discovery` | SEO / AIO surfaces: if the machine can't see it, it doesn't exist (a rung) |\n| N12 skills | none in the registry | none | Agent skills (a rung), authored in `mzizi-dev/agent-tools/mzizi-skills/` |\n\nA node with no crate yet (N4, N5, N6) takes a `.tsx` today. Adding its first `.rs` means adding\nits crate and its `crate-for-node.json` entry in the same pull request; `pnpm rust:generate`\nrefuses Rust that no crate claims.\n\nIf you are unsure where something belongs, ask the MCP (`https://mcp.mzizi.dev/mcp`, free, no\naccount): `mzizi_list_components` with `node: N` shows what already lives in a node, and\n`mzizi_get_architecture` returns the node, rung and strand model with live counts. Never\nhardcode counts.\n\n### Step 2 — Write the file\n\nCreate it in the directory the node names:\n\n```\ncomponents/registry/n3-brand/mzizi-your-component.rs # the Roots component\ncomponents/registry/n3-brand/mzizi-your-component.tsx # the React build, same contract\n```\n\nThe Rust file carries `pub const CONTRACT: &str` and follows the rules in \"Using a UI component\" above: variants as enums, tokens by reference, browser facilities as props, icons as slots,\nno helpers shared between components. `.ts` is for a non-rendering module.\n\nThat is the authoring step. There is no row to insert and no registration to perform before\nwriting code: the file _is_ the component, and `cargo test` (for the `.rs`), `pnpm typecheck`\nand `pnpm lint` (for the `.tsx`) and `pnpm build` (every derived artifact regenerated, with no\ndiff) are what tell you it is real.\n\nNaming: N3 brand components take a `mzizi-` prefix. (Components formerly named `nyuchi-*` are\n`mzizi-*`, and the old names redirect.) Everything else is the bare name, matching what a\nconsumer will type in `mzizi add` or `npx shadcn add`.\n\n### Wiring the Rust into its crate\n\n`pnpm rust:generate` writes the committed copy to `mzizi-rs/crates/<crate>/src/generated/`. Add\nthe `#[path = \"generated/<name>.rs\"] pub mod <name_snake>;` line and the `pub use` to the\ncrate's `src/lib.rs` by hand, and add the component to the crate's `tests/contract.rs`. Edit\nthe registry file, never the copy. The workspace lints forbid `unsafe_code` and warn on\n`missing_docs`; clippy turns the warning into a failure. Cap parallel builds on a small machine\n(`CARGO_BUILD_JOBS=2`).\n\nA release is a version bump in `mzizi-rs/Cargo.toml`. On merge to `main`,\n`.github/workflows/publish-crates.yml` re-runs the Rust gate and publishes each crate in\ndependency order, skipping any version already on crates.io. A published crate name is\npermanent. Say in the PR body that components changed, so the site, docs and skills are\nupdated to match.\n\n### Step 3 — Declare it in `registry.json`\n\nOne item, carrying the install contract. `meta` is the part you author; four other fields are\n**derived from it** and must not be hand-written.\n\n```jsonc\n{\n \"name\": \"your-component-name\",\n \"type\": \"registry:ui\",\n \"description\": \"Short description of what it does.\",\n \"dependencies\": [],\n \"registryDependencies\": [],\n \"files\": [\n {\n \"path\": \"components/registry/n2-primitives/your-component-name.tsx\",\n \"target\": \"components/ui/your-component-name.tsx\",\n \"type\": \"registry:ui\"\n }\n ],\n \"meta\": {\n \"collection\": \"primitives\",\n \"owner\": \"mzizi\",\n \"useCases\": [\"Use case 1\", \"Use case 2\"],\n \"variants\": [\"default\", \"sm\", \"lg\"],\n \"sizes\": [\"default\", \"sm\", \"lg\"],\n \"features\": [\"cn() for className composition\", \"data-slot attribute\"],\n \"a11y\": [\"ARIA attributes\", \"Keyboard accessible\"],\n \"hasDemo\": true\n }\n}\n```\n\n`files[]` lists the `.tsx`: it is the shadcn install contract for the React build. The `.rs`\nbeside it is not listed; the node's crate compiles it, and `/v1/rs/<name>` serves it with the\ncrate's name.\n\n**Do not write `title`, `categories`, `docs` or `author`.** `pnpm registry:metadata` derives all\nfour from `meta` and the repo. `docs` in particular is what the shadcn CLI prints _after_\ninstalling, so it is generated from your `useCases` / `variants` / `sizes` / `features` / `a11y`\nrather than duplicated by hand — and an item with no `meta` deliberately gets no `docs`, because\na stub reads as documentation and says nothing.\n\nThen run the generators and let the gates check you:\n\n```bash\npnpm registry:paths # fill file paths from what is on disk\npnpm registry:metadata # derive title, categories, docs, author\npnpm registry:normalize # canonical ordering\npnpm registry:validate # every item resolves on disk; every dependency addressable\npnpm registry:verify # the CI drift gate\npnpm typecheck && pnpm lint && pnpm build # build: every generator; commit what it writes\n```\n\nFollow the criteria for the chosen node.\n\n#### Every node: the Rust file\n\n- `pub const CONTRACT: &str` with the clauses the rendered markup must meet (slot, role, touch\n floors, variant tables), evaluated by the crate's `tests/contract.rs` on the rendered markup\n- The same `data-slot`, `data-portal` and variant values as the `.tsx`, asserted by the same\n suite; a deliberate divergence is asserted from both sides\n- `prefers_reduced_motion: bool` as a prop where the `.tsx` reads the harness's motion\n- Icons as `Element` slots, not an icon crate\n- Doc comments on every public item (`missing_docs` is a warning, and clippy runs with\n `-D warnings`); no `unsafe`\n\n#### N2 primitive checklist\n\n- No `useMziziHarness` import in the `.tsx`\n- `data-slot` attribute on the root element\n- `data-portal` attribute pointing at `https://mzizi.dev/components/<name>`\n- `cn()` for className composition in the `.tsx`\n- No raw Tailwind colours: use semantic tokens (`bg-primary`, `text-foreground`, …)\n- Icons from `@/lib/icons` in the `.tsx`, never `lucide-react` directly\n- **The Bundu floor: every interactive control has a 49px hit region on every device** (owner,\n 2026-11-02: 7 x 7, above Apple's 44 pt; desktop included). Body text is 17px and captions 14px\n everywhere; nothing readable is under 14px, so use `text-sm` (14px), never `text-[10px]`.\n Buttons and inputs take their height from the size roles, never `h-12`, `h-14` or\n `min-h-[44px]`: `h-control` (49px touch, drawn 36px at desktop density), `h-control-sm`,\n `h-control-lg` (56px touch), `size-control-icon`, `h-field` (49px drawn everywhere, because a\n native field cannot carry a pseudo-element), as `button.tsx`, `button.astro` and `button.rs`\n state, and `/v1/brand` `.componentSpecs` serves (`minTouchTarget: 49`). At desktop density a\n button-like control may be drawn 28 to 40px: the token layer's `hit-area` rule gives it a\n transparent `::after` of `max(100%, var(--target-floor))`. The rule steps aside for an element\n with its own `after:` utility, for menu and listbox rows (they take `min-height: var(--target-floor)`),\n for a calendar day and for an element with `overflow: hidden`; a standalone link that is a target\n is drawn at 49px or takes `hit-area`. `primitives/button.mz` still says 56 until it is updated in\n its own repository. A smaller visual box, such as a badge, is not a touch target; if it becomes\n one, give it `min-h-touch-min`. `data-mzizi-sizes=\"legacy\"` on the root is the compatibility\n alias that restores the pre-2026-11-01 sizes (and steps out of the floor); it is planned for removal\n in the next major, so set it only to buy time. Contrast floors beside it: body 7:1, non-body\n 4.5:1, UI 3:1, with five button-label roles held at 4.5:1 (`primary-foreground`,\n `brand-accent-foreground`, `app-accent-foreground`, `sidebar-primary-foreground`,\n `destructive-foreground`). `text-base` can collide with `--color-base`: write `text-body`\n (details in `mzizi-design`).\n- **The spacing floor: padding, gaps and leading have minimums too** (owner, 2026-11-04, mzizi-registry#617; `BUNDU_FLOOR.spacing`). **Inset 14px** between text and its container edge (`--inset-floor`), **7px** between adjacent controls, between a label and its field and between sibling items (`--gap-floor`; visible edge to visible edge, hit regions may touch but never overlap), and **1.5** body line height (`--leading-floor`). The base layer gives every element the floor even with no padding or gap set; the token test fails a component spec below it; every component contract asserts its spacing minimums, and the TypeScript runner and `mzizi-rs/contract-eval` must agree; a rendered geometry guard checks neighbouring controls, text inset and WCAG 1.4.12 stress. A component you add or change keeps at or above these numbers in its spec and its contract, in the `.tsx`, the `.astro` and the `.rs` alike, and nothing may lower them (no brand pack, no density layer); `data-mzizi-sizes=\"legacy\"` is the only opt-out. Use the components and tokens, and never `p-0`, `px-0`, `gap-0`, a negative margin, `leading-none`, `leading-tight` or an arbitrary `p-[4px]` on a text container or a row of controls without an exception comment (`mzizi-spacing-exempt: <reason>`); `mzizi check` (`@nyuchi/mzizi-cli` 0.8.0 or later) fails the project otherwise. A registry or `@bundu/ui` older than the spacing floor has none of the custom properties. Details in `mzizi-design`.\n- Pill-shaped categories (button, input, avatar, badge, toggle) use `rounded-full`\n\n#### N3 brand checklist\n\n- `mzizi-` prefix on the name\n- `useMziziHarness(\"<name>\")` in the `.tsx` for the motion preference (and `log` or\n `LiveRegion` where the component uses them); `prefers_reduced_motion` as a prop in the `.rs`\n- The entrance animation off when motion is reduced\n- An ARIA role or `aria-label`, and a region that is named in every state, the loading\n skeleton included\n- `data-slot` and `data-portal` attributes, in every state\n- `focus-visible` ring on interactive elements, and interactive elements with a 49px hit region\n- I18N via `Intl` formatters for dates, numbers and currency\n- Semantic tokens for status-bearing colours\n\n#### N6 page checklist\n\n- Pure composition: no inline buttons, cards or SVGs\n- Accept children/slots for content\n- Semantic CSS vars only (`bg-card`, `text-foreground`, `bg-primary`)\n- A loading-state prop\n- `role=\"main\"` and an `aria-label`\n\n#### N8 assurance checklist\n\n- A sans-IO Rust core (typed input, typed result, every facility behind a trait) in\n `mzizi-assurance`, with the `.ts` module as the React build\n- A banner comment identifying the node and purpose\n- A configurable rules array\n- Optional `onViolation` / `onComplete` callbacks\n\n### Step 4 — Docs and demos\n\nThere is nothing extra to write. Docs come from the `meta` you already authored in Step 3\n(`useCases`, `variants`, `sizes`, `features`, `a11y`), and `pnpm registry:metadata` turns them\ninto the `docs` string the CLI prints after install. Set `meta.hasDemo` when a demo exists.\n\nIf a prop table is expected, `pnpm props:extract` derives it from the component's own types and\n`pnpm props:verify` is the gate. Props are read from the code, never restated beside it.\n\n### Step 5 — Versions and history\n\nThere is no status row and no database. A component's version history is the registry's\nchangelog (`content/changelog/`, generated, checked by `pnpm changelog:generate:check`) and the\nrename map. `mzizi_get_component` with `include: [\"versions\"]` returns it. Read what a component\nis through the MCP or `https://api.mzizi.dev/v1/ui/<name>` rather than assuming.\n\n### Step 6 — Validate accessibility\n\nIf your component introduces a new colour pair, validate it before shipping. The MCP tool is\ncomputed locally and needs no account:\n\n```\nmzizi_check_accessibility(foreground: \"#FFFFFF\", background: \"#0047AB\")\n```\n\nIt returns the WCAG contrast ratio with AA/AAA verdicts and whether the pair clears 7:1, the\nbody-text floor. Non-body text needs 4.5:1 and UI parts 3:1 (five button-label roles hold 4.5:1);\nthe tool does not compute APCA, so read the role values in `mzizi_get_tokens` for those.\n\n### Step 7 — Before you open the PR\n\n`pnpm check` runs every registry gate CI runs, in order, including `registry:validate`,\n`registry:verify`, `tokens:verify`, `changelog:generate:check`, `rust:generate:check`, the\ntests and the build. Then the Rust workspace gate:\n\n```bash\npnpm check\ncargo fmt --manifest-path mzizi-rs/Cargo.toml --all -- --check\ncargo clippy --manifest-path mzizi-rs/Cargo.toml --workspace --all-targets -- -D warnings\ncargo test --manifest-path mzizi-rs/Cargo.toml --workspace\ncargo package --manifest-path mzizi-rs/Cargo.toml --workspace\n```\n\nIf you touched `files[]` or `meta`, run `pnpm registry:paths`, `pnpm registry:metadata` and\n`pnpm registry:normalize` first; their `--check` twins fail otherwise.\n\nA green run means the item resolves to real files, every dependency is addressable, the derived\nfields match `meta`, the Rust compiles, and its contract holds on rendered markup. Say in the PR\nbody that a component changed, so the site, the docs and the skills are updated to match.\n\n### What to avoid\n\n- **Never put component source in a database row, a JSON document, or any second place.** The\n file under `components/registry/` is the component; everything else is a representation of\n it, and a representation can drift from the code while still looking correct. That is not\n hypothetical here: a `source_code` column once served stale copies for 571 components.\n- Never edit `mzizi-rs/crates/*/src/generated/`. It is overwritten by `pnpm rust:generate`.\n- Never hand-write `title`, `categories`, `docs` or `author` in `registry.json`. They are\n derived, and a hand-written value is a second source that the next generator run silently\n overwrites.\n- Never hardcode hex colour values (except documented third-party brands like Ethereum,\n Google, EcoCash).\n- Never use `lucide-react` directly in a `.tsx`; go through `@/lib/icons`.\n- Never use `margin-left` / `padding-right` / `left`; use logical properties\n (`margin-inline-start`, …) for RTL support.\n- Never start new work in the React build alone. If a component has a Rust implementation,\n change the Rust and keep the `.tsx` in step.\n- Never bump a major version on your own initiative. That is a maintainers' call.\n\n## The React build\n\n`npx shadcn@latest add https://api.mzizi.dev/v1/ui/<name>` installs the `.tsx` for an existing\nReact app, for all 577 items. Always pass the absolute URL: a bare name resolves against\nshadcn's own registry and installs a different component. `mzizi add <name>` installs the React\nbuild too. Neither is where new work goes.\n\n## See also\n\n- `docs/roots/RFC-roots.md` in the registry: the inventory, the conversion batches and what\n the API and site still need for Rust-first serving (`/v1/roots` is proposed, not live).\n- The `mzizi-language` skill for the clause grammar and `mz contract`.\n- The `mzizi-design` skill for the palette, radius, type and the Ubuntu constraints.\n- The `mzizi-backend` skill for mounting a server core on Workers.",
25
25
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/mzizi-roots",
26
26
  "files": []
27
27
  },
@@ -209,7 +209,7 @@ export const BUNDLED_SKILLS = [
209
209
  "name": "mzizi-architecture",
210
210
  "category": "build",
211
211
  "description": "Use this skill before you start a new Mzizi app, site, console or page, before you add a page to one, and whenever you decide where code, data or a component belongs. Design architecture is the DNA helix (8 nodes, 4 rungs, 6 strands), read live from api.mzizi.dev/v1/architecture or mzizi_get_architecture, with what each placement obliges. App architecture puts the shell first: every app or page mounts exactly one shell before anything else (AppShell for a signed-in app, the Dashboard Standard; DiscoverShell for a public browse site, the Discover Standard), then the page patterns, the five application layers, and the rule that a missing component goes upstream. System architecture covers Rust first, Astro as the default UI, Cloudflare Workers, the four API gateways, files as the registry's data layer at a pinned commit, one service per domain database, and what is decided versus built. Use it when an app lacks a shell, a page invents its own chrome, or someone asks how the pieces fit.",
212
- "body_mdx": "# Mzizi architecture: design, app and system\n\nThree questions, in this order, before anything is built:\n\n1. **Design architecture.** Where on the helix does this belong, and what does that placement\n oblige it to do?\n2. **App architecture.** Which shell frames it? The shell is installed **first**, before the\n first page.\n3. **System architecture.** Which service owns the data, which gateway serves it, and what runs\n where?\n\nThe rule behind all three is **upstream first**. If the registry lacks a component, a shell or a\npattern that you need, or has one that is wrong, change it in `mzizi-dev/mzizi-registry`, its\ncontract in `contracts/` and its build in `@bundu/ui`. Keep a local copy only while that pull\nrequest is open, marked `TODO(mzizi): <PR URL>`. Never fork the shell or restyle it locally.\n\nThis skill describes registry `main` at v4.18.0 (`0d17818`, 2026-11-01), the Roots crates at\n0.7.0 and `@bundu/ui` 0.7.0. Counts move, so read the live sources below rather than this text.\n\n## Design architecture: the DNA helix\n\nThe design system is a double helix. Read it live, not from memory:\n\n```bash\ncurl -s https://api.mzizi.dev/v1/architecture # nodes, rungs, strands\ncurl -s https://api.mzizi.dev/v1/architecture/nodes/7 # one node, with its rules\n```\n\nOn the MCP, call `mzizi_get_architecture`, or `mzizi_get_architecture node=7` for one node. The\nnode number is uncapped, so never assume a range.\n\n**Eight nodes.** These are the backbone, and each owns one kind of thing:\n\n| Node | Name | Owns | Strand |\n| ---- | ---------------- | --------------------------------------------------------------- | -------------- |\n| N1 | Design tokens | the only node allowed to define CSS values | swappable |\n| N2 | Primitives | the smallest composable unit of UI | core-guarantee |\n| N3 | Brand components | primitives wrapped in Ubuntu, the only consumers of the harness | shipped |\n| N4 | Safety rails | input validation, output sanitisation, AI guardrails | core-guarantee |\n| N5 | Resilience | circuit breakers, retries, timeouts, fallback chains | core-guarantee |\n| N6 | Pages | composition of N2 and N3, implementing nothing | shipped |\n| N7 | Shell | the app chrome: header, footer, navigation, theme, mounted once | shipped |\n| N8 | Assurance | observability, health monitoring, SLO tracking | core-guarantee |\n\n**Four rungs.** These bridge both backbones: N9 Fundi (self-healing: it consumes N8 signals and\nfiles structured issues for people, and never auto-merges), N10 Documentation, N11 Discovery\n(SEO and AIO, preview images, structured data, agent-readiness) and N12 Skills (this bundle).\n\n**Six strands.** Four belong to the engineering backbone: `core-guarantee` (travels unchanged\nand cannot be turned off), `shipped` (in the box, but free to evolve), `swappable` (the only fork\nseams: tokens, icons and the framework) and `spine`. Two belong to the meaning backbone:\n`genetic-code` and `transcription` (\"Doctrine is data, not tribal knowledge\").\n\nWhat a placement obliges, from the nodes' own implementation rules:\n\n- **N1 only defines values.** No hex, `rgba()` or inline style outside N1. Everything else reads\n `var(--…)` or its platform equivalent.\n- **N6 composes and implements nothing.** A page imports from N2 and N3, never defines a\n primitive, and pulls a spinner or skeleton from N2 rather than inlining one. Pages render on\n the server by default (Astro, no client JavaScript).\n- **N7 is singular.** There is one shell per app. It is the only place that holds sticky\n positioning, backdrop blur and theme-provider wiring. It surfaces auth state and never\n implements auth logic. It is responsive from 320px up.\n- **N4, N5 and N8 are core guarantees.** You cannot configure accessibility, safety,\n resilience or observability away.\n\nDo not confuse the helix with the five application layers below. They are two models with two\nnumberings, so never quote a count from one while describing the other.\n\n## App architecture: the shell comes first\n\nThe failure this section exists to stop is apps built page by page with no shell installed. Each\none then hand-rolls its own header, nav and footer, drifts from every other app, and loses the\nno-JS, density, accessibility and brand-overlay guarantees the shell's contract carries.\n**Step one of any new app is the shell.** Step one of any new page in an existing app is to\nconfirm the page renders inside it.\n\n### 1. Pick the shell from the kind of surface\n\n| Surface | Shell | Standard | Contract |\n| ---------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------- |\n| A signed-in app, console or admin surface | `@bundu/ui/app/AppShell.astro` | Mzizi Dashboard Standard (docs.mzizi.dev/patterns/dashboard-standard) | `contracts/app/app-shell` |\n| A public discover or browse site (the Mukoko family) | `@bundu/ui/discover/DiscoverShell.astro` | Mzizi Discover Standard (docs.mzizi.dev/patterns/discover-standard) | `contracts/discover/discover-shell` |\n| A marketing site | none yet: see \"Gaps\" below | the `site/` components (`Hero`, `Section`, `Container`, `MineralStrip`, …) | no shell contract |\n\nRead the shell's contract before you wire it. The props, slots, `noJs`, `density` and `theming`\nthere are the specification. On the MCP that is `mzizi_get_component app-shell`, and in the\npackage, `@bundu/ui/contracts/app/*`.\n\n### 2. Install it (Astro, the default)\n\n```bash\nnpm install @bundu/ui # 0.7.0 or later\n```\n\nImport the tokens and your brand's overlay (colour and mark only) in your global stylesheet, as\nthe `mzizi-design` skill says. Then describe the navigation as data and wrap **every**\nsigned-in page in one layout that mounts `AppShell`:\n\n```astro\n---\n// src/layouts/App.astro: the one layout every signed-in page uses\nimport AppShell from \"@bundu/ui/app/AppShell.astro\";\nimport { nav } from \"../nav\"; // navigation as data, not markup\nconst { title } = Astro.props;\n---\n<AppShell nav={nav} accent=\"gold\" footerLinks={[{ label: \"Status\", href: \"/status\" }]}>\n <slot />\n</AppShell>\n```\n\nA public discover page puts `DiscoverMeta` in `<head>` and `DiscoverShell` in `<body>`, then\n`DiscoverHero`. The Discover Standard page has the full order.\n\nThen build pages from the patterns, never from scratch. For a dashboard, that means\n`PageHeader` first, then `Toolbar` or `FilterBar`, then `StatTiles`, then content in `Card`,\n`DataTable`, `FormLayout`, `EmptyState` or `DetailPanel`. For discover pages, that means\n`DiscoverSection`, `ResultGrid`, `DetailHero` and `RelatedRail`.\n\n### 3. Check that the shell is there\n\nRun this before you add a page, and in review. An app passes when each line holds:\n\n```bash\n# The shell is a dependency.\ngrep -E '\"@bundu/ui\"' package.json\n# Exactly one layout mounts it.\ngrep -rlE 'AppShell|DiscoverShell' src/layouts\n# Every page uses that layout. List the pages that do not:\ngrep -rL -E 'layouts/(App|Discover)' src/pages --include='*.astro'\n# No page hand-rolls chrome the shell owns.\ngrep -rnE '<(header|nav|footer)[ >]' src/pages\n```\n\nAny page the third command prints, and any match from the fourth, is a page outside the shell.\nMove it into the layout. A shell that is missing entirely means step 2 was skipped, so install\nthe shell before anything else.\n\n### 4. Structure the app's own code: five layers, imports flow down\n\nThese layers are local to one application (docs.mzizi.dev/patterns/architecture). They are not\nthe helix:\n\n| Layer | What it holds |\n| ----- | ------------------------------------------------------------------------------------------------- |\n| L1 | Shared primitives, installed from the registry and never edited in place |\n| L2 | Domain composites built only from L1, the first layer that knows what the app is about |\n| L3 | Page sections that arrange L2. They arrange and do not implement |\n| L4 | Error boundaries and loading states around every L3 section, so one failure never blanks the page |\n| L5 | Routes: data fetching and composition, inside the shell. This is where the network lives |\n\nA component imports from the layer below, never sideways or upward.\n\n### 5. A React or Rust app\n\n- **React (Next.js) adopts through the registry's `.tsx` builds.** AppShell and DiscoverShell\n have `.tsx` siblings that implement the same contract:\n `npx shadcn@latest add https://api.mzizi.dev/v1/ui/app-shell`. Always give the full URL. A bare\n name installs shadcn's own component and reports success.\n- **Rust.** `mzizi-shell` (in `mzizi-roots`) carries N7 parts such as the bottom nav, footer,\n command palette, connectivity bar, theme provider, route guard and deep links. **It has no\n AppShell or DiscoverShell.** Both contracts have `rs: null`. A Rust or Astro-with-Roots app\n takes the shell from `@bundu/ui` and renders Roots components inside it. Do not write a Rust\n shell in an app. See the gaps below.\n\n### Gaps: report them upstream, do not paper over them\n\nThese were found in the registry at v4.18.0 and are tracked in mzizi-registry#589:\n\n- AppShell and DiscoverShell have no Rust build (`rs: null`), and `mzizi-shell` documents that\n `mzizi-header` and `mzizi-sidebar` wait on N2 ports.\n- No shell contract exists for marketing sites (`site/` has no `site-shell`). The N6 doctrine\n names a \"Site Standard\", but docs.mzizi.dev has no page for it.\n\nWhen you hit one of these, open or comment on the registry issue instead of building a local\nshell.\n\n## System architecture\n\nWhat runs where, and who owns what. **Decided** and **built** are different words. Say which one\napplies.\n\n- **Rust first, Astro for the UI.** Astro is the default web UI. Underneath, the code is Rust\n first and TypeScript second, and Mzizi's own surfaces have no third UI framework. Components\n are Mzizi Roots (Rust) where a Rust build exists and the React build otherwise, with React\n deprioritised. No Dioxus, Leptos or Yew in new work. The Roots crates still depend on Dioxus\n until the move to Mzizi's own `render(props)` finishes.\n- **Cloudflare Workers** run the services: `api.mzizi.dev` (`mzizi-dev/mzizi-api`, a Hono\n Worker), `mcp.mzizi.dev` (`mzizi-mcp`) and fundi. Containers and bare metal are decided as\n further targets and are not built. The `mzizi-backend` skill has the patterns.\n- **Four API gateways.** The Mzizi API (`api.mzizi.dev`, Hono, built), the Nyuchi API (B2B), the\n Mukoko API (consumer) and the Nyuchi Servers API (internal). It is decided that the last three\n are Rust. An app reads through its gateway and never opens a database connection of its own\n for a read the gateway serves. Add the endpoint to the gateway instead.\n- **The registry's files are its data layer.** `registry.json`, `components/`, `contracts/` and\n `content/doctrine/` are bundled into the API and the MCP at a pinned registry commit (the\n `x-mzizi-source` header and `catalogue.json` name it). Nothing is read from a database at\n request time. Only the console keeps state, and its move to Cloudflare D1 is decided, not\n built.\n- **One owner per domain.** Platform data lives in domain-separated databases (news,\n engagement, identity, entity, places, …). Each is written only by the service that owns it,\n and other services reference its records by id rather than copying them. Doctrine is data:\n read `mzizi_get_doctrine`, never a copy in a prompt.\n- **Trust is two things.** Assurance levels and attestations come from the one access decision\n in nyuchi-api `gateway/lib/assurance.py`. Read that decision rather than deriving your own,\n and draw them with the `--assurance-*` and `--attestation-*` roles in `mzizi-design`.\n- **Every surface is free by default.** Gate only what acts as a person: WorkOS sign-in, with\n 401 for a missing token and 403 for a token that lacks the scope.\n- **Failures become issues.** N8 signals feed fundi (N9), which files deduplicated GitHub\n issues for people and never merges a fix itself.\n\n## Checklist for a new app or page\n\n- [ ] Helix placement decided, and its node's rules read live.\n- [ ] Shell chosen from the surface table, its contract read, and `@bundu/ui` installed.\n- [ ] One layout mounts the shell, and every page uses it (the check in step 3 prints nothing).\n- [ ] Pages built from the Standard's patterns, with no hand-rolled header, nav or footer.\n- [ ] Tokens and brand overlay from `mzizi-design`. No local colour, radius, spacing or type. Nothing under the spacing floor (14px inset, 7px gaps between controls, labels and items, 1.5 leading); `mzizi check` passes.\n- [ ] Data read through the owning gateway. No new direct database read.\n- [ ] Anything missing or wrong in the registry raised upstream, with any local stopgap marked\n `TODO(mzizi): <PR URL>`.\n\n## See also\n\n- `mzizi-design`: tokens, brand overlays, Apple HIG and where Mzizi overrides it, and the Bundu\n floor (17px body, 14px captions, a 49px hit region, and the spacing floor: 14px inset, 7px gaps, 1.5 leading) with the desktop density layer.\n- `mzizi-roots`: the components, Rust first. \"Starting an app\" covers project setup, and\n \"Contributing a component\" covers adding one upstream.\n- `mzizi-backend`: Workers and MCP servers.\n- `discoverability`: N11 in practice, covering OG images, structured data and agent-readiness.",
212
+ "body_mdx": "# Mzizi architecture: design, app and system\n\nThree questions, in this order, before anything is built:\n\n1. **Design architecture.** Where on the helix does this belong, and what does that placement\n oblige it to do?\n2. **App architecture.** Which shell frames it? The shell is installed **first**, before the\n first page.\n3. **System architecture.** Which service owns the data, which gateway serves it, and what runs\n where?\n\nThe rule behind all three is **upstream first**. If the registry lacks a component, a shell or a\npattern that you need, or has one that is wrong, change it in `mzizi-dev/mzizi-registry`, its\ncontract in `contracts/` and its build in `@bundu/ui`. Keep a local copy only while that pull\nrequest is open, marked `TODO(mzizi): <PR URL>`. Never fork the shell or restyle it locally.\n\nThis skill describes registry `main` at v4.18.0 (`0d17818`, 2026-11-01), the Roots crates at\n0.7.0 and `@bundu/ui` 0.7.0. Counts move, so read the live sources below rather than this text.\n\n## Design architecture: the DNA helix\n\nThe design system is a double helix. Read it live, not from memory:\n\n```bash\ncurl -s https://api.mzizi.dev/v1/architecture # nodes, rungs, strands\ncurl -s https://api.mzizi.dev/v1/architecture/nodes/7 # one node, with its rules\n```\n\nOn the MCP, call `mzizi_get_architecture`, or `mzizi_get_architecture node=7` for one node. The\nnode number is uncapped, so never assume a range.\n\n**Eight nodes.** These are the backbone, and each owns one kind of thing:\n\n| Node | Name | Owns | Strand |\n| ---- | ---------------- | --------------------------------------------------------------- | -------------- |\n| N1 | Design tokens | the only node allowed to define CSS values | swappable |\n| N2 | Primitives | the smallest composable unit of UI | core-guarantee |\n| N3 | Brand components | primitives wrapped in Ubuntu, the only consumers of the harness | shipped |\n| N4 | Safety rails | input validation, output sanitisation, AI guardrails | core-guarantee |\n| N5 | Resilience | circuit breakers, retries, timeouts, fallback chains | core-guarantee |\n| N6 | Pages | composition of N2 and N3, implementing nothing | shipped |\n| N7 | Shell | the app chrome: header, footer, navigation, theme, mounted once | shipped |\n| N8 | Assurance | observability, health monitoring, SLO tracking | core-guarantee |\n\n**Four rungs.** These bridge both backbones: N9 Fundi (self-healing: it consumes N8 signals and\nfiles structured issues for people, and never auto-merges), N10 Documentation, N11 Discovery\n(SEO and AIO, preview images, structured data, agent-readiness) and N12 Skills (this bundle).\n\n**Six strands.** Four belong to the engineering backbone: `core-guarantee` (travels unchanged\nand cannot be turned off), `shipped` (in the box, but free to evolve), `swappable` (the only fork\nseams: tokens, icons and the framework) and `spine`. Two belong to the meaning backbone:\n`genetic-code` and `transcription` (\"Doctrine is data, not tribal knowledge\").\n\nWhat a placement obliges, from the nodes' own implementation rules:\n\n- **N1 only defines values.** No hex, `rgba()` or inline style outside N1. Everything else reads\n `var(--…)` or its platform equivalent.\n- **N6 composes and implements nothing.** A page imports from N2 and N3, never defines a\n primitive, and pulls a spinner or skeleton from N2 rather than inlining one. Pages render on\n the server by default (Astro, no client JavaScript).\n- **N7 is singular.** There is one shell per app. It is the only place that holds sticky\n positioning, backdrop blur and theme-provider wiring. It surfaces auth state and never\n implements auth logic. It is responsive from 320px up.\n- **N4, N5 and N8 are core guarantees.** You cannot configure accessibility, safety,\n resilience or observability away.\n\nDo not confuse the helix with the five application layers below. They are two models with two\nnumberings, so never quote a count from one while describing the other.\n\n## App architecture: the shell comes first\n\nThe failure this section exists to stop is apps built page by page with no shell installed. Each\none then hand-rolls its own header, nav and footer, drifts from every other app, and loses the\nno-JS, density, accessibility and brand-overlay guarantees the shell's contract carries.\n**Step one of any new app is the shell.** Step one of any new page in an existing app is to\nconfirm the page renders inside it.\n\n### 1. Pick the shell from the kind of surface\n\n| Surface | Shell | Standard | Contract |\n| ---------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------- |\n| A signed-in app, console or admin surface | `@bundu/ui/app/AppShell.astro` | Mzizi Dashboard Standard (docs.mzizi.dev/patterns/dashboard-standard) | `contracts/app/app-shell` |\n| A public discover or browse site (the Mukoko family) | `@bundu/ui/discover/DiscoverShell.astro` | Mzizi Discover Standard (docs.mzizi.dev/patterns/discover-standard) | `contracts/discover/discover-shell` |\n| A marketing site | none yet: see \"Gaps\" below | the `site/` components (`Hero`, `Section`, `Container`, `MineralStrip`, …) | no shell contract |\n\nRead the shell's contract before you wire it. The props, slots, `noJs`, `density` and `theming`\nthere are the specification. On the MCP that is `mzizi_get_component app-shell`, and in the\npackage, `@bundu/ui/contracts/app/*`.\n\n### 2. Install it (Astro, the default)\n\n```bash\nnpm install @bundu/ui # 0.7.0 or later\n```\n\nImport the tokens and your brand's overlay (colour and mark only) in your global stylesheet, as\nthe `mzizi-design` skill says. Then describe the navigation as data and wrap **every**\nsigned-in page in one layout that mounts `AppShell`:\n\n```astro\n---\n// src/layouts/App.astro: the one layout every signed-in page uses\nimport AppShell from \"@bundu/ui/app/AppShell.astro\";\nimport { nav } from \"../nav\"; // navigation as data, not markup\nconst { title } = Astro.props;\n---\n<AppShell nav={nav} accent=\"gold\" footerLinks={[{ label: \"Status\", href: \"/status\" }]}>\n <slot />\n</AppShell>\n```\n\nA public discover page puts `DiscoverMeta` in `<head>` and `DiscoverShell` in `<body>`, then\n`DiscoverHero`. The Discover Standard page has the full order.\n\nThen build pages from the patterns, never from scratch. For a dashboard, that means\n`PageHeader` first, then `Toolbar` or `FilterBar`, then `StatTiles`, then content in `Card`,\n`DataTable`, `FormLayout`, `EmptyState` or `DetailPanel`. For discover pages, that means\n`DiscoverSection`, `ResultGrid`, `DetailHero` and `RelatedRail`.\n\n### 3. Check that the shell is there\n\nRun this before you add a page, and in review. An app passes when each line holds:\n\n```bash\n# The shell is a dependency.\ngrep -E '\"@bundu/ui\"' package.json\n# Exactly one layout mounts it.\ngrep -rlE 'AppShell|DiscoverShell' src/layouts\n# Every page uses that layout. List the pages that do not:\ngrep -rL -E 'layouts/(App|Discover)' src/pages --include='*.astro'\n# No page hand-rolls chrome the shell owns.\ngrep -rnE '<(header|nav|footer)[ >]' src/pages\n```\n\nAny page the third command prints, and any match from the fourth, is a page outside the shell.\nMove it into the layout. A shell that is missing entirely means step 2 was skipped, so install\nthe shell before anything else.\n\n### 4. Structure the app's own code: five layers, imports flow down\n\nThese layers are local to one application (docs.mzizi.dev/patterns/architecture). They are not\nthe helix:\n\n| Layer | What it holds |\n| ----- | ------------------------------------------------------------------------------------------------- |\n| L1 | Shared primitives, installed from the registry and never edited in place |\n| L2 | Domain composites built only from L1, the first layer that knows what the app is about |\n| L3 | Page sections that arrange L2. They arrange and do not implement |\n| L4 | Error boundaries and loading states around every L3 section, so one failure never blanks the page |\n| L5 | Routes: data fetching and composition, inside the shell. This is where the network lives |\n\nA component imports from the layer below, never sideways or upward.\n\n### 5. A React or Rust app\n\n- **React (Next.js) adopts through the registry's `.tsx` builds.** AppShell and DiscoverShell\n have `.tsx` siblings that implement the same contract:\n `npx shadcn@latest add https://api.mzizi.dev/v1/ui/app-shell`. Always give the full URL. A bare\n name installs shadcn's own component and reports success.\n- **Rust.** `mzizi-shell` (in `mzizi-roots`) carries N7 parts such as the bottom nav, footer,\n command palette, connectivity bar, theme provider, route guard and deep links. **It has no\n AppShell or DiscoverShell.** Both contracts have `rs: null`. A Rust or Astro-with-Roots app\n takes the shell from `@bundu/ui` and renders Roots components inside it. Do not write a Rust\n shell in an app. See the gaps below.\n\n### Gaps: report them upstream, do not paper over them\n\nThese were found in the registry at v4.18.0 and are tracked in mzizi-registry#589:\n\n- AppShell and DiscoverShell have no Rust build (`rs: null`), and `mzizi-shell` documents that\n `mzizi-header` and `mzizi-sidebar` wait on N2 ports.\n- No shell contract exists for marketing sites (`site/` has no `site-shell`). The N6 doctrine\n names a \"Site Standard\", but docs.mzizi.dev has no page for it.\n\nWhen you hit one of these, open or comment on the registry issue instead of building a local\nshell.\n\n## System architecture\n\nWhat runs where, and who owns what. **Decided** and **built** are different words. Say which one\napplies.\n\n- **Rust first, Astro for the UI.** Astro is the default web UI. Underneath, the code is Rust\n first and TypeScript second, and Mzizi's own surfaces have no third UI framework. Components\n are Mzizi Roots (Rust) where a Rust build exists and the React build otherwise, with React\n deprioritised. No third-party UI framework in new work. The one the Roots UI crates were first built on was removed across every Mzizi\n repository on 2026-11-04 (mzizi-registry#635); the Roots rendering target is the registry's decision.\n- **Cloudflare Workers** run the services: `api.mzizi.dev` (`mzizi-dev/mzizi-api`, a Hono\n Worker), `mcp.mzizi.dev` (`mzizi-mcp`) and fundi. Containers and bare metal are decided as\n further targets and are not built. The `mzizi-backend` skill has the patterns.\n- **Four API gateways.** The Mzizi API (`api.mzizi.dev`, Hono, built), the Nyuchi API (B2B), the\n Mukoko API (consumer) and the Nyuchi Servers API (internal). It is decided that the last three\n are Rust. An app reads through its gateway and never opens a database connection of its own\n for a read the gateway serves. Add the endpoint to the gateway instead.\n- **The registry's files are its data layer.** `registry.json`, `components/`, `contracts/` and\n `content/doctrine/` are bundled into the API and the MCP at a pinned registry commit (the\n `x-mzizi-source` header and `catalogue.json` name it). Nothing is read from a database at\n request time. Only the console keeps state, and its move to Cloudflare D1 is decided, not\n built.\n- **One owner per domain.** Platform data lives in domain-separated databases (news,\n engagement, identity, entity, places, …). Each is written only by the service that owns it,\n and other services reference its records by id rather than copying them. Doctrine is data:\n read `mzizi_get_doctrine`, never a copy in a prompt.\n- **Trust is two things.** Assurance levels and attestations come from the one access decision\n in nyuchi-api `gateway/lib/assurance.py`. Read that decision rather than deriving your own,\n and draw them with the `--assurance-*` and `--attestation-*` roles in `mzizi-design`.\n- **Every surface is free by default.** Gate only what acts as a person: WorkOS sign-in, with\n 401 for a missing token and 403 for a token that lacks the scope.\n- **Failures become issues.** N8 signals feed fundi (N9), which files deduplicated GitHub\n issues for people and never merges a fix itself.\n\n## Checklist for a new app or page\n\n- [ ] Helix placement decided, and its node's rules read live.\n- [ ] Shell chosen from the surface table, its contract read, and `@bundu/ui` installed.\n- [ ] One layout mounts the shell, and every page uses it (the check in step 3 prints nothing).\n- [ ] Pages built from the Standard's patterns, with no hand-rolled header, nav or footer.\n- [ ] Tokens and brand overlay from `mzizi-design`. No local colour, radius, spacing or type. Nothing under the spacing floor (14px inset, 7px gaps between controls, labels and items, 1.5 leading); `mzizi check` passes.\n- [ ] Data read through the owning gateway. No new direct database read.\n- [ ] Anything missing or wrong in the registry raised upstream, with any local stopgap marked\n `TODO(mzizi): <PR URL>`.\n\n## See also\n\n- `mzizi-design`: tokens, brand overlays, Apple HIG and where Mzizi overrides it, and the Bundu\n floor (17px body, 14px captions, a 49px hit region, and the spacing floor: 14px inset, 7px gaps, 1.5 leading) with the desktop density layer.\n- `mzizi-roots`: the components, Rust first. \"Starting an app\" covers project setup, and\n \"Contributing a component\" covers adding one upstream.\n- `mzizi-backend`: Workers and MCP servers.\n- `discoverability`: N11 in practice, covering OG images, structured data and agent-readiness.",
213
213
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/mzizi-architecture",
214
214
  "files": []
215
215
  },
@@ -217,7 +217,7 @@ export const BUNDLED_SKILLS = [
217
217
  "name": "discoverability",
218
218
  "category": "build",
219
219
  "description": "SEO + AIO for the Mzizi ecosystem: how to guarantee machines can find and render a site — social/messaging link previews (OpenGraph), search engines, and AI agents. Covers the 'if the machine can't see it, it doesn't exist' convention, the brand-agnostic Mzizi hive OG-image template (dynamic per page: title + description + site icon), build-time OG images on static Astro (the standard), the rules for a surface already on Next.js (next 16.3.6 or later, no request input in an ImageResponse), APCA-checked text colours, and a pre-ship checklist. Use when a shared link shows no preview or when adding a new route/site.",
220
- "body_mdx": "# Discoverability — SEO + AIO for the Mzizi ecosystem\n\n> **Covenant:** _If the machine can't see it, it doesn't exist._\n\nDiscoverability is the machine-facing surface of every Mzizi-ecosystem site — the\npart crawlers, social/messaging scrapers, and AI agents read. It is **cross-cutting**,\nnot a component you install: it is a genetic-code **convention** (\"every surface ships\na title, a description, a dynamic preview image, structured data, and an\nagent-readable index\") base-paired to the components that satisfy it (the OG template,\n`llms.txt`, JSON-LD helpers). A surface a machine can't parse may as well not exist —\nand a **missing social preview is the single biggest discoverability no-go**, because\nit's the first thing a human sees when a link is shared.\n\nAll values here are the current doctrine. The source of truth is the repo:\n`mzizi-dev/mzizi-registry` for doctrine and tokens, this package for the skills, served\nread-only at <https://api.mzizi.dev/v1> and by the MCP at `https://mcp.mzizi.dev/mcp`. There is\nno database behind any of it. Brand colour specifically is `palette.canonical.json` in this\npackage: 21 families, generated and gated, never retyped.\n\n## The number-one failure: a missing OG image\n\nThe most common — and most damaging — bug is a link that unfurls with **no preview\nimage at all**. The canonical cause is metadata that points at an image file that\ndoesn't exist:\n\n```ts\n// ❌ WRONG — points at a static file that isn't in /public, so every scraper 404s\nopenGraph: { images: [\"/og-image.png\"] }\ntwitter: { images: [\"/og-image.png\"] }\n```\n\nWorse, an explicit `images` array **overrides** a framework's dynamic OG route, so\nthe good image never gets a chance. **Rule: never hardcode `openGraph.images`.** Let\nthe framework's file-convention / endpoint serve a _generated_ image, and set an\nabsolute base URL so scrapers get an absolute URL.\n\n## The Mzizi \"hive\" OG card\n\nOne template, every route, every brand. The card is:\n\n- **Deep-night ground** (`#1B1A17`) with a faint flat-top **honeycomb hex lattice**\n (white at ~5% — texture, not noise) and a few solid **mineral accent cells**\n (cobalt / gold lead, plus tanzanite / malachite / copper for warmth).\n- The **site icon on top, above the title** (brand recognition first), then the\n title, an optional eyebrow, the SEO description, and the domain.\n- **Left-aligned** (reads better than centred at thumbnail size).\n- **Brand-agnostic:** the icon is a parameter, so the same template serves nyuchi\n (the bee), mukoko (the swarm), mzizi, shamwari — each with its own mark + domain.\n It picks up the _site's own icon_, never a hardcoded one.\n\n### Colours are checked twice: WCAG ratio and APCA\n\nOG text is read at thumbnail size, so contrast matters. The design system's floors are WCAG 2\n**7:1 for body text and 4.5:1 for non-body text** (owner, 2026-11-02; the Bundu floor), and\nthe APCA bars beside them (**Lc 75 large / Lc 90 body**). Verified against the `#1B1A17`\nground (APCA reverse polarity):\n\n| Role | Hex | WCAG ratio | APCA Lc | Verdict |\n| -------------------------------- | --------- | ---------- | ------- | ------- |\n| Title | `#F5F5F4` | 15.95:1 | 92 | ✓ body |\n| Secondary (eyebrow, description) | `#E5E3DE` | 13.57:1 | 81 | ✓ large |\n| Domain (gold) | `#FFD740` | 12.47:1 | 76 | ✓ large |\n\nDo **not** use dim greys like `#B2AFA8` (only Lc 50, though it measures 7.95:1 and passes WCAG 7:1, so\nthe ratio alone is not enough) for OG text: it looks fine on a monitor and vanishes in a\nmessaging thumbnail. Re-run both checks when changing any pair\n(`mzizi_check_accessibility` gives the WCAG ratio; APCA needs its own tool).\n\nThe Bundu spacing floor (inset, gaps and leading minimums; the `mzizi-design` skill has the\nnumbers) applies to the pages you build around these tags, not to the image.\n\n## Implementation — Astro, static (the standard)\n\nNew surfaces are static Astro: mzizi.dev (`mzizi-dev/mzizi-site`) is one. A statically built\nsite generates its OG images **at build time**:\n\n1. A build-time image endpoint, `src/pages/og/[...slug].png.ts`, with `getStaticPaths()` over\n the site's pages or content collection. Render each page's title and description with\n **satori** to SVG, then rasterise with **sharp** to PNG.\n2. In the shared layout's `<head>`, set\n `<meta property=\"og:image\" content={new URL(`/og/${slug}.png`, Astro.site)} />` per page,\n plus `twitter:card` = `summary_large_image`. Set `site` in `astro.config.mjs` so every URL is\n absolute.\n3. Use the hive design: honeycomb, the site's icon, and the brand's colour (for mzizi.dev,\n hematite, a Heritage tone; see the constellation in `mzizi-design`).\n\nKeep the site's runtime dependency-light; satori and sharp are build-only. If the site renders\nMzizi Roots components, they arrive as static HTML from `dioxus-ssr` and change nothing here.\n\n**Docs on Mintlify** (docs.mzizi.dev) get their preview images from Mintlify. Check them with a\nscraper like any other surface; do not hand-wire `og:image` there.\n\n## A surface already on Next.js (App Router)\n\nDo not start a new surface on Next.js; the standard is the static Astro build above. There is\nno Next.js reference template to copy any more: `mzizi-registry` is not a Next.js app, and its\n`next/og` template (`lib/og/`) and OG routes were removed on 2026-10-02 over a critical RCE\nexposure (GHSA-vcvr-r3jv-pc5j, `next` >= 16.2.0 < 16.3.6): a route put the URL slug into a\nNode `ImageResponse`. Build the hive card from the design above. For a surface that is already\non Next.js:\n\n- **Require `next` >= 16.3.6** (the patched line). Check the lockfile, not only the range.\n- **Never put request input into an `ImageResponse`.** No route param, slug, search param,\n header or cookie goes into the image, as text or as a path. Look the slug up in the site's\n own content and render only that entry's own title and description; an unknown slug is a\n 404 (`notFound()`), never a fallback that echoes it.\n- **Generate the images at build time.** Give each dynamic segment's `opengraph-image.tsx`\n `generateStaticParams()` over the site's own content and `export const dynamicParams = false`,\n so no OG image renders per request and an unknown path is a 404.\n- **Set `metadataBase` and never `openGraph.images` / `twitter.images`** in the layout's\n `metadata`. The `opengraph-image.tsx` file convention supplies an absolute, hashed URL, and\n the Twitter card falls back to it.\n\n## Beyond the image — the rest of the convention\n\n- **Title + meta description** on every route (unique, page-specific).\n- **Structured data**: schema.org JSON-LD for the page's primary entity.\n- **`llms.txt`** at the site root — the machine-readable index for LLMs.\n- **`robots.txt` + `sitemap.xml`** — generated, not hand-maintained.\n- **Canonical URLs** absolute (a set `metadataBase` / `site`).\n- **Agent-readiness**: an MCP discovery card at `/.well-known/mcp.json` pointing at the MCP\n endpoint (mzizi.dev ships one for `https://mcp.mzizi.dev/mcp`).\n\n## Pre-ship checklist\n\n- [ ] Sharing the URL in Slack / WhatsApp / iMessage / X renders a preview **image**.\n- [ ] The OG image is **generated** (dynamic), carries the page's title + description + the site icon, and its URL is **absolute**.\n- [ ] No hardcoded `openGraph.images` / `twitter.images`.\n- [ ] OG text colours pass WCAG (≥ 7:1 body, ≥ 4.5:1 non-body) and APCA (Lc ≥ 75 large / ≥ 90 body).\n- [ ] `metadataBase` / `site` is set; canonical URL is absolute.\n- [ ] Title + meta description are unique per route.\n- [ ] `llms.txt`, `robots.txt`, `sitemap.xml` present; JSON-LD on primary entities.\n- [ ] Validate the card in a scraper (e.g. opengraph.xyz) after deploy.\n\n_The whole helix is Mzizi — and discoverability is the convention that makes each\nsurface legible to the machines that carry it. Ndiri nekuti tiri._",
220
+ "body_mdx": "# Discoverability — SEO + AIO for the Mzizi ecosystem\n\n> **Covenant:** _If the machine can't see it, it doesn't exist._\n\nDiscoverability is the machine-facing surface of every Mzizi-ecosystem site — the\npart crawlers, social/messaging scrapers, and AI agents read. It is **cross-cutting**,\nnot a component you install: it is a genetic-code **convention** (\"every surface ships\na title, a description, a dynamic preview image, structured data, and an\nagent-readable index\") base-paired to the components that satisfy it (the OG template,\n`llms.txt`, JSON-LD helpers). A surface a machine can't parse may as well not exist —\nand a **missing social preview is the single biggest discoverability no-go**, because\nit's the first thing a human sees when a link is shared.\n\nAll values here are the current doctrine. The source of truth is the repo:\n`mzizi-dev/mzizi-registry` for doctrine and tokens, this package for the skills, served\nread-only at <https://api.mzizi.dev/v1> and by the MCP at `https://mcp.mzizi.dev/mcp`. There is\nno database behind any of it. Brand colour specifically is `palette.canonical.json` in this\npackage: 21 families, generated and gated, never retyped.\n\n## The number-one failure: a missing OG image\n\nThe most common — and most damaging — bug is a link that unfurls with **no preview\nimage at all**. The canonical cause is metadata that points at an image file that\ndoesn't exist:\n\n```ts\n// ❌ WRONG — points at a static file that isn't in /public, so every scraper 404s\nopenGraph: { images: [\"/og-image.png\"] }\ntwitter: { images: [\"/og-image.png\"] }\n```\n\nWorse, an explicit `images` array **overrides** a framework's dynamic OG route, so\nthe good image never gets a chance. **Rule: never hardcode `openGraph.images`.** Let\nthe framework's file-convention / endpoint serve a _generated_ image, and set an\nabsolute base URL so scrapers get an absolute URL.\n\n## The Mzizi \"hive\" OG card\n\nOne template, every route, every brand. The card is:\n\n- **Deep-night ground** (`#1B1A17`) with a faint flat-top **honeycomb hex lattice**\n (white at ~5% — texture, not noise) and a few solid **mineral accent cells**\n (cobalt / gold lead, plus tanzanite / malachite / copper for warmth).\n- The **site icon on top, above the title** (brand recognition first), then the\n title, an optional eyebrow, the SEO description, and the domain.\n- **Left-aligned** (reads better than centred at thumbnail size).\n- **Brand-agnostic:** the icon is a parameter, so the same template serves nyuchi\n (the bee), mukoko (the swarm), mzizi, shamwari — each with its own mark + domain.\n It picks up the _site's own icon_, never a hardcoded one.\n\n### Colours are checked twice: WCAG ratio and APCA\n\nOG text is read at thumbnail size, so contrast matters. The design system's floors are WCAG 2\n**7:1 for body text and 4.5:1 for non-body text** (owner, 2026-11-02; the Bundu floor), and\nthe APCA bars beside them (**Lc 75 large / Lc 90 body**). Verified against the `#1B1A17`\nground (APCA reverse polarity):\n\n| Role | Hex | WCAG ratio | APCA Lc | Verdict |\n| -------------------------------- | --------- | ---------- | ------- | ------- |\n| Title | `#F5F5F4` | 15.95:1 | 92 | ✓ body |\n| Secondary (eyebrow, description) | `#E5E3DE` | 13.57:1 | 81 | ✓ large |\n| Domain (gold) | `#FFD740` | 12.47:1 | 76 | ✓ large |\n\nDo **not** use dim greys like `#B2AFA8` (only Lc 50, though it measures 7.95:1 and passes WCAG 7:1, so\nthe ratio alone is not enough) for OG text: it looks fine on a monitor and vanishes in a\nmessaging thumbnail. Re-run both checks when changing any pair\n(`mzizi_check_accessibility` gives the WCAG ratio; APCA needs its own tool).\n\nThe Bundu spacing floor (inset, gaps and leading minimums; the `mzizi-design` skill has the\nnumbers) applies to the pages you build around these tags, not to the image.\n\n## Implementation — Astro, static (the standard)\n\nNew surfaces are static Astro: mzizi.dev (`mzizi-dev/mzizi-site`) is one. A statically built\nsite generates its OG images **at build time**:\n\n1. A build-time image endpoint, `src/pages/og/[...slug].png.ts`, with `getStaticPaths()` over\n the site's pages or content collection. Render each page's title and description with\n **satori** to SVG, then rasterise with **sharp** to PNG.\n2. In the shared layout's `<head>`, set\n `<meta property=\"og:image\" content={new URL(`/og/${slug}.png`, Astro.site)} />` per page,\n plus `twitter:card` = `summary_large_image`. Set `site` in `astro.config.mjs` so every URL is\n absolute.\n3. Use the hive design: honeycomb, the site's icon, and the brand's colour (for mzizi.dev,\n hematite, a Heritage tone; see the constellation in `mzizi-design`).\n\nKeep the site's runtime dependency-light; satori and sharp are build-only. If the site renders\nMzizi Roots components, they arrive as static HTML and change nothing here.\n\n**Docs on Mintlify** (docs.mzizi.dev) get their preview images from Mintlify. Check them with a\nscraper like any other surface; do not hand-wire `og:image` there.\n\n## A surface already on Next.js (App Router)\n\nDo not start a new surface on Next.js; the standard is the static Astro build above. There is\nno Next.js reference template to copy any more: `mzizi-registry` is not a Next.js app, and its\n`next/og` template (`lib/og/`) and OG routes were removed on 2026-10-02 over a critical RCE\nexposure (GHSA-vcvr-r3jv-pc5j, `next` >= 16.2.0 < 16.3.6): a route put the URL slug into a\nNode `ImageResponse`. Build the hive card from the design above. For a surface that is already\non Next.js:\n\n- **Require `next` >= 16.3.6** (the patched line). Check the lockfile, not only the range.\n- **Never put request input into an `ImageResponse`.** No route param, slug, search param,\n header or cookie goes into the image, as text or as a path. Look the slug up in the site's\n own content and render only that entry's own title and description; an unknown slug is a\n 404 (`notFound()`), never a fallback that echoes it.\n- **Generate the images at build time.** Give each dynamic segment's `opengraph-image.tsx`\n `generateStaticParams()` over the site's own content and `export const dynamicParams = false`,\n so no OG image renders per request and an unknown path is a 404.\n- **Set `metadataBase` and never `openGraph.images` / `twitter.images`** in the layout's\n `metadata`. The `opengraph-image.tsx` file convention supplies an absolute, hashed URL, and\n the Twitter card falls back to it.\n\n## Beyond the image — the rest of the convention\n\n- **Title + meta description** on every route (unique, page-specific).\n- **Structured data**: schema.org JSON-LD for the page's primary entity.\n- **`llms.txt`** at the site root — the machine-readable index for LLMs.\n- **`robots.txt` + `sitemap.xml`** — generated, not hand-maintained.\n- **Canonical URLs** absolute (a set `metadataBase` / `site`).\n- **Agent-readiness**: an MCP discovery card at `/.well-known/mcp.json` pointing at the MCP\n endpoint (mzizi.dev ships one for `https://mcp.mzizi.dev/mcp`).\n\n## Pre-ship checklist\n\n- [ ] Sharing the URL in Slack / WhatsApp / iMessage / X renders a preview **image**.\n- [ ] The OG image is **generated** (dynamic), carries the page's title + description + the site icon, and its URL is **absolute**.\n- [ ] No hardcoded `openGraph.images` / `twitter.images`.\n- [ ] OG text colours pass WCAG (≥ 7:1 body, ≥ 4.5:1 non-body) and APCA (Lc ≥ 75 large / ≥ 90 body).\n- [ ] `metadataBase` / `site` is set; canonical URL is absolute.\n- [ ] Title + meta description are unique per route.\n- [ ] `llms.txt`, `robots.txt`, `sitemap.xml` present; JSON-LD on primary entities.\n- [ ] Validate the card in a scraper (e.g. opengraph.xyz) after deploy.\n\n_The whole helix is Mzizi — and discoverability is the convention that makes each\nsurface legible to the machines that carry it. Ndiri nekuti tiri._",
221
221
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/discoverability",
222
222
  "files": []
223
223
  },
@@ -225,7 +225,7 @@ export const BUNDLED_SKILLS = [
225
225
  "name": "digital-hygiene",
226
226
  "category": "dev",
227
227
  "description": "Use this skill whenever an agent clones a repository, adds a git worktree, fans out sub-agents, builds or tests (pnpm, npm, vitest, cargo, Astro, wrangler), reviews a PR, or finishes a task. It keeps agent hosts and cloud sessions from filling their disks: check free disk with `df -h /` before starting, refuse to fan out below 50 GB on a shared host, and in a Claude Code cloud session treat df's Avail as the fixed allowance. Work only in a folder unique to you (a clone or git worktree with an owner marker), never touch a folder you did not create, share build caches, never commit build output. On close, audit uncommitted changes, stashes and local branches with no commit on any remote (a squash merge leaves commits local-only), then delete only your own clone. Ships hygiene.sh; every deleting command is a dry run unless given --apply. Never rm -rf an unchecked variable, never touch ~/GitHub, claude-* temp, browser profiles or another agent's folders, never delete staging.",
228
- "body_mdx": "# Digital hygiene: leave the machine as you found it\n\n> **Covenant:** _A clone is borrowed space. Give it back when the work is done._\n\nOn 2026-10-05 an agent host ran out of disk. Its temp directory held **217 GB** in about\n**930 leftover agent clones**: roughly 92 GB of `node_modules` and 14 GB of Rust `target/`.\nNo agent had removed its clone when its work closed. When the disk filled, every agent on the\nmachine stopped mid-task, including the ones doing nothing wrong. Hygiene is not tidiness. It\nis what lets the next agent run at all, and Ubuntu applies here: what one agent leaves behind,\nevery other agent pays for.\n\nOn 2026-10-06 the other failure happened. Several agents on one release task cloned into one\nshared, readable folder, `$TMPDIR/rel1006`, and one of them ran `git checkout` in the others'\nclones. Their working trees moved under them mid-task. A readable folder name says what the task\nis; it does not say whose clone it is. So every clone now gets a folder that is unique to the\nagent that made it, and the script refuses to touch a clone another agent owns.\n\nOn 2026-10-07, in a Claude Code cloud session with a fixed disk allowance of about 38 GB, the\ngaps were different. The 50 GB rule could never be met, so agents either ignored it or stopped.\n`$TMPDIR` was unset, so a documented `\"$TMPDIR/<task>-…\"` meant `/<task>-…` at the filesystem\nroot. `/tmp` held 4.8 GB of litter that no clone audit sees: about 600 folders of Vitest module\ncopies that test runs never removed, a 472 MB stray clone and a 572 MB Node compile cache. A\nshared `CARGO_TARGET_DIR` served a stale test binary, so 38 tests ran instead of 42 and the run\nwas a false green. Code reviews fetched PR heads as `pr<N>` branches into shared clones, and the\nsession's stop hook then flagged already-merged work as unpushed. This skill now covers cloud\nsessions, worktrees, tool temp litter, the shared-target hazard and scratch review branches.\n\nThis skill has four parts: what to do **before** you start, **while** you work, **on close**,\nand in the **periodic sweep**. The safety rules apply to all of them. The script `hygiene.sh`\nin this skill folder runs the checks, the audit and the prune for you.\n\n## Get the script\n\n`hygiene.sh` is portable bash (3.2+) for macOS and Linux. Every command that deletes (`prune`,\n`tmp`, `close`, `sweep`) is a **dry run** unless you pass `--apply`; `close` and `sweep` still\nfetch first.\n`clone` and `worktree` always write: they make a clone or a worktree. `check` and `audit` only\nread.\n\n| Where you are | Path |\n| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/digital-hygiene/hygiene.sh` |\n| The public `mzizi` plugin | `skills/digital-hygiene/hygiene.sh` in the plugin |\n| Only the MCP | `curl -fsSL https://unpkg.com/@nyuchi/mzizi-skills/skills/digital-hygiene/hygiene.sh -o \"${TMPDIR:-/tmp}/hygiene.sh\"` |\n\n```bash\nH=\"${TMPDIR:-/tmp}/hygiene.sh\" # or one of the paths above\nexport HYGIENE_AGENT_ID=\"$(od -An -N3 -tx1 /dev/urandom | tr -d ' \\n')\" # once per agent\nexport HYGIENE_ROOT=\"${TMPDIR:-/tmp}\" # name the root: --apply refuses a shared fallback root\nbash \"$H\" check # free disk; exits 1 below 50 GB (cloud: 25% of the allowance)\nD=\"$(bash \"$H\" clone <org>/<repo> --task <task>)\" # ${TMPDIR:-/tmp}/<task>-<repo>-<agent-id>\nW=\"$(bash \"$H\" worktree <clone> --task <task> --branch <b>)\" # or a worktree: …-<agent-id>-wt-<b>\n# (of your own marked clone; of a session's unmarked base clone with --shared)\nbash \"$H\" audit # every clone and worktree: CLEAN, KEEP or ACTIVE, with its owner\nbash \"$H\" prune # regenerable build folders it would remove\nbash \"$H\" tmp # orphaned tool temp folders in ${TMPDIR:-/tmp} it would remove\nbash \"$H\" close \"$D\" --apply # close your own clone (or worktree: git worktree remove)\nbash \"$H\" sweep --exclude \"$D\" # prune + tmp + delete clean idle clones (dry run)\n```\n\nOptions: `--root DIR` (default `$HYGIENE_ROOT`, else `$TMPDIR`, else `/tmp`; a deleting command\nrefuses that fallback unless only you can write in it, so name it), `--tmp DIR` (for\n`tmp`; default `$TMPDIR`, else `/tmp`), `--agent ID` (default `$HYGIENE_AGENT_ID`), `--task NAME`\nand `--branch B` (for `clone` and `worktree`), `--from REF` (where a new worktree branch or a\ndetached worktree starts), `--force-new` and `--shared` (see \"Worktrees\"), `--older-than MIN`\n(default 120),\n`--min-gb N` or `--min-pct P` (for `check`), `--exclude PATH` (repeatable), `--force-owner`,\n`--apply`. `clone` and `worktree` create a missing root with `mkdir -p` (the root only, never a\ntask folder), after the same checks as an existing root. PR lookups use `gh api` only, so Claude\nCode's built-in `gh` in a cloud session works; `HYGIENE_GH=none` turns them off. Exit codes: `0`\nok, `1` low disk, a refused path or a clone another agent owns (and from `close`, a clone left in\nplace only because a worktree of it, or of a clone inside it, is still checked out: close that\nworktree first), `2` a clone was kept because it holds unpublished work.\n\n**`$TMPDIR` can be unset**, as it is in a Claude Code cloud session. Then `\"$TMPDIR/x\"` is\n`/x`, at the filesystem root. Every command in this skill writes `\"${TMPDIR:-/tmp}\"`; write it\nthat way in your own commands too. `hygiene.sh` falls back to `/tmp` by itself, and `check`\nwarns when `$TMPDIR` is unset.\n\n## Cloud sessions (Claude Code on the web)\n\nA cloud session runs on a VM that Anthropic hosts, one per session, and the VM goes away when\nthe session ends. Nothing you leave there outlives the session. The risk is different: you fill\n**your own allowance** part-way through the work, and every agent in the session stops.\n\n- **How to tell.** Claude Code sets `CLAUDE_CODE_REMOTE=true` in a cloud session, and it is never\n `true` locally ([environment variables](https://code.claude.com/docs/en/env-vars)). The\n `CLAUDE_CODE_REMOTE_SESSION_ID` variable names the session. A Remote Control session is a local\n session that you steer from another device: it runs on your machine and follows the local\n rules.\n\n ```bash\n [ \"${CLAUDE_CODE_REMOTE:-}\" = true ] && echo \"cloud session\"\n ```\n\n- **The disk is a per-session allowance, so `df`'s \"Avail\" is what counts.** The filesystem is\n the host's. On 2026-10-07 `df -h /` showed a size of 252G with 29G used and 8.9G available,\n and the 252G meant nothing to the session. Used plus available, about 38 GB there, is the\n session's allowance. Anthropic documents about 30 GB of disk for hosted environments\n ([resource limits](https://code.claude.com/docs/en/cloud-environments#resource-limits)), so\n measure it; don't assume it.\n- **Thresholds.** The 50 GB rule can never be met in a cloud session. When\n `CLAUDE_CODE_REMOTE=true`, `hygiene.sh check` uses **25% of used + available** by default\n (about 9.5 GB of a 38 GB allowance), or the `--min-gb N` the session sets. It prints the\n allowance and how many heavy builds fit.\n- **Fan-out.** Allow **10 GB free for each concurrent heavy build**, such as a pnpm install\n with a build or test run, or a cargo build, **on top of the threshold**: the threshold is the\n floor the session keeps free, so builds may not spend it. `check` prints the room as\n (free − threshold) / 10 GB, rounded down. With 22 GB free over a 9.5 GB threshold, run one\n heavy build at a time; with room for 0, sweep first or run nothing heavy beside what runs.\n Agents that only read, review or write docs need no build budget. Check again before each\n wave of sub-agents, not only at the start.\n- **Where clones and worktrees go.** The session's repositories are usually cloned already (for\n example under `/workspace`). Make a `git worktree` of that clone per branch instead of a\n second clone: it shares the object store, so a worktree costs only its checkout. Keep every\n worktree, and any extra clone, under one root next to the clones and on the same disk:\n `export HYGIENE_ROOT=/workspace/wt`. Then `hygiene.sh worktree`, `clone`, `audit` and\n `sweep` all use it. The session's base clone has no owner marker, so add worktrees to it with\n `hygiene.sh worktree /workspace/<repo> --shared …`. The folder need not exist: `worktree` and `clone` create the root itself\n (`mkdir -p`, the root only), and `audit`, `prune` and `sweep` report a missing root as an\n error. By hand, `mkdir -p \"$HYGIENE_ROOT\"` once. Don't put clones in `/tmp`. Tools litter `/tmp`, and Claude Code keeps the\n session's own scratch there (`/tmp/claude-*`), which you must never touch.\n- **Give space back as you go.** Once your PR is open and CI is green, delete that worktree's\n `node_modules` (`hygiene.sh prune`, or by hand), which a reinstall regenerates. Run\n `hygiene.sh tmp` after test runs. If the allowance runs out anyway, free what you created\n first, then commit and push, and report that the session's disk is spent. A new session\n starts on a fresh machine.\n\n## Before you start\n\n1. **Check the disk.** Run `df -h /`, or `hygiene.sh check`. **On a shared host, do not fan out\n below 50 GB free.** In a cloud session, `check`'s threshold is 25% of the session's\n allowance, and you need 10 GB free above it per concurrent heavy build (see \"Cloud sessions\"). Below\n the threshold, don't spawn sub-agents and don't start parallel clones or builds. Run a sweep\n first (see below), or report the low disk to your supervisor. A fan-out on a nearly full\n disk fails all at once, part-way through every branch.\n2. **Clone only into a folder unique to you: `${TMPDIR:-/tmp}/<task>-<repo>-<short-id>`**, or a\n worktree (see \"Worktrees\" below). The\n short id is unique per agent, for example six random hex characters\n (`od -An -N3 -tx1 /dev/urandom | tr -d ' \\n'`). Set it once as `HYGIENE_AGENT_ID` and use\n it for every clone you make. Never clone into `~/GitHub`, which holds the owner's working\n copies. `hygiene.sh clone` does all of this: it creates the folder (and refuses one that\n already exists), clones into it, writes your agent id into `<clone>/.git/hygiene-owner` (inside\n `.git`, so `git clean -fdx` cannot remove it), and prints the path.\n\n ```bash\n D=\"$(bash \"$H\" clone <org>/<repo> --task <task-name>)\" # or by hand:\n D=\"${TMPDIR:-/tmp}/<task-name>-<repo>-$HYGIENE_AGENT_ID\"\n mkdir \"$D\" && git clone https://github.com/<org>/<repo>.git \"$D\" # mkdir, not mkdir -p\n printf '%s\\n' \"$HYGIENE_AGENT_ID\" >\"$D/.git/hygiene-owner\" # the marker: never skip it\n ```\n\n A clone with no marker has no owner the script can see, so another agent's sweep may\n delete it once it is clean and idle. A `git worktree` of your clone is owned by your clone's\n marker, unless it has its own marker in its git dir (`hygiene.sh worktree` writes one).\n - **Never cd into, check out, fetch into or modify a folder you did not create,** however\n its name reads and whoever's task it seems to be. A shared parent folder is fine\n (`${TMPDIR:-/tmp}/rel1006/` holding one clone per agent); a shared clone is not. If you need\n another agent's work, fetch its pushed branch into your own clone.\n - `mkdir` without `-p` is the claim: it fails when the folder exists, so you notice instead\n of moving into someone else's clone.\n\n3. **Share build caches; don't multiply them.** Each clone with its own caches costs gigabytes\n in every clone.\n - **Rust:** one `CARGO_TARGET_DIR` per machine or session, for example\n `export CARGO_TARGET_DIR=\"${TMPDIR:-/tmp}/cargo-target\"`. Every clone then reuses one\n compiled dependency tree instead of building its own `target/`.\n\n **The hazard: a shared target dir can run a stale test binary.** Cargo names a workspace\n crate's artifacts with a hash that leaves out where the workspace is on disk, so worktrees\n of one repository on different branches build to the same files. Its freshness check\n compares file times, so a binary that another worktree built from other source can look up\n to date. On 2026-10-07 an agent saw 38 tests run\n instead of 42, a false green, until `cargo clean -p <crate>`. So:\n - **Before the final verification run** of a crate you changed, run\n `cargo clean -p <crate>` (it removes only that crate's artifacts, and the dependency tree\n stays shared), then build and test. Check that the test count matches the tests you\n expect.\n - Or give a crate you edit its own target for the task:\n `CARGO_TARGET_DIR=\"$PWD/target\" cargo test -p <crate>`. Use the shared target for\n dependencies and for crates you only read. Prune the private `target/` when you close.\n\n - **pnpm:** the content-addressed store is shared by default. Keep it that way: never pass\n `--store-dir` into the clone, and don't use `node-linker=hoisted` copies when links will\n do.\n - **npm:** the cache (`~/.npm`) is shared. Use `npm ci --prefer-offline` and don't point\n `--cache` into the clone.\n - Install only what the task needs. `pnpm install --filter <pkg>...` installs one workspace\n package; a whole monorepo install for a docs fix is waste.\n\n### Worktrees: a first-class alternative to a clone\n\nA `git worktree` is a second checkout of a clone you already have. It shares the clone's object\nstore, so it costs only the checked-out files, and it is the right default when the repository\nis already on disk (a cloud session's `/workspace/<repo>`, or your own clone). The rules for\nclones apply to worktrees, plus three of their own:\n\n1. **Make it with an owner marker.** `hygiene.sh worktree <clone> --task <task> --branch <b>`\n claims `$HYGIENE_ROOT/<task>-<repo>-<agent-id>-wt-<b>` (`mkdir`, as for a clone; `/` in the\n branch becomes `-`, and a detached worktree ends in `-wt-<short sha>`). The `-wt-` suffix keeps\n it apart from your own clone for the same task. It adds the worktree, writes your agent id to\n its own git dir (`<clone>/.git/worktrees/<name>/hygiene-owner`), and prints the path.\n - `--branch <b>` checks out the local `<b>` when it exists. Otherwise, when `origin/<b>`\n exists, it creates `<b>` from `origin/<b>` and tracks it. Otherwise it creates `<b>` from\n `--from REF` (default: the clone's HEAD). Without `--branch` the worktree is detached at\n `--from` (or HEAD).\n - `--from` is never ignored. With a local `<b>`, the script refuses it, so drop `--from` or\n pick a new branch. With `origin/<b>` at another commit, it refuses too, unless you pass\n `--force-new`, which starts a new `<b>` from `--from` that tracks nothing.\n - Beside the marker it records the branch it created (close deletes it once it is\n published) and the commit the worktree started from (`hygiene-from`, marked `remote` when\n that commit was on the remote at that moment, for example a PR head just fetched into\n `FETCH_HEAD`).\n - If `git worktree add` fails part-way, or the marker cannot be written, the script runs\n `git worktree remove --force`, removes the folder and deletes the branch it created, then\n exits 1. It never leaves a registered worktree without a marker.\n\n By hand:\n\n ```bash\n W=\"$HYGIENE_ROOT/<task>-<repo>-$HYGIENE_AGENT_ID-wt-<branch>\"\n mkdir \"$W\" && git -C <clone> worktree add -b <branch> \"$W\" origin/staging\n printf '%s\\n' \"$HYGIENE_AGENT_ID\" >\"$(git -C \"$W\" rev-parse --absolute-git-dir)/hygiene-owner\"\n ```\n\n2. **Only from a clone that is yours or the session's.** Adding a worktree writes into the\n clone's `.git`, and closing it later runs `git worktree remove` and `git branch -D` there, so\n the clone passes the same checks as anything the script deletes under. The script refuses a\n clone under `~/GitHub` or on an `--exclude` path, a clone whose git data is there (it resolves\n the real git dir first), a clone whose marker names another agent, and a clone with no marker\n at all. A session's unmarked base clone (`/workspace/<repo>`) is\n shared by the session's agents: pass `--shared` to add worktrees to it, but never check out,\n commit in or fetch PR heads into the base checkout itself. `--shared` never lifts the other\n refusals.\n3. **Close it with `git worktree remove`, never `rm -rf`.** `rm -rf` leaves the worktree's\n entry under `<clone>/.git/worktrees/`, and its branch stays \"checked out\" there until\n `git worktree prune`. `hygiene.sh close <worktree> --apply` audits only what the worktree holds,\n which is its uncommitted changes and its checked-out branch. Stashes and the clone's other\n branches stay with the clone. If the audit is clean, the script runs\n `git worktree remove` and then deletes the branch it created. By hand:\n `git -C <clone> worktree remove <path>`, and `git -C <clone> worktree prune` after an\n accidental `rm`. A clone that still has worktrees checked out is `KEEP` in an audit, because\n deleting it would cut them off from their git data. `close` leaves it in place and exits `1`,\n not `2`, since nothing in it is unpublished. It names each worktree holding the clone, with\n its owner and whether it is locked: close your own worktrees first, and report one that is\n locked or another agent's instead of unlocking or removing it. A worktree that\n `git worktree remove` would refuse without `--force` (locked, holding a submodule, or with\n modified or untracked files) is never removed, and the dry run says so too.\n\n Before every git write (the fetch, `git worktree add` and `remove`, `git branch -D`) the\n script resolves the checkout's real git dir and common git dir and refuses `/`, your home and\n anything under `~/GitHub`. A worktree of a clone under `~/GitHub`, or a `--separate-git-dir`\n checkout whose git dir is there, is never fetched into, removed or deleted. An `--exclude`\n path also refuses writes to a clone's refs and config (the fetch, `git branch -D`), but not\n to a worktree's own entry under `<clone>/.git/worktrees/`: `sweep --exclude <your clone>`\n still removes that clone's finished worktrees, and keeps the branches they were on.\n\n## While you work\n\n- **Don't commit build output.** `node_modules/`, `target/`, `dist/`, `.astro/`, `.wrangler/`,\n `.turbo/`, `.next/` and `.vercel/output/` belong in `.gitignore`. If one shows up in\n `git status`, ignore it; don't add it.\n- **Delete what you made and no longer need.** That includes scratch builds, downloaded\n tarballs and one-off test clones, as soon as they have served their purpose.\n- **Check the disk again before anything large:** a release build, a full test matrix, or a\n second fan-out.\n- **Before every commit, check where you are.** Run `git status` and\n `git branch --show-current`, and confirm HEAD sits on your branch's upstream:\n\n ```bash\n git status && git branch --show-current\n git rev-parse --abbrev-ref '@{u}' # the upstream you expect, e.g. origin/feat/x\n git log --oneline '@{u}..HEAD' # only your own unpushed commits\n git log --oneline 'HEAD..@{u}' # empty, or pull first\n ```\n\n A branch you did not check out, a different upstream, or commits you did not write mean\n something else moved this clone. Stop and report it; don't commit on top.\n\n- **Review a PR in a throwaway worktree, never as a branch in a shared clone.** For a code\n review, `git fetch origin pull/<N>/head:pr<N>` in a shared clone leaves a `pr<N>` branch\n whose commits are on no remote branch. Stop hooks and audits then flag it as unpushed work\n long after the PR merged. Fetch the head without a branch, and check it out detached in a\n worktree of your own:\n\n ```bash\n git -C <clone> fetch origin \"pull/<N>/head\"\n R=\"$(bash \"$H\" worktree <clone> --task review-pr<N> --from FETCH_HEAD)\" # detached\n # ... review, build, test ...\n bash \"$H\" close \"$R\" --apply # git worktree remove\n ```\n\n `close` fetches first, which overwrites `FETCH_HEAD`, and each worktree keeps its own\n `FETCH_HEAD` in its git dir. So the script judges a review worktree by the commit it recorded\n at creation: published when it was fetched from the remote, or when its PR is merged or closed\n (a squash-merged or fork PR's head is on no remote branch), or while it is an open PR's head.\n\n `hygiene.sh audit` lists a `pr<N>` scratch branch (also `pr-<N>`, `pr/<N>` and\n `review/pr<N>`) as **safe to delete** when PR #N is merged or closed and the branch has nothing\n beyond the PR's head. It prints the `git branch -D` to run. A scratch branch with commits beyond\n the head is unpublished work, and the audit keeps it.\n\n- **Test runners leave temp folders.** Vitest 5.0.0 to 5.0.2 leave one folder per run in\n `${TMPDIR:-/tmp}`, and so does Vitest 4.x as Vite+ (`vite-plus`) runs it. It has a random 21-character name, holds only `client/` and `ssr/` module\n copies, and is 25 to 57 MB per run. Use Vitest 5.0.3 or later (Vite+ 1.1.0 or later), and run\n `hygiene.sh tmp` after a session of test runs: it finds the folders by their contents, whatever\n the version.\n\n## On close: when the PR is merged or abandoned\n\nClosing a clone is an audit first and a deletion second. **Never delete a clone you have not\naudited.** The audit asks one question: is there any work here that exists nowhere else?\n\n1. **Fetch**, so you judge against the remote as it is now: `git fetch --prune origin`. Even\n then, a remote-tracking ref is the clone's memory of the remote, not the remote: it outlives\n a branch deleted upstream whenever a fetch fails or never ran. Ask the remote itself\n (`git ls-remote origin`); a remote you cannot reach vouches for nothing.\n2. **Uncommitted changes:** `git status --porcelain --untracked-files=all --ignore-submodules=none`.\n Anything listed exists only here. Pass both options: `status.showUntrackedFiles=no` would hide\n untracked files, and `submodule.<name>.ignore` would hide a submodule moved off its commit.\n Status cannot see two more kinds of work: an edit to a file marked `--assume-unchanged` or\n `--skip-worktree` (`git ls-files -v`: a lowercase tag, or `S`), and **ignored files**\n (`git status --ignored`). An ignored `.env` or a folder of notes kept out of git is work;\n only build output (`node_modules`, `target`, `dist` and the like) is not.\n3. **Stashes:** `git stash list`. A stash is never on a remote.\n4. **Local refs whose commits are on no remote** — branches, and also tags, notes and\n remote-tracking refs (one whose branch was deleted upstream may be the only copy):\n\n ```bash\n git ls-remote origin | cut -f1 | sort -u | sed 's/^/^/' >/tmp/on-remote # what origin has now\n git for-each-ref --format='%(refname)' | while read -r r; do\n n=$(git rev-list --count --stdin \"$r\" </tmp/on-remote 2>/dev/null) || n=?\n [ \"$n\" = 0 ] || echo \"$r: $n commit(s) on no remote\"\n done\n ```\n\n (A commit the remote has that you have not fetched is not named here; `--stdin` skips\n nothing, so fetch first. A commit in `FETCH_HEAD` counts only when it came from the\n remote's own URL: `git fetch . <branch>` writes one too.)\n\n5. **Map each of those branches to its PR state.** Merged or closed counts as published:\n\n ```bash\n gh pr list -R <owner>/<repo> --head <branch> --state all --json number,state,title\n ```\n\n `hygiene.sh` asks `gh api repos/<owner>/<repo>/pulls?head=<owner>:<branch>`. That never finds\n a PR from a fork, so it then asks for the PRs associated with the branch's tip commit\n (`commits/<sha>/pulls`), and then scans the latest 100 PRs for one whose head ref is the\n branch and whose head commit is the tip or behind it. A tip that is an open PR's head is\n pushed, even when no remote-tracking ref has it.\n\n **A squash merge makes \"not on any remote\" meaningless as proof.** Squashing writes a new\n commit on the base branch, so the branch's own commits stay local-only even though the\n work shipped. Then the remote branch is deleted, and every squash-merged branch looks\n unpublished. So check in this order:\n - Is every commit on a remote? Then it is published.\n - Is there a PR from this branch that is `MERGED` or `CLOSED`, **and is the branch's tip that\n PR's head, or behind it**? Then it is published (merged, or deliberately abandoned). A\n commit made after the PR's head is not in the PR, whatever its state.\n - Is every unpublished commit already on a remote as **the same change**\n (`git patch-id --stable`, as `git cherry` compares; a rebase or a one-commit squash keeps\n the change)? Then it is published. A matching commit subject proves nothing: two\n different changes can carry one subject.\n - Otherwise it is **unpublished**. This includes an `OPEN` PR whose branch is behind your\n local commits.\n\n6. **Publish or report anything genuinely unpublished.** Push the branch and open a PR (into\n `staging` where the repo has one), or report it to your supervisor with the branch name,\n the commit subjects and why it was not published. Don't delete work to make a clone\n \"clean\".\n7. **Delete the clone, and only your own.** Delete it only after steps 1 to 6 come back\n clean, and then report what was freed. Never delete another agent's folder, even an empty\n one, even in a shared parent.\n\n`hygiene.sh close <clone> --apply` runs steps 1 to 5 and 7. It refuses, before it fetches or\nreads anything, a clone whose `.git/hygiene-owner` names another agent, or any marked clone when\nyou have given no agent id. A clone nested inside yours,\nsuch as a gitignored cache clone, is deleted with it, so it is audited too: unpublished work\nthere keeps the whole clone. It exits `2`, and deletes\nnothing, when step 6 is needed. A clone kept only because a worktree of it, or of a clone nested\nin it, is still checked out is not step 6: `close` exits `1` and says to close that worktree\nfirst. A submodule belongs to its parent, whatever the shape of its `.git` (a file pointing\ninto the parent's `.git/modules/`, or a folder, when `git submodule add` adopted a repository\nthat was already there). The script asks the parent: git's superproject, or a gitlink or\n`.gitmodules` path for it. It is audited as part of the parent, and `close` refuses it on its\nown.\n\n**Nothing goes with a folder unless everything in it may go.** Before a clone is deleted\n(`rm -rf`) or a worktree removed (`git worktree remove`, which also deletes everything inside\nit), the script finds every `.git` entry inside it, in gitignored folders, `node_modules` and\n`target` too, and gates each checkout the same way. Git has to read it as its own repository,\nits git dir has to be inside the folder, it may not be locked, and its own audit has to be clean\nand idle. A worktree of a clone outside the folder, such as a cache holding a\n`git worktree add` of `~/GitHub/live`, is never deleted with it: `git worktree remove` it\nagainst its own clone first, or report it. Another agent's clone inside it holds it the same\nway (`close` exits `1`: report it). Any refusal keeps the whole folder, with a reason that names\nthe checkout inside.\n\n- **A `.git` git does not read as its own** (a `.git` folder that lost its `HEAD`, a git dir that\n is gone) is kept for a human. Git skips such a folder and answers for the repository around\n it, so the script checks that the git dir git resolves is the checkout's own and that the\n checkout is its top level. `close` audits it and exits `2`, as `sweep` does.\n- **Git dirs not named `.git`** are gated too: a bare repository or mirror (`foo.git/`) and a\n deinit'd submodule's `.git/modules/<name>`. One goes only when it has no stash and every\n commit any of its refs or `HEAD` reaches is named by a remote it can reach (`git ls-remote`),\n never by its own remote-tracking refs. One with no reachable remote and any commit is kept:\n delete it by hand once you know it holds nothing of yours.\n- **A checkout found inside another runs nothing its config names.** A package that ships a\n `.git` in `node_modules`, a cache clone or a bare mirror has config anyone could have written,\n and `git ls-remote`, `git fetch` or even `git status` with it can run a command (an `ext::`\n remote URL, `core.sshCommand`, `remote.<name>.uploadpack`, `core.fsmonitor`, a filter). So the\n script never fetches into one, and asks its remotes only by URL (https or ssh, from an empty\n repository with every other protocol off), alongside what the clone around it was told by its\n own remotes. Any config key outside a short list of inert ones (`core.*` basics,\n `remote.*.url`/`fetch`, `branch.*`, `submodule.*.url`, …) keeps it unread, as `KEEP`\n (\"untrusted git config\"). Every git the script runs has `core.fsmonitor`, hooks, signature\n checks, the `ext`/`fd` transports and lazy fetches switched off, and no git runs on a scanned\n checkout at all until its config (and any `commondir`, which is followed, or refused for a\n bare git dir) has passed; a `core.worktree` pointing outside it or an\n `objects/info/alternates` keeps it. A `commondir` and a `.git` gitfile are\n read exactly as git reads them (the whole file, only a trailing `\\n`/`\\r`\n stripped); one with an embedded newline or CR, a NUL, or a byte outside\n printable ASCII is refused rather than parsed two ways, and a malformed\n `.git` file keeps its folder instead of being skipped. A registered submodule of your own clone is trusted while\n its config is inert.\n- **The dry run counts each byte once.** A sweep measures every checkout once before anything\n goes, and a checkout inside another is not counted again with the folder around it. The total\n is shown in exact KB, and it is what `--apply` frees.\n\n## The periodic sweep\n\nRun a sweep when `check` fails, before a large fan-out, and at the end of a long supervisor\nsession. It has three passes, and the dry run comes first:\n\n1. **Safe prune of regenerable folders.** These are `node_modules`, `target`, `.astro`,\n `.wrangler`, `.turbo` and `.next`, removed **only when older than N minutes** (default 120),\n so an agent mid-build is not hit. A sibling manifest has to prove what each folder is:\n `Cargo.toml` (or the cargo `CACHEDIR.TAG`) for `target`, `package.json` for `.next`,\n `.turbo` and `.astro`, and a `wrangler` config for `.wrangler`. A folder named `target` that\n holds notes is not a build cache. Pruning loses nothing, because the next install or build\n recreates the folder. A folder with a git checkout inside it (a `.git` anywhere in it) is\n skipped, since its checkout is not regenerable.\n2. **Orphaned tool temp folders** (`hygiene.sh tmp`, also run on its own) in `${TMPDIR:-/tmp}`\n (or `--tmp DIR`). A folder there is judged by its contents, not only its name, and only\n folders directly in the temp directory, owned by you and untouched for N minutes, are\n candidates:\n - **Vitest/Vite module copies:** a random name (16 to 32 letters, digits, `_` or `-`) holding\n only `client/` and/or `ssr/`. Each of those holds only files named by a 40-hex hash, and\n the files are Vite SSR module code.\n - **npm pack leftovers:** a `tmp.*` folder (from `mktemp -d`) holding only `*.tgz` files\n and/or an unpacked `package/` with a `package.json`.\n\n It never touches `claude-*` (Claude Code's temp and the session's scratchpad), browser\n profiles (`.org.chromium.*`, `*chrom*`, `puppeteer*`, `playwright*`, `*profile*`), symlinks,\n another user's folders, a folder with a git checkout inside it, or an `--exclude` path. It\n reports `node-compile-cache` (Node's\n shared compile cache) with its size and never removes it; delete it by hand when no `node`\n process runs. A stray clone in `/tmp` is a clone: `hygiene.sh audit --root /tmp` finds it.\n\n3. **Audit report of the clones and worktrees, then delete the clean ones.** Each gets one\n verdict:\n - `CLEAN`: nothing unpublished, idle. Deleted with `--apply`.\n - `KEEP`: unpublished work, with the reason. Never deleted: publish it or report it.\n - `ACTIVE`: a file changed inside the window. Another session is probably using it. Leave\n it.\n - `OTHER`: its `.git/hygiene-owner` names another agent. Not audited, not pruned, not deleted.\n\nThe sweep judges worktrees before clones. Removing a clean worktree frees its clone, which is\nthen judged without it (in a dry run, as if it were gone), so a clone is never reported as kept\nonly because the sweep had not reached its worktree yet. A clone held only by a worktree that is\nstill in use (active, or another agent's) is left in place and reported, but it is not counted\nas kept and does not make the sweep exit `2`. The dry run predicts `--apply` exactly: a worktree\nthat `git worktree remove` would refuse (locked, holding a submodule) is not counted as removed,\nso the clone it holds is not reported as deletable either. A submodule is never listed on its\nown; it goes only with its parent.\n\nA worktree is deleted with `git worktree remove`, never `rm -rf`. Prune, close and sweep leave\na clone or worktree another agent owns alone. A supervisor clearing up after\nagents that have finished passes `--force-owner`, which still deletes only `CLEAN`, idle\nclones and is still a dry run without `--apply`. A clone with no marker (made by hand, or\nbefore markers) is judged as before.\n\n```bash\nbash hygiene.sh sweep --exclude \"${TMPDIR:-/tmp}/<your-task>\" # read the report\nbash hygiene.sh sweep --exclude \"${TMPDIR:-/tmp}/<your-task>\" --apply # then act on it\n```\n\nReport the result: what was freed, how many clones were deleted, and every `KEEP` with its\nreason.\n\n## Safety rules (all of the above)\n\n- **Never `rm -rf` a path built from an unchecked variable.** If `$DIR` is empty or unset,\n `rm -rf \"$DIR/\"*` deletes from `/`. Use fixed base paths and let `find … -prune` produce the\n paths. Check every path before you delete it: it is absolute, it is inside the root, it is not\n the root itself, `/` or `$HOME`, and it is not under `~/GitHub`. Then use\n `rm -rf -- \"$path\"`. `hygiene.sh` routes every deletion through that check.\n- **Never touch `~/GitHub` working copies, `claude-*` temp folders, browser profiles, or another\n agent's folders.** The owner keeps live\n work in `~/GitHub`. The script refuses a root or a clone there. A clone you did not create\n is someone else's until they or your supervisor say otherwise: never cd into it, check it\n out, fetch into it, build in it or delete it. The script refuses a clone whose\n `.git/hygiene-owner` is not yours unless you pass `--force-owner`. Use `--exclude` for your own\n live clone.\n- **Never delete `staging`,** neither the branch on the remote nor a release PR's head branch.\n A clone is deleted locally; branches on the remote are the repository's business, not the\n sweep's.\n- **Delete only under a root you named, and never through a symlink.** In a temp folder other\n users can write in (`/tmp`, and often `$TMPDIR`), a path checked now and deleted later can be\n redirected: another user swaps a folder for a symlink in between. So `prune`, `close` and\n `sweep` with `--apply` refuse the fallback root unless it is a folder only you can write in;\n set `HYGIENE_ROOT` (or pass `--root`) to the folder your clones live in, which in a cloud\n session is `export HYGIENE_ROOT=\"${TMPDIR:-/tmp}\"`. Every deletion checks that each folder\n from the root down is a real folder you own, not a symlink, then deletes from inside the\n parent it entered with `cd -P` (agent-tools#279).\n- **Run nothing a repository's config names.** In a root other users can write in, a checkout\n is trusted only when `clone` or `worktree` marked it (`hygiene-owner`) and the marker, the\n checkout and its git dir are yours and writable by nobody else; anyone can plant a repository\n there, marker included. Trusted or not, no git runs in a checkout until every key in its\n config is inert (no `credential.helper`, `uploadpack`, `sshCommand`, `hooksPath` or filter)\n and its `hooks/` holds nothing but git's `*.sample` files: a checkout that fails is kept for\n a human. The remote calls reset\n `credential.helper` to your own helpers and turn the file transport off; set\n `HYGIENE_FILE_REMOTES=1` only for a local mirror you trust (agent-tools#279).\n- **Dry run first, then `--apply`.** Read what would go before anything goes.\n- **Report what was freed,** in GB, with the number of folders and clones and every clone that\n was kept and why. \"Cleaned up\" is not a report.\n\n## Checklist\n\n- [ ] `df -h /` before starting. On a shared host, 50 GB free or more before any fan-out. In a\n cloud session (`CLAUDE_CODE_REMOTE=true`), `hygiene.sh check` passes, and you have 10 GB\n free above its threshold per concurrent heavy build.\n- [ ] Every path is written `\"${TMPDIR:-/tmp}/…\"`, never a bare `\"$TMPDIR/…\"`.\n- [ ] Cloned into `${TMPDIR:-/tmp}/<task>-<repo>-<short-id>` (`hygiene.sh clone`), or a marked\n worktree (`hygiene.sh worktree`; in a cloud session under `HYGIENE_ROOT`), unique to you;\n never `~/GitHub`, never a folder another agent made.\n- [ ] PR reviews in a throwaway detached worktree, not `pr<N>` branches in a shared clone.\n- [ ] `cargo clean -p <crate>` before the final test run of a crate you changed, when\n `CARGO_TARGET_DIR` is shared.\n- [ ] `git status`, `git branch --show-current` and the upstream checked before every commit.\n- [ ] `CARGO_TARGET_DIR` shared; pnpm store and npm cache not redirected into the clone.\n- [ ] No build output committed.\n- [ ] On close: fetched, then checked the status, the stashes, and the branches on no remote.\n PR state looked up per branch, because of squash merges.\n- [ ] Unpublished work pushed into a PR, or reported.\n- [ ] Your own clone deleted, or your worktree removed with `git worktree remove`, nobody\n else's, and the amount freed reported.\n- [ ] `hygiene.sh tmp` run after test runs; orphaned tool temp folders removed.",
228
+ "body_mdx": "# Digital hygiene: leave the machine as you found it\n\n> **Covenant:** _A clone is borrowed space. Give it back when the work is done._\n\nOn 2026-10-05 an agent host ran out of disk. Its temp directory held **217 GB** in about\n**930 leftover agent clones**: roughly 92 GB of `node_modules` and 14 GB of Rust `target/`.\nNo agent had removed its clone when its work closed. When the disk filled, every agent on the\nmachine stopped mid-task, including the ones doing nothing wrong. Hygiene is not tidiness. It\nis what lets the next agent run at all, and Ubuntu applies here: what one agent leaves behind,\nevery other agent pays for.\n\nOn 2026-10-06 the other failure happened. Several agents on one release task cloned into one\nshared, readable folder, `$TMPDIR/rel1006`, and one of them ran `git checkout` in the others'\nclones. Their working trees moved under them mid-task. A readable folder name says what the task\nis; it does not say whose clone it is. So every clone now gets a folder that is unique to the\nagent that made it, and the script refuses to touch a clone another agent owns.\n\nOn 2026-10-07, in a Claude Code cloud session with a fixed disk allowance of about 38 GB, the\ngaps were different. The 50 GB rule could never be met, so agents either ignored it or stopped.\n`$TMPDIR` was unset, so a documented `\"$TMPDIR/<task>-…\"` meant `/<task>-…` at the filesystem\nroot. `/tmp` held 4.8 GB of litter that no clone audit sees: about 600 folders of Vitest module\ncopies that test runs never removed, a 472 MB stray clone and a 572 MB Node compile cache. A\nshared `CARGO_TARGET_DIR` served a stale test binary, so 38 tests ran instead of 42 and the run\nwas a false green. Code reviews fetched PR heads as `pr<N>` branches into shared clones, and the\nsession's stop hook then flagged already-merged work as unpushed. This skill now covers cloud\nsessions, worktrees, tool temp litter, the shared-target hazard and scratch review branches.\n\nThis skill has four parts: what to do **before** you start, **while** you work, **on close**,\nand in the **periodic sweep**. The safety rules apply to all of them. The script `hygiene.sh`\nin this skill folder runs the checks, the audit and the prune for you.\n\n## Get the script\n\n`hygiene.sh` is portable bash (3.2+) for macOS and Linux. Every command that deletes (`prune`,\n`tmp`, `close`, `sweep`) is a **dry run** unless you pass `--apply`; `close` and `sweep` still\nfetch first.\n`clone` and `worktree` always write: they make a clone or a worktree. `check` and `audit` only\nread.\n\n| Where you are | Path |\n| ------------------------------- | ------------------------------------------------------------------------------------------ |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/digital-hygiene/hygiene.sh` |\n| The `mzizi-dev` plugin | `skills/digital-hygiene/hygiene.sh` in the plugin |\n| Only the MCP | Install the package, then use the first path: `npm install --no-save @nyuchi/mzizi-skills` |\n\n```bash\nH=\"${TMPDIR:-/tmp}/hygiene.sh\" # or one of the paths above\nexport HYGIENE_AGENT_ID=\"$(od -An -N3 -tx1 /dev/urandom | tr -d ' \\n')\" # once per agent\nexport HYGIENE_ROOT=\"${TMPDIR:-/tmp}\" # name the root: --apply refuses a shared fallback root\nbash \"$H\" check # free disk; exits 1 below 50 GB (cloud: 25% of the allowance)\nD=\"$(bash \"$H\" clone <org>/<repo> --task <task>)\" # ${TMPDIR:-/tmp}/<task>-<repo>-<agent-id>\nW=\"$(bash \"$H\" worktree <clone> --task <task> --branch <b>)\" # or a worktree: …-<agent-id>-wt-<b>\n# (of your own marked clone; of a session's unmarked base clone with --shared)\nbash \"$H\" audit # every clone and worktree: CLEAN, KEEP or ACTIVE, with its owner\nbash \"$H\" prune # regenerable build folders it would remove\nbash \"$H\" tmp # orphaned tool temp folders in ${TMPDIR:-/tmp} it would remove\nbash \"$H\" close \"$D\" --apply # close your own clone (or worktree: git worktree remove)\nbash \"$H\" sweep --exclude \"$D\" # prune + tmp + delete clean idle clones (dry run)\n```\n\nOptions: `--root DIR` (default `$HYGIENE_ROOT`, else `$TMPDIR`, else `/tmp`; a deleting command\nrefuses that fallback unless only you can write in it, so name it), `--tmp DIR` (for\n`tmp`; default `$TMPDIR`, else `/tmp`), `--agent ID` (default `$HYGIENE_AGENT_ID`), `--task NAME`\nand `--branch B` (for `clone` and `worktree`), `--from REF` (where a new worktree branch or a\ndetached worktree starts), `--force-new` and `--shared` (see \"Worktrees\"), `--older-than MIN`\n(default 120),\n`--min-gb N` or `--min-pct P` (for `check`), `--exclude PATH` (repeatable), `--force-owner`,\n`--apply`. `clone` and `worktree` create a missing root with `mkdir -p` (the root only, never a\ntask folder), after the same checks as an existing root. PR lookups use `gh api` only, so Claude\nCode's built-in `gh` in a cloud session works; `HYGIENE_GH=none` turns them off. Exit codes: `0`\nok, `1` low disk, a refused path or a clone another agent owns (and from `close`, a clone left in\nplace only because a worktree of it, or of a clone inside it, is still checked out: close that\nworktree first), `2` a clone was kept because it holds unpublished work.\n\n**`$TMPDIR` can be unset**, as it is in a Claude Code cloud session. Then `\"$TMPDIR/x\"` is\n`/x`, at the filesystem root. Every command in this skill writes `\"${TMPDIR:-/tmp}\"`; write it\nthat way in your own commands too. `hygiene.sh` falls back to `/tmp` by itself, and `check`\nwarns when `$TMPDIR` is unset.\n\n## Cloud sessions (Claude Code on the web)\n\nA cloud session runs on a VM that Anthropic hosts, one per session, and the VM goes away when\nthe session ends. Nothing you leave there outlives the session. The risk is different: you fill\n**your own allowance** part-way through the work, and every agent in the session stops.\n\n- **How to tell.** Claude Code sets `CLAUDE_CODE_REMOTE=true` in a cloud session, and it is never\n `true` locally ([environment variables](https://code.claude.com/docs/en/env-vars)). The\n `CLAUDE_CODE_REMOTE_SESSION_ID` variable names the session. A Remote Control session is a local\n session that you steer from another device: it runs on your machine and follows the local\n rules.\n\n ```bash\n [ \"${CLAUDE_CODE_REMOTE:-}\" = true ] && echo \"cloud session\"\n ```\n\n- **The disk is a per-session allowance, so `df`'s \"Avail\" is what counts.** The filesystem is\n the host's. On 2026-10-07 `df -h /` showed a size of 252G with 29G used and 8.9G available,\n and the 252G meant nothing to the session. Used plus available, about 38 GB there, is the\n session's allowance. Anthropic documents about 30 GB of disk for hosted environments\n ([resource limits](https://code.claude.com/docs/en/cloud-environments#resource-limits)), so\n measure it; don't assume it.\n- **Thresholds.** The 50 GB rule can never be met in a cloud session. When\n `CLAUDE_CODE_REMOTE=true`, `hygiene.sh check` uses **25% of used + available** by default\n (about 9.5 GB of a 38 GB allowance), or the `--min-gb N` the session sets. It prints the\n allowance and how many heavy builds fit.\n- **Fan-out.** Allow **10 GB free for each concurrent heavy build**, such as a pnpm install\n with a build or test run, or a cargo build, **on top of the threshold**: the threshold is the\n floor the session keeps free, so builds may not spend it. `check` prints the room as\n (free − threshold) / 10 GB, rounded down. With 22 GB free over a 9.5 GB threshold, run one\n heavy build at a time; with room for 0, sweep first or run nothing heavy beside what runs.\n Agents that only read, review or write docs need no build budget. Check again before each\n wave of sub-agents, not only at the start.\n- **Where clones and worktrees go.** The session's repositories are usually cloned already (for\n example under `/workspace`). Make a `git worktree` of that clone per branch instead of a\n second clone: it shares the object store, so a worktree costs only its checkout. Keep every\n worktree, and any extra clone, under one root next to the clones and on the same disk:\n `export HYGIENE_ROOT=/workspace/wt`. Then `hygiene.sh worktree`, `clone`, `audit` and\n `sweep` all use it. The session's base clone has no owner marker, so add worktrees to it with\n `hygiene.sh worktree /workspace/<repo> --shared …`. The folder need not exist: `worktree` and `clone` create the root itself\n (`mkdir -p`, the root only), and `audit`, `prune` and `sweep` report a missing root as an\n error. By hand, `mkdir -p \"$HYGIENE_ROOT\"` once. Don't put clones in `/tmp`. Tools litter `/tmp`, and Claude Code keeps the\n session's own scratch there (`/tmp/claude-*`), which you must never touch.\n- **Give space back as you go.** Once your PR is open and CI is green, delete that worktree's\n `node_modules` (`hygiene.sh prune`, or by hand), which a reinstall regenerates. Run\n `hygiene.sh tmp` after test runs. If the allowance runs out anyway, free what you created\n first, then commit and push, and report that the session's disk is spent. A new session\n starts on a fresh machine.\n\n## Before you start\n\n1. **Check the disk.** Run `df -h /`, or `hygiene.sh check`. **On a shared host, do not fan out\n below 50 GB free.** In a cloud session, `check`'s threshold is 25% of the session's\n allowance, and you need 10 GB free above it per concurrent heavy build (see \"Cloud sessions\"). Below\n the threshold, don't spawn sub-agents and don't start parallel clones or builds. Run a sweep\n first (see below), or report the low disk to your supervisor. A fan-out on a nearly full\n disk fails all at once, part-way through every branch.\n2. **Clone only into a folder unique to you: `${TMPDIR:-/tmp}/<task>-<repo>-<short-id>`**, or a\n worktree (see \"Worktrees\" below). The\n short id is unique per agent, for example six random hex characters\n (`od -An -N3 -tx1 /dev/urandom | tr -d ' \\n'`). Set it once as `HYGIENE_AGENT_ID` and use\n it for every clone you make. Never clone into `~/GitHub`, which holds the owner's working\n copies. `hygiene.sh clone` does all of this: it creates the folder (and refuses one that\n already exists), clones into it, writes your agent id into `<clone>/.git/hygiene-owner` (inside\n `.git`, so `git clean -fdx` cannot remove it), and prints the path.\n\n ```bash\n D=\"$(bash \"$H\" clone <org>/<repo> --task <task-name>)\" # or by hand:\n D=\"${TMPDIR:-/tmp}/<task-name>-<repo>-$HYGIENE_AGENT_ID\"\n mkdir \"$D\" && git clone https://github.com/<org>/<repo>.git \"$D\" # mkdir, not mkdir -p\n printf '%s\\n' \"$HYGIENE_AGENT_ID\" >\"$D/.git/hygiene-owner\" # the marker: never skip it\n ```\n\n A clone with no marker has no owner the script can see, so another agent's sweep may\n delete it once it is clean and idle. A `git worktree` of your clone is owned by your clone's\n marker, unless it has its own marker in its git dir (`hygiene.sh worktree` writes one).\n - **Never cd into, check out, fetch into or modify a folder you did not create,** however\n its name reads and whoever's task it seems to be. A shared parent folder is fine\n (`${TMPDIR:-/tmp}/rel1006/` holding one clone per agent); a shared clone is not. If you need\n another agent's work, fetch its pushed branch into your own clone.\n - `mkdir` without `-p` is the claim: it fails when the folder exists, so you notice instead\n of moving into someone else's clone.\n\n3. **Share build caches; don't multiply them.** Each clone with its own caches costs gigabytes\n in every clone.\n - **Rust:** one `CARGO_TARGET_DIR` per machine or session, for example\n `export CARGO_TARGET_DIR=\"${TMPDIR:-/tmp}/cargo-target\"`. Every clone then reuses one\n compiled dependency tree instead of building its own `target/`.\n\n **The hazard: a shared target dir can run a stale test binary.** Cargo names a workspace\n crate's artifacts with a hash that leaves out where the workspace is on disk, so worktrees\n of one repository on different branches build to the same files. Its freshness check\n compares file times, so a binary that another worktree built from other source can look up\n to date. On 2026-10-07 an agent saw 38 tests run\n instead of 42, a false green, until `cargo clean -p <crate>`. So:\n - **Before the final verification run** of a crate you changed, run\n `cargo clean -p <crate>` (it removes only that crate's artifacts, and the dependency tree\n stays shared), then build and test. Check that the test count matches the tests you\n expect.\n - Or give a crate you edit its own target for the task:\n `CARGO_TARGET_DIR=\"$PWD/target\" cargo test -p <crate>`. Use the shared target for\n dependencies and for crates you only read. Prune the private `target/` when you close.\n\n - **pnpm:** the content-addressed store is shared by default. Keep it that way: never pass\n `--store-dir` into the clone, and don't use `node-linker=hoisted` copies when links will\n do.\n - **npm:** the cache (`~/.npm`) is shared. Use `npm ci --prefer-offline` and don't point\n `--cache` into the clone.\n - Install only what the task needs. `pnpm install --filter <pkg>...` installs one workspace\n package; a whole monorepo install for a docs fix is waste.\n\n### Worktrees: a first-class alternative to a clone\n\nA `git worktree` is a second checkout of a clone you already have. It shares the clone's object\nstore, so it costs only the checked-out files, and it is the right default when the repository\nis already on disk (a cloud session's `/workspace/<repo>`, or your own clone). The rules for\nclones apply to worktrees, plus three of their own:\n\n1. **Make it with an owner marker.** `hygiene.sh worktree <clone> --task <task> --branch <b>`\n claims `$HYGIENE_ROOT/<task>-<repo>-<agent-id>-wt-<b>` (`mkdir`, as for a clone; `/` in the\n branch becomes `-`, and a detached worktree ends in `-wt-<short sha>`). The `-wt-` suffix keeps\n it apart from your own clone for the same task. It adds the worktree, writes your agent id to\n its own git dir (`<clone>/.git/worktrees/<name>/hygiene-owner`), and prints the path.\n - `--branch <b>` checks out the local `<b>` when it exists. Otherwise, when `origin/<b>`\n exists, it creates `<b>` from `origin/<b>` and tracks it. Otherwise it creates `<b>` from\n `--from REF` (default: the clone's HEAD). Without `--branch` the worktree is detached at\n `--from` (or HEAD).\n - `--from` is never ignored. With a local `<b>`, the script refuses it, so drop `--from` or\n pick a new branch. With `origin/<b>` at another commit, it refuses too, unless you pass\n `--force-new`, which starts a new `<b>` from `--from` that tracks nothing.\n - Beside the marker it records the branch it created (close deletes it once it is\n published) and the commit the worktree started from (`hygiene-from`, marked `remote` when\n that commit was on the remote at that moment, for example a PR head just fetched into\n `FETCH_HEAD`).\n - If `git worktree add` fails part-way, or the marker cannot be written, the script runs\n `git worktree remove --force`, removes the folder and deletes the branch it created, then\n exits 1. It never leaves a registered worktree without a marker.\n\n By hand:\n\n ```bash\n W=\"$HYGIENE_ROOT/<task>-<repo>-$HYGIENE_AGENT_ID-wt-<branch>\"\n mkdir \"$W\" && git -C <clone> worktree add -b <branch> \"$W\" origin/staging\n printf '%s\\n' \"$HYGIENE_AGENT_ID\" >\"$(git -C \"$W\" rev-parse --absolute-git-dir)/hygiene-owner\"\n ```\n\n2. **Only from a clone that is yours or the session's.** Adding a worktree writes into the\n clone's `.git`, and closing it later runs `git worktree remove` and `git branch -D` there, so\n the clone passes the same checks as anything the script deletes under. The script refuses a\n clone under `~/GitHub` or on an `--exclude` path, a clone whose git data is there (it resolves\n the real git dir first), a clone whose marker names another agent, and a clone with no marker\n at all. A session's unmarked base clone (`/workspace/<repo>`) is\n shared by the session's agents: pass `--shared` to add worktrees to it, but never check out,\n commit in or fetch PR heads into the base checkout itself. `--shared` never lifts the other\n refusals.\n3. **Close it with `git worktree remove`, never `rm -rf`.** `rm -rf` leaves the worktree's\n entry under `<clone>/.git/worktrees/`, and its branch stays \"checked out\" there until\n `git worktree prune`. `hygiene.sh close <worktree> --apply` audits only what the worktree holds,\n which is its uncommitted changes and its checked-out branch. Stashes and the clone's other\n branches stay with the clone. If the audit is clean, the script runs\n `git worktree remove` and then deletes the branch it created. By hand:\n `git -C <clone> worktree remove <path>`, and `git -C <clone> worktree prune` after an\n accidental `rm`. A clone that still has worktrees checked out is `KEEP` in an audit, because\n deleting it would cut them off from their git data. `close` leaves it in place and exits `1`,\n not `2`, since nothing in it is unpublished. It names each worktree holding the clone, with\n its owner and whether it is locked: close your own worktrees first, and report one that is\n locked or another agent's instead of unlocking or removing it. A worktree that\n `git worktree remove` would refuse without `--force` (locked, holding a submodule, or with\n modified or untracked files) is never removed, and the dry run says so too.\n\n Before every git write (the fetch, `git worktree add` and `remove`, `git branch -D`) the\n script resolves the checkout's real git dir and common git dir and refuses `/`, your home and\n anything under `~/GitHub`. A worktree of a clone under `~/GitHub`, or a `--separate-git-dir`\n checkout whose git dir is there, is never fetched into, removed or deleted. An `--exclude`\n path also refuses writes to a clone's refs and config (the fetch, `git branch -D`), but not\n to a worktree's own entry under `<clone>/.git/worktrees/`: `sweep --exclude <your clone>`\n still removes that clone's finished worktrees, and keeps the branches they were on.\n\n## While you work\n\n- **Don't commit build output.** `node_modules/`, `target/`, `dist/`, `.astro/`, `.wrangler/`,\n `.turbo/`, `.next/` and `.vercel/output/` belong in `.gitignore`. If one shows up in\n `git status`, ignore it; don't add it.\n- **Delete what you made and no longer need.** That includes scratch builds, downloaded\n tarballs and one-off test clones, as soon as they have served their purpose.\n- **Check the disk again before anything large:** a release build, a full test matrix, or a\n second fan-out.\n- **Before every commit, check where you are.** Run `git status` and\n `git branch --show-current`, and confirm HEAD sits on your branch's upstream:\n\n ```bash\n git status && git branch --show-current\n git rev-parse --abbrev-ref '@{u}' # the upstream you expect, e.g. origin/feat/x\n git log --oneline '@{u}..HEAD' # only your own unpushed commits\n git log --oneline 'HEAD..@{u}' # empty, or pull first\n ```\n\n A branch you did not check out, a different upstream, or commits you did not write mean\n something else moved this clone. Stop and report it; don't commit on top.\n\n- **Review a PR in a throwaway worktree, never as a branch in a shared clone.** For a code\n review, `git fetch origin pull/<N>/head:pr<N>` in a shared clone leaves a `pr<N>` branch\n whose commits are on no remote branch. Stop hooks and audits then flag it as unpushed work\n long after the PR merged. Fetch the head without a branch, and check it out detached in a\n worktree of your own:\n\n ```bash\n git -C <clone> fetch origin \"pull/<N>/head\"\n R=\"$(bash \"$H\" worktree <clone> --task review-pr<N> --from FETCH_HEAD)\" # detached\n # ... review, build, test ...\n bash \"$H\" close \"$R\" --apply # git worktree remove\n ```\n\n `close` fetches first, which overwrites `FETCH_HEAD`, and each worktree keeps its own\n `FETCH_HEAD` in its git dir. So the script judges a review worktree by the commit it recorded\n at creation: published when it was fetched from the remote, or when its PR is merged or closed\n (a squash-merged or fork PR's head is on no remote branch), or while it is an open PR's head.\n\n `hygiene.sh audit` lists a `pr<N>` scratch branch (also `pr-<N>`, `pr/<N>` and\n `review/pr<N>`) as **safe to delete** when PR #N is merged or closed and the branch has nothing\n beyond the PR's head. It prints the `git branch -D` to run. A scratch branch with commits beyond\n the head is unpublished work, and the audit keeps it.\n\n- **Test runners leave temp folders.** Vitest 5.0.0 to 5.0.2 leave one folder per run in\n `${TMPDIR:-/tmp}`, and so does Vitest 4.x as Vite+ (`vite-plus`) runs it. It has a random 21-character name, holds only `client/` and `ssr/` module\n copies, and is 25 to 57 MB per run. Use Vitest 5.0.3 or later (Vite+ 1.1.0 or later), and run\n `hygiene.sh tmp` after a session of test runs: it finds the folders by their contents, whatever\n the version.\n\n## On close: when the PR is merged or abandoned\n\nClosing a clone is an audit first and a deletion second. **Never delete a clone you have not\naudited.** The audit asks one question: is there any work here that exists nowhere else?\n\n1. **Fetch**, so you judge against the remote as it is now: `git fetch --prune origin`. Even\n then, a remote-tracking ref is the clone's memory of the remote, not the remote: it outlives\n a branch deleted upstream whenever a fetch fails or never ran. Ask the remote itself\n (`git ls-remote origin`); a remote you cannot reach vouches for nothing.\n2. **Uncommitted changes:** `git status --porcelain --untracked-files=all --ignore-submodules=none`.\n Anything listed exists only here. Pass both options: `status.showUntrackedFiles=no` would hide\n untracked files, and `submodule.<name>.ignore` would hide a submodule moved off its commit.\n Status cannot see two more kinds of work: an edit to a file marked `--assume-unchanged` or\n `--skip-worktree` (`git ls-files -v`: a lowercase tag, or `S`), and **ignored files**\n (`git status --ignored`). An ignored `.env` or a folder of notes kept out of git is work;\n only build output (`node_modules`, `target`, `dist` and the like) is not.\n3. **Stashes:** `git stash list`. A stash is never on a remote.\n4. **Local refs whose commits are on no remote** — branches, and also tags, notes and\n remote-tracking refs (one whose branch was deleted upstream may be the only copy):\n\n ```bash\n git ls-remote origin | cut -f1 | sort -u | sed 's/^/^/' >/tmp/on-remote # what origin has now\n git for-each-ref --format='%(refname)' | while read -r r; do\n n=$(git rev-list --count --stdin \"$r\" </tmp/on-remote 2>/dev/null) || n=?\n [ \"$n\" = 0 ] || echo \"$r: $n commit(s) on no remote\"\n done\n ```\n\n (A commit the remote has that you have not fetched is not named here; `--stdin` skips\n nothing, so fetch first. A commit in `FETCH_HEAD` counts only when it came from the\n remote's own URL: `git fetch . <branch>` writes one too.)\n\n5. **Map each of those branches to its PR state.** Merged or closed counts as published:\n\n ```bash\n gh pr list -R <owner>/<repo> --head <branch> --state all --json number,state,title\n ```\n\n `hygiene.sh` asks `gh api repos/<owner>/<repo>/pulls?head=<owner>:<branch>`. That never finds\n a PR from a fork, so it then asks for the PRs associated with the branch's tip commit\n (`commits/<sha>/pulls`), and then scans the latest 100 PRs for one whose head ref is the\n branch and whose head commit is the tip or behind it. A tip that is an open PR's head is\n pushed, even when no remote-tracking ref has it.\n\n **A squash merge makes \"not on any remote\" meaningless as proof.** Squashing writes a new\n commit on the base branch, so the branch's own commits stay local-only even though the\n work shipped. Then the remote branch is deleted, and every squash-merged branch looks\n unpublished. So check in this order:\n - Is every commit on a remote? Then it is published.\n - Is there a PR from this branch that is `MERGED` or `CLOSED`, **and is the branch's tip that\n PR's head, or behind it**? Then it is published (merged, or deliberately abandoned). A\n commit made after the PR's head is not in the PR, whatever its state.\n - Is every unpublished commit already on a remote as **the same change**\n (`git patch-id --stable`, as `git cherry` compares; a rebase or a one-commit squash keeps\n the change)? Then it is published. A matching commit subject proves nothing: two\n different changes can carry one subject.\n - Otherwise it is **unpublished**. This includes an `OPEN` PR whose branch is behind your\n local commits.\n\n6. **Publish or report anything genuinely unpublished.** Push the branch and open a PR (into\n `staging` where the repo has one), or report it to your supervisor with the branch name,\n the commit subjects and why it was not published. Don't delete work to make a clone\n \"clean\".\n7. **Delete the clone, and only your own.** Delete it only after steps 1 to 6 come back\n clean, and then report what was freed. Never delete another agent's folder, even an empty\n one, even in a shared parent.\n\n`hygiene.sh close <clone> --apply` runs steps 1 to 5 and 7. It refuses, before it fetches or\nreads anything, a clone whose `.git/hygiene-owner` names another agent, or any marked clone when\nyou have given no agent id. A clone nested inside yours,\nsuch as a gitignored cache clone, is deleted with it, so it is audited too: unpublished work\nthere keeps the whole clone. It exits `2`, and deletes\nnothing, when step 6 is needed. A clone kept only because a worktree of it, or of a clone nested\nin it, is still checked out is not step 6: `close` exits `1` and says to close that worktree\nfirst. A submodule belongs to its parent, whatever the shape of its `.git` (a file pointing\ninto the parent's `.git/modules/`, or a folder, when `git submodule add` adopted a repository\nthat was already there). The script asks the parent: git's superproject, or a gitlink or\n`.gitmodules` path for it. It is audited as part of the parent, and `close` refuses it on its\nown.\n\n**Nothing goes with a folder unless everything in it may go.** Before a clone is deleted\n(`rm -rf`) or a worktree removed (`git worktree remove`, which also deletes everything inside\nit), the script finds every `.git` entry inside it, in gitignored folders, `node_modules` and\n`target` too, and gates each checkout the same way. Git has to read it as its own repository,\nits git dir has to be inside the folder, it may not be locked, and its own audit has to be clean\nand idle. A worktree of a clone outside the folder, such as a cache holding a\n`git worktree add` of `~/GitHub/live`, is never deleted with it: `git worktree remove` it\nagainst its own clone first, or report it. Another agent's clone inside it holds it the same\nway (`close` exits `1`: report it). Any refusal keeps the whole folder, with a reason that names\nthe checkout inside.\n\n- **A `.git` git does not read as its own** (a `.git` folder that lost its `HEAD`, a git dir that\n is gone) is kept for a human. Git skips such a folder and answers for the repository around\n it, so the script checks that the git dir git resolves is the checkout's own and that the\n checkout is its top level. `close` audits it and exits `2`, as `sweep` does.\n- **Git dirs not named `.git`** are gated too: a bare repository or mirror (`foo.git/`) and a\n deinit'd submodule's `.git/modules/<name>`. One goes only when it has no stash and every\n commit any of its refs or `HEAD` reaches is named by a remote it can reach (`git ls-remote`),\n never by its own remote-tracking refs. One with no reachable remote and any commit is kept:\n delete it by hand once you know it holds nothing of yours.\n- **A checkout found inside another runs nothing its config names.** A package that ships a\n `.git` in `node_modules`, a cache clone or a bare mirror has config anyone could have written,\n and `git ls-remote`, `git fetch` or even `git status` with it can run a command (an `ext::`\n remote URL, `core.sshCommand`, `remote.<name>.uploadpack`, `core.fsmonitor`, a filter). So the\n script never fetches into one, and asks its remotes only by URL (https or ssh, from an empty\n repository with every other protocol off), alongside what the clone around it was told by its\n own remotes. Any config key outside a short list of inert ones (`core.*` basics,\n `remote.*.url`/`fetch`, `branch.*`, `submodule.*.url`, …) keeps it unread, as `KEEP`\n (\"untrusted git config\"). Every git the script runs has `core.fsmonitor`, hooks, signature\n checks, the `ext`/`fd` transports and lazy fetches switched off, and no git runs on a scanned\n checkout at all until its config (and any `commondir`, which is followed, or refused for a\n bare git dir) has passed; a `core.worktree` pointing outside it or an\n `objects/info/alternates` keeps it. A `commondir` and a `.git` gitfile are\n read exactly as git reads them (the whole file, only a trailing `\\n`/`\\r`\n stripped); one with an embedded newline or CR, a NUL, or a byte outside\n printable ASCII is refused rather than parsed two ways, and a malformed\n `.git` file keeps its folder instead of being skipped. A registered submodule of your own clone is trusted while\n its config is inert.\n- **The dry run counts each byte once.** A sweep measures every checkout once before anything\n goes, and a checkout inside another is not counted again with the folder around it. The total\n is shown in exact KB, and it is what `--apply` frees.\n\n## The periodic sweep\n\nRun a sweep when `check` fails, before a large fan-out, and at the end of a long supervisor\nsession. It has three passes, and the dry run comes first:\n\n1. **Safe prune of regenerable folders.** These are `node_modules`, `target`, `.astro`,\n `.wrangler`, `.turbo` and `.next`, removed **only when older than N minutes** (default 120),\n so an agent mid-build is not hit. A sibling manifest has to prove what each folder is:\n `Cargo.toml` (or the cargo `CACHEDIR.TAG`) for `target`, `package.json` for `.next`,\n `.turbo` and `.astro`, and a `wrangler` config for `.wrangler`. A folder named `target` that\n holds notes is not a build cache. Pruning loses nothing, because the next install or build\n recreates the folder. A folder with a git checkout inside it (a `.git` anywhere in it) is\n skipped, since its checkout is not regenerable.\n2. **Orphaned tool temp folders** (`hygiene.sh tmp`, also run on its own) in `${TMPDIR:-/tmp}`\n (or `--tmp DIR`). A folder there is judged by its contents, not only its name, and only\n folders directly in the temp directory, owned by you and untouched for N minutes, are\n candidates:\n - **Vitest/Vite module copies:** a random name (16 to 32 letters, digits, `_` or `-`) holding\n only `client/` and/or `ssr/`. Each of those holds only files named by a 40-hex hash, and\n the files are Vite SSR module code.\n - **npm pack leftovers:** a `tmp.*` folder (from `mktemp -d`) holding only `*.tgz` files\n and/or an unpacked `package/` with a `package.json`.\n\n It never touches `claude-*` (Claude Code's temp and the session's scratchpad), browser\n profiles (`.org.chromium.*`, `*chrom*`, `puppeteer*`, `playwright*`, `*profile*`), symlinks,\n another user's folders, a folder with a git checkout inside it, or an `--exclude` path. It\n reports `node-compile-cache` (Node's\n shared compile cache) with its size and never removes it; delete it by hand when no `node`\n process runs. A stray clone in `/tmp` is a clone: `hygiene.sh audit --root /tmp` finds it.\n\n3. **Audit report of the clones and worktrees, then delete the clean ones.** Each gets one\n verdict:\n - `CLEAN`: nothing unpublished, idle. Deleted with `--apply`.\n - `KEEP`: unpublished work, with the reason. Never deleted: publish it or report it.\n - `ACTIVE`: a file changed inside the window. Another session is probably using it. Leave\n it.\n - `OTHER`: its `.git/hygiene-owner` names another agent. Not audited, not pruned, not deleted.\n\nThe sweep judges worktrees before clones. Removing a clean worktree frees its clone, which is\nthen judged without it (in a dry run, as if it were gone), so a clone is never reported as kept\nonly because the sweep had not reached its worktree yet. A clone held only by a worktree that is\nstill in use (active, or another agent's) is left in place and reported, but it is not counted\nas kept and does not make the sweep exit `2`. The dry run predicts `--apply` exactly: a worktree\nthat `git worktree remove` would refuse (locked, holding a submodule) is not counted as removed,\nso the clone it holds is not reported as deletable either. A submodule is never listed on its\nown; it goes only with its parent.\n\nA worktree is deleted with `git worktree remove`, never `rm -rf`. Prune, close and sweep leave\na clone or worktree another agent owns alone. A supervisor clearing up after\nagents that have finished passes `--force-owner`, which still deletes only `CLEAN`, idle\nclones and is still a dry run without `--apply`. A clone with no marker (made by hand, or\nbefore markers) is judged as before.\n\n```bash\nbash hygiene.sh sweep --exclude \"${TMPDIR:-/tmp}/<your-task>\" # read the report\nbash hygiene.sh sweep --exclude \"${TMPDIR:-/tmp}/<your-task>\" --apply # then act on it\n```\n\nReport the result: what was freed, how many clones were deleted, and every `KEEP` with its\nreason.\n\n## Safety rules (all of the above)\n\n- **Never `rm -rf` a path built from an unchecked variable.** If `$DIR` is empty or unset,\n `rm -rf \"$DIR/\"*` deletes from `/`. Use fixed base paths and let `find … -prune` produce the\n paths. Check every path before you delete it: it is absolute, it is inside the root, it is not\n the root itself, `/` or `$HOME`, and it is not under `~/GitHub`. Then use\n `rm -rf -- \"$path\"`. `hygiene.sh` routes every deletion through that check.\n- **Never touch `~/GitHub` working copies, `claude-*` temp folders, browser profiles, or another\n agent's folders.** The owner keeps live\n work in `~/GitHub`. The script refuses a root or a clone there. A clone you did not create\n is someone else's until they or your supervisor say otherwise: never cd into it, check it\n out, fetch into it, build in it or delete it. The script refuses a clone whose\n `.git/hygiene-owner` is not yours unless you pass `--force-owner`. Use `--exclude` for your own\n live clone.\n- **Never delete `staging`,** neither the branch on the remote nor a release PR's head branch.\n A clone is deleted locally; branches on the remote are the repository's business, not the\n sweep's.\n- **Delete only under a root you named, and never through a symlink.** In a temp folder other\n users can write in (`/tmp`, and often `$TMPDIR`), a path checked now and deleted later can be\n redirected: another user swaps a folder for a symlink in between. So `prune`, `close` and\n `sweep` with `--apply` refuse the fallback root unless it is a folder only you can write in;\n set `HYGIENE_ROOT` (or pass `--root`) to the folder your clones live in, which in a cloud\n session is `export HYGIENE_ROOT=\"${TMPDIR:-/tmp}\"`. Every deletion checks that each folder\n from the root down is a real folder you own, not a symlink, then deletes from inside the\n parent it entered with `cd -P` (agent-tools#279).\n- **Run nothing a repository's config names.** In a root other users can write in, a checkout\n is trusted only when `clone` or `worktree` marked it (`hygiene-owner`) and the marker, the\n checkout and its git dir are yours and writable by nobody else; anyone can plant a repository\n there, marker included. Trusted or not, no git runs in a checkout until every key in its\n config is inert (no `credential.helper`, `uploadpack`, `sshCommand`, `hooksPath` or filter)\n and its `hooks/` holds nothing but git's `*.sample` files: a checkout that fails is kept for\n a human. The remote calls reset\n `credential.helper` to your own helpers and turn the file transport off; set\n `HYGIENE_FILE_REMOTES=1` only for a local mirror you trust (agent-tools#279).\n- **Dry run first, then `--apply`.** Read what would go before anything goes.\n- **Report what was freed,** in GB, with the number of folders and clones and every clone that\n was kept and why. \"Cleaned up\" is not a report.\n\n## Checklist\n\n- [ ] `df -h /` before starting. On a shared host, 50 GB free or more before any fan-out. In a\n cloud session (`CLAUDE_CODE_REMOTE=true`), `hygiene.sh check` passes, and you have 10 GB\n free above its threshold per concurrent heavy build.\n- [ ] Every path is written `\"${TMPDIR:-/tmp}/…\"`, never a bare `\"$TMPDIR/…\"`.\n- [ ] Cloned into `${TMPDIR:-/tmp}/<task>-<repo>-<short-id>` (`hygiene.sh clone`), or a marked\n worktree (`hygiene.sh worktree`; in a cloud session under `HYGIENE_ROOT`), unique to you;\n never `~/GitHub`, never a folder another agent made.\n- [ ] PR reviews in a throwaway detached worktree, not `pr<N>` branches in a shared clone.\n- [ ] `cargo clean -p <crate>` before the final test run of a crate you changed, when\n `CARGO_TARGET_DIR` is shared.\n- [ ] `git status`, `git branch --show-current` and the upstream checked before every commit.\n- [ ] `CARGO_TARGET_DIR` shared; pnpm store and npm cache not redirected into the clone.\n- [ ] No build output committed.\n- [ ] On close: fetched, then checked the status, the stashes, and the branches on no remote.\n PR state looked up per branch, because of squash merges.\n- [ ] Unpublished work pushed into a PR, or reported.\n- [ ] Your own clone deleted, or your worktree removed with `git worktree remove`, nobody\n else's, and the amount freed reported.\n- [ ] `hygiene.sh tmp` run after test runs; orphaned tool temp folders removed.",
229
229
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/digital-hygiene",
230
230
  "files": [
231
231
  {
@@ -241,7 +241,7 @@ export const BUNDLED_SKILLS = [
241
241
  "name": "progress-report",
242
242
  "category": "dev",
243
243
  "description": "Use this skill whenever dev work starts, in any repo. All dev work runs on a 10-minute progress-report loop. When work starts, schedule a recurring report every 10 minutes (`/loop 10m` or CronCreate `*/10 * * * *`; the cron dies with the session; without cron, report at each checkpoint). Measure, don't guess: each report has 20-cell progress bars with a percentage and a one-line basis (plan steps, features, code migrated as lines, and an Overall bar labelled estimate when not exact) and per workstream open PRs, latest release, staging ahead of main, CI, and a live HTTP check for deployed sites. It ends with one 'Since last report:' line and a final 'Needs you:' line. A \"what's blocking / what's live\" status gives each product one line: live value, what waits, who acts. When nothing changed, report a single line. Ticks are read-only: never publish, release, merge, delete or deploy from a tick unless the owner approved that exact action. No secrets or PII. Ships progress.sh (bar, pct, repo, lines, http).",
244
- "body_mdx": "# Progress report: measured, every ten minutes\n\n> **Covenant:** _The owner should never have to ask \"where are we?\"_\n\nAll dev work runs on a 10-minute progress-report loop. The owner reads the reports to decide\nwhat to do next, so every number in them is measured, and every report ends with what the\nowner has to do. A report that guesses is worse than none, because it is believed.\n\nThis skill has five parts: **starting** the loop, **what a report holds**, the **\"what's\nblocking / what's live\" status**, **the rules for a tick**, and **stopping**. The script\n`progress.sh` in this skill folder renders the bars and reads the repo state for you.\n\n## Get the script\n\n`progress.sh` is portable bash (3.2+) for macOS and Linux. It is **read-only**: it reads git,\nthe GitHub API (through `gh`) and HTTP status codes, and never writes anything.\n\n| Where you are | Path |\n| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/progress-report/progress.sh` |\n| The public `mzizi` plugin | `skills/progress-report/progress.sh` in the plugin |\n| Only the MCP | `curl -fsSL https://unpkg.com/@nyuchi/mzizi-skills/skills/progress-report/progress.sh -o \"${TMPDIR:-/tmp}/progress.sh\"` |\n\n```bash\nP=\"${TMPDIR:-/tmp}/progress.sh\" # or one of the paths above\nbash \"$P\" bar 78 \"Plan steps\" \"7 of 9\" # Plan steps ████████████████░░░░ 78% 7 of 9\nbash \"$P\" pct 7 9 # 78\nbash \"$P\" repo mzizi-dev/agent-tools # open PRs, latest release, staging vs main, CI\nbash \"$P\" -C \"${TMPDIR:-/tmp}/task/app\" lines origin/staging app/ # lines under app/ at that ref\nbash \"$P\" http https://mcp.mzizi.dev/ https://mzizi.dev # status code per deployed site\n```\n\n`lines` counts the committed tree at a ref (`git grep -c ''`, binary files skipped), never the\nworking copy, so two reports on the same commit give the same number. `repo` reads only, every\npage of each list. Its CI line takes the newest run of each GitHub Actions workflow, the\nchecks of other apps, and the commit statuses (deploy integrations often post only a status)\non the default branch's head, and names what failed. A workflow whose newest run was\ncancelled is shown as cancelled (`; 1 cancelled: CI`), neither green nor red, because nothing\nfinished it; read that as \"not verified\", never as passing. `pct` and\n`bar` never round unfinished work up to 100%.\n\n## Starting the loop\n\nWhen dev work starts, schedule the report before you write any code:\n\n- **Claude Code:** `/loop 10m <report prompt>`, or CronCreate with `*/10 * * * *`. The cron\n lives in the session: it dies when the session ends and expires after 7 days, so a long build\n re-arms it in each new session.\n- **Surfaces without cron** (a chat, a CI job, an agent with no scheduler): report at each\n natural checkpoint instead, such as a PR opened, CI finished, a merge, a release, or a\n blocker.\n\nThe report prompt names the work and where its measures come from, for example:\n\n```text\nProgress report for the Toddle rebuild (nyuchi/toddle-enhancement-extension#41).\nBars: plan steps from the checklist in #41; features from the feature table in docs/plan.md;\ncode migrated = lines in app/ vs src/ at origin/staging. Workstreams: the repo, the Chrome Web\nStore listing. Use the progress-report skill. Read-only.\n```\n\nWrite the measures down once, in the tracking issue (see \"Track big work in GitHub issues\").\nThen every tick measures the same thing the same way.\n\n## What a report holds\n\n### 1. Progress bars\n\nEach bar is 20 cells of `█` and `░`, a percentage, and a **one-line basis**: what was counted.\nUse `progress.sh bar` so every bar has the same width and alignment. Typical bars:\n\n| Bar | Basis (measured) |\n| ------------- | ------------------------------------------------------------------------------------ |\n| Plan steps | Steps done of total, from the repo's plan doc or the tracking issue's checklist. |\n| Features done | Features shipped of the features listed in the plan. |\n| Code migrated | Lines in the new tree against the old (`progress.sh lines`), at the same ref. |\n| Overall | How it combines the others. Labelled `(estimate)` and shown as `~N%` when not exact. |\n\nOnly show a bar you can measure. If there is no checklist, say so and don't invent a\ndenominator. A bar never reads 100% for unfinished work: `progress.sh` keeps a cell empty\nuntil the count is complete.\n\n### 2. Per workstream\n\nFor each repo, service or content stream the work touches:\n\n- **Open PRs**, the **latest release or tag**, **staging ahead of main**, and **CI** on the\n default branch: `progress.sh repo <owner/repo>`.\n- **Deployed sites:** a live HTTP check (`progress.sh http <url>…`). Report the status code\n you got; don't call a site live without one.\n- **Content**, when the work includes it (Intercom articles, Sanity documents, store\n listings): the **published** count against the **draft** count, read from the source's API\n or MCP.\n\n### 3. Since last report\n\nOne line: what changed since the previous tick (a PR merged, CI went green, a bar moved from\n68% to 74%).\n\n### 4. Needs you\n\nThe **last line** of every report: `Needs you:` and the owner actions and decisions that block\nthe work, each one specific enough to act on. Write `Needs you: nothing` when nothing does.\n\n### When nothing changed\n\nIf nothing changed since the last tick, the whole report is one line, such as\n`10:40 no change since 10:30 (PR #212 CI still running). Needs you: nothing.`\n\n## Example\n\nThe Toddle rebuild format, partway through (the numbers are illustrative):\n\n```text\nProgress report 14:20 (Toddle rebuild, nyuchi/toddle-enhancement-extension#41)\n\nFeatures rebuilt (UI) ████████████████████ 100% every visible feature is React in app/\nPlan steps ████████████████░░░░ 78% 7 of 9\nCode in app/ ██████████████░░░░░░ 69% 16,090 app/ · 7,266 src/\n───────────────────────────────────────────────\nOverall (estimate) ███████████████░░░░░ ~75% plan steps, weighted by code moved\n\nnyuchi/toddle-enhancement-extension\n open PRs 2 (#58 settings panel, #59 remove src/)\n latest release v1.6.0 (2026-10-05)\n staging vs main 4 ahead, 0 behind\n CI on main green (9 checks)\nChrome Web Store 1.6.0 published, 1.7.0 draft staged\n\nSince last report: #57 merged; code in app/ 64% → 69%.\nNeeds you: approve the 1.7.0 store submission (staged, not submitted).\n```\n\n## Status: what's blocking, what's live\n\nUse this format when the owner asks \"what's blocking?\" or \"what's live?\", when work crosses\nseveral products, or when a release is in flight. It can stand in for the per-workstream block\nof a tick, or follow it. Its one rule: **merged to `staging` is not live.** A product is live\nwhen you read its live value this minute, and deploys lag a minute or two behind a merge to\n`main`, so say \"not yet\" before you say \"stale\".\n\nOne line per product, with three parts: **live value**, **waiting**, **who acts**.\n\n- **Live value:** what the product serves now, read from its own source in this tick (a\n version, a registry commit, an HTTP status), or `?` and why. Never a value from an earlier\n tick or from a merged PR.\n- **Waiting:** what stands between here and the target, specific enough to check, such as\n `PR #120 in the merge queue`, `release PR #340 open` or `npm lists 0.17.0, not 0.18.0`.\n Write `nothing` when the live value is the target.\n- **Who acts:** `me` (the agent, within what the owner approved), `CI`, `bot`, `deploy` (a\n Cloudflare deploy, which lags), or `owner` for a decision only the owner can make: a\n release, the `forward-ported` label, a major version. Name a person only by role.\n\n```text\nStatus 14:20 (registry 4.25.0 to every product) target: registry=c3e6e87c49ee\n\nmzizi-api live registry=1ac4f81a0571 waiting: bump PR #121 in the merge queue, then release who: CI, then owner\nmzizi-mcp live registry=1ac4f81a0571 waiting: bump PR open, parity not run who: me\n@bundu/ui live 0.10.0, pin 780457a waiting: registry:sync not run who: me\nmzizi-site live 200, counts for 675 waiting: the API release first who: owner (release)\nmzizi-docs live 200 waiting: nothing who: -\n\nBlocking: the API release (release PR not cut; the main gate may need the owner's forward-ported label).\nSince last report: API bump PR moved from open to queued.\nNeeds you: say whether to cut the API release now, and whether forward-ported is approved for it.\n```\n\n(The values above are illustrative: read yours from the live commands. `ship-release`'s\n`verify-live.sh` prints most of them, and `registry-propagation` has the alignment table.)\n\nRules for this status:\n\n- **One line per product**, plus a `Blocking:` line naming the single thing that, once\n cleared, moves the most lines. If nothing blocks, write `Blocking: nothing`.\n- **Honest wording.** `merged` and `queued` are not `live`. Say `live` only with a value read\n now; say `not yet` for what a deploy has not reached; say `?` when a read failed.\n- **End with `Needs you:`**, the owner's actions and decisions, each one specific enough to act on\n (`Needs you: nothing` when nothing is). It is still the last line of the report.\n- Still read-only: reading the live state never publishes, releases, merges or deploys.\n\n## Rules for a tick\n\n- **Ticks are read-only by default.** Never publish, release, merge, delete or deploy from a\n report tick unless the owner already approved that specific action. A report observes the\n work; it does not do it. Installing an approved test build is fine when the owner set that\n up.\n- **Measure, don't guess.** Every number comes from a command, an API or a file you read in\n this tick. If a read fails, say `?` and why, rather than reusing an old number.\n- **Privacy.** No secrets, tokens or credential status, and no personal information (names of\n members of the public, emails, phone numbers) in a report. Name a secret by its name, never\n its value.\n- **Short.** A report is read on a phone between other things. Bars, workstreams, one \"Since\n last report\" line, then \"Needs you\".\n\n## Stopping\n\nThe loop stops when:\n\n- **the work is done:** send one final report with the outcome (what merged, what is live and\n verified, what is left) and its \"Needs you\" line, then cancel the loop; or\n- **the owner says stop.**\n\nCancel the scheduled job (`CronDelete`, or end the `/loop`) so nothing keeps reporting on\nfinished work.\n\n## Checklist\n\n- [ ] The loop is scheduled (10 minutes) or, without cron, checkpoints are named.\n- [ ] Each bar has a measured basis; the Overall bar says `estimate` when it is one.\n- [ ] Each workstream shows PRs, release, staging vs main, CI, live HTTP, and content counts.\n- [ ] One \"Since last report:\" line, and \"Needs you:\" is the last line.\n- [ ] A \"what's blocking / what's live\" status has one line per product (live value, waiting, who acts), says live only for a value read this tick, and keeps merged and live apart.\n- [ ] Nothing published, released, merged, deleted or deployed by a tick without approval.\n- [ ] No secrets, no PII.\n- [ ] A final report when the work is done, and the loop cancelled.",
244
+ "body_mdx": "# Progress report: measured, every ten minutes\n\n> **Covenant:** _The owner should never have to ask \"where are we?\"_\n\nAll dev work runs on a 10-minute progress-report loop. The owner reads the reports to decide\nwhat to do next, so every number in them is measured, and every report ends with what the\nowner has to do. A report that guesses is worse than none, because it is believed.\n\nThis skill has five parts: **starting** the loop, **what a report holds**, the **\"what's\nblocking / what's live\" status**, **the rules for a tick**, and **stopping**. The script\n`progress.sh` in this skill folder renders the bars and reads the repo state for you.\n\n## Get the script\n\n`progress.sh` is portable bash (3.2+) for macOS and Linux. It is **read-only**: it reads git,\nthe GitHub API (through `gh`) and HTTP status codes, and never writes anything.\n\n| Where you are | Path |\n| ------------------------------- | ------------------------------------------------------------------------------------------ |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/progress-report/progress.sh` |\n| The `mzizi-dev` plugin | `skills/progress-report/progress.sh` in the plugin |\n| Only the MCP | Install the package, then use the first path: `npm install --no-save @nyuchi/mzizi-skills` |\n\n```bash\nP=\"${TMPDIR:-/tmp}/progress.sh\" # or one of the paths above\nbash \"$P\" bar 78 \"Plan steps\" \"7 of 9\" # Plan steps ████████████████░░░░ 78% 7 of 9\nbash \"$P\" pct 7 9 # 78\nbash \"$P\" repo mzizi-dev/agent-tools # open PRs, latest release, staging vs main, CI\nbash \"$P\" -C \"${TMPDIR:-/tmp}/task/app\" lines origin/staging app/ # lines under app/ at that ref\nbash \"$P\" http https://mcp.mzizi.dev/ https://mzizi.dev # status code per deployed site\n```\n\n`lines` counts the committed tree at a ref (`git grep -c ''`, binary files skipped), never the\nworking copy, so two reports on the same commit give the same number. `repo` reads only, every\npage of each list. Its CI line takes the newest run of each GitHub Actions workflow, the\nchecks of other apps, and the commit statuses (deploy integrations often post only a status)\non the default branch's head, and names what failed. A workflow whose newest run was\ncancelled is shown as cancelled (`; 1 cancelled: CI`), neither green nor red, because nothing\nfinished it; read that as \"not verified\", never as passing. `pct` and\n`bar` never round unfinished work up to 100%.\n\n## Starting the loop\n\nWhen dev work starts, schedule the report before you write any code:\n\n- **Claude Code:** `/loop 10m <report prompt>`, or CronCreate with `*/10 * * * *`. The cron\n lives in the session: it dies when the session ends and expires after 7 days, so a long build\n re-arms it in each new session.\n- **Surfaces without cron** (a chat, a CI job, an agent with no scheduler): report at each\n natural checkpoint instead, such as a PR opened, CI finished, a merge, a release, or a\n blocker.\n\nThe report prompt names the work and where its measures come from, for example:\n\n```text\nProgress report for the Toddle rebuild (nyuchi/toddle-enhancement-extension#41).\nBars: plan steps from the checklist in #41; features from the feature table in docs/plan.md;\ncode migrated = lines in app/ vs src/ at origin/staging. Workstreams: the repo, the Chrome Web\nStore listing. Use the progress-report skill. Read-only.\n```\n\nWrite the measures down once, in the tracking issue (see \"Track big work in GitHub issues\").\nThen every tick measures the same thing the same way.\n\n## What a report holds\n\n### 1. Progress bars\n\nEach bar is 20 cells of `█` and `░`, a percentage, and a **one-line basis**: what was counted.\nUse `progress.sh bar` so every bar has the same width and alignment. Typical bars:\n\n| Bar | Basis (measured) |\n| ------------- | ------------------------------------------------------------------------------------ |\n| Plan steps | Steps done of total, from the repo's plan doc or the tracking issue's checklist. |\n| Features done | Features shipped of the features listed in the plan. |\n| Code migrated | Lines in the new tree against the old (`progress.sh lines`), at the same ref. |\n| Overall | How it combines the others. Labelled `(estimate)` and shown as `~N%` when not exact. |\n\nOnly show a bar you can measure. If there is no checklist, say so and don't invent a\ndenominator. A bar never reads 100% for unfinished work: `progress.sh` keeps a cell empty\nuntil the count is complete.\n\n### 2. Per workstream\n\nFor each repo, service or content stream the work touches:\n\n- **Open PRs**, the **latest release or tag**, **staging ahead of main**, and **CI** on the\n default branch: `progress.sh repo <owner/repo>`.\n- **Deployed sites:** a live HTTP check (`progress.sh http <url>…`). Report the status code\n you got; don't call a site live without one.\n- **Content**, when the work includes it (Intercom articles, Sanity documents, store\n listings): the **published** count against the **draft** count, read from the source's API\n or MCP.\n\n### 3. Since last report\n\nOne line: what changed since the previous tick (a PR merged, CI went green, a bar moved from\n68% to 74%).\n\n### 4. Needs you\n\nThe **last line** of every report: `Needs you:` and the owner actions and decisions that block\nthe work, each one specific enough to act on. Write `Needs you: nothing` when nothing does.\n\n### When nothing changed\n\nIf nothing changed since the last tick, the whole report is one line, such as\n`10:40 no change since 10:30 (PR #212 CI still running). Needs you: nothing.`\n\n## Example\n\nThe Toddle rebuild format, partway through (the numbers are illustrative):\n\n```text\nProgress report 14:20 (Toddle rebuild, nyuchi/toddle-enhancement-extension#41)\n\nFeatures rebuilt (UI) ████████████████████ 100% every visible feature is React in app/\nPlan steps ████████████████░░░░ 78% 7 of 9\nCode in app/ ██████████████░░░░░░ 69% 16,090 app/ · 7,266 src/\n───────────────────────────────────────────────\nOverall (estimate) ███████████████░░░░░ ~75% plan steps, weighted by code moved\n\nnyuchi/toddle-enhancement-extension\n open PRs 2 (#58 settings panel, #59 remove src/)\n latest release v1.6.0 (2026-10-05)\n staging vs main 4 ahead, 0 behind\n CI on main green (9 checks)\nChrome Web Store 1.6.0 published, 1.7.0 draft staged\n\nSince last report: #57 merged; code in app/ 64% → 69%.\nNeeds you: approve the 1.7.0 store submission (staged, not submitted).\n```\n\n## Status: what's blocking, what's live\n\nUse this format when the owner asks \"what's blocking?\" or \"what's live?\", when work crosses\nseveral products, or when a release is in flight. It can stand in for the per-workstream block\nof a tick, or follow it. Its one rule: **merged to `staging` is not live.** A product is live\nwhen you read its live value this minute, and deploys lag a minute or two behind a merge to\n`main`, so say \"not yet\" before you say \"stale\".\n\nOne line per product, with three parts: **live value**, **waiting**, **who acts**.\n\n- **Live value:** what the product serves now, read from its own source in this tick (a\n version, a registry commit, an HTTP status), or `?` and why. Never a value from an earlier\n tick or from a merged PR.\n- **Waiting:** what stands between here and the target, specific enough to check, such as\n `PR #120 in the merge queue`, `release PR #340 open` or `npm lists 0.17.0, not 0.18.0`.\n Write `nothing` when the live value is the target.\n- **Who acts:** `me` (the agent, within what the owner approved), `CI`, `bot`, `deploy` (a\n Cloudflare deploy, which lags), or `owner` for a decision only the owner can make: a\n release, the `forward-ported` label, a major version. Name a person only by role.\n\n```text\nStatus 14:20 (registry 4.25.0 to every product) target: registry=c3e6e87c49ee\n\nmzizi-api live registry=1ac4f81a0571 waiting: bump PR #121 in the merge queue, then release who: CI, then owner\nmzizi-mcp live registry=1ac4f81a0571 waiting: bump PR open, parity not run who: me\n@bundu/ui live 0.10.0, pin 780457a waiting: registry:sync not run who: me\nmzizi-site live 200, counts for 675 waiting: the API release first who: owner (release)\nmzizi-docs live 200 waiting: nothing who: -\n\nBlocking: the API release (release PR not cut; the main gate may need the owner's forward-ported label).\nSince last report: API bump PR moved from open to queued.\nNeeds you: say whether to cut the API release now, and whether forward-ported is approved for it.\n```\n\n(The values above are illustrative: read yours from the live commands. `ship-release`'s\n`verify-live.sh` prints most of them, and `registry-propagation` has the alignment table.)\n\nRules for this status:\n\n- **One line per product**, plus a `Blocking:` line naming the single thing that, once\n cleared, moves the most lines. If nothing blocks, write `Blocking: nothing`.\n- **Honest wording.** `merged` and `queued` are not `live`. Say `live` only with a value read\n now; say `not yet` for what a deploy has not reached; say `?` when a read failed.\n- **End with `Needs you:`**, the owner's actions and decisions, each one specific enough to act on\n (`Needs you: nothing` when nothing is). It is still the last line of the report.\n- Still read-only: reading the live state never publishes, releases, merges or deploys.\n\n## Rules for a tick\n\n- **Ticks are read-only by default.** Never publish, release, merge, delete or deploy from a\n report tick unless the owner already approved that specific action. A report observes the\n work; it does not do it. Installing an approved test build is fine when the owner set that\n up.\n- **Measure, don't guess.** Every number comes from a command, an API or a file you read in\n this tick. If a read fails, say `?` and why, rather than reusing an old number.\n- **Privacy.** No secrets, tokens or credential status, and no personal information (names of\n members of the public, emails, phone numbers) in a report. Name a secret by its name, never\n its value.\n- **Short.** A report is read on a phone between other things. Bars, workstreams, one \"Since\n last report\" line, then \"Needs you\".\n\n## Stopping\n\nThe loop stops when:\n\n- **the work is done:** send one final report with the outcome (what merged, what is live and\n verified, what is left) and its \"Needs you\" line, then cancel the loop; or\n- **the owner says stop.**\n\nCancel the scheduled job (`CronDelete`, or end the `/loop`) so nothing keeps reporting on\nfinished work.\n\n## Checklist\n\n- [ ] The loop is scheduled (10 minutes) or, without cron, checkpoints are named.\n- [ ] Each bar has a measured basis; the Overall bar says `estimate` when it is one.\n- [ ] Each workstream shows PRs, release, staging vs main, CI, live HTTP, and content counts.\n- [ ] One \"Since last report:\" line, and \"Needs you:\" is the last line.\n- [ ] A \"what's blocking / what's live\" status has one line per product (live value, waiting, who acts), says live only for a value read this tick, and keeps merged and live apart.\n- [ ] Nothing published, released, merged, deleted or deployed by a tick without approval.\n- [ ] No secrets, no PII.\n- [ ] A final report when the work is done, and the loop cancelled.",
245
245
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/progress-report",
246
246
  "files": [
247
247
  {
@@ -257,23 +257,23 @@ export const BUNDLED_SKILLS = [
257
257
  "name": "ship-release",
258
258
  "category": "dev",
259
259
  "description": "Use this skill whenever you release a Mzizi repo, meaning staging goes to main and the change goes live (npm, Cloudflare Workers, docs.mzizi.dev, mzizi.dev). It is the runbook: cut the release branch off origin/main with staging's exact tree in one commit and prove the tree hashes are equal, the PR title and the issue line in its body, the version rule (next minor above npm, a CHANGELOG section per bumped version, majors need the owner), the main-gate failure about commits that staging may not have and its owner-only forward-ported label (never --admin), merge queue methods (GraphQL is blocked in cloud sessions, use the REST route), stale checks, a publish that npm lists late, immutable releases, and verifying LIVE with commands. Ships verify-live.sh, which only reads. A release is live when the live commands say so, never when it merges.",
260
- "body_mdx": "# Ship a release: staging to main, then prove it is live\n\n> **Covenant:** _A release is done when the live commands say so, not when the PR merges._\n\nReleases go live without sitting around. The owner's direction is to choose the practice that\nkeeps releases moving and every claim checkable: cut, check, queue, publish, verify, report.\nThis skill is the runbook, written from the releases that stalled for the same few reasons.\n\nIt has seven parts: **cut**, **versions**, **the main gate**, **queues**, **stale checks**,\n**publish**, and **verify live**. `verify-live.sh` in this skill folder prints the live state.\n\n**Scope.** Which repos release this way: the org's repos with a `staging` branch that deploys\nor publishes from `main` (agent-tools, mzizi-registry, mzizi-api, mzizi-docs, mzizi-site,\npackages-npm). Each repo's own `AGENTS.md` wins where it differs. Who may release: only the\nowner, or an agent the owner authorised for that release. Without that authority, stop at a\nmerged PR into `staging` and say so in \"Needs you:\".\n\n## Get the script\n\n`verify-live.sh` is portable bash (3.2+) and **read-only**: it reads npm, the GitHub API\n(through `gh`) and HTTP responses, and never publishes, tags or deploys.\n\n| Where you are | Path |\n| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/ship-release/verify-live.sh` |\n| The public `mzizi` plugin | `skills/ship-release/verify-live.sh` in the plugin |\n| Only the MCP | `curl -fsSL https://unpkg.com/@nyuchi/mzizi-skills/skills/ship-release/verify-live.sh -o \"${TMPDIR:-/tmp}/verify-live.sh\"` |\n\n```bash\nV=\"${TMPDIR:-/tmp}/verify-live.sh\" # or one of the paths above\nbash \"$V\" npm @nyuchi/mzizi-skills @nyuchi/mzizi-skills@0.18.0 # latest, and is 0.18.0 listed yet\nbash \"$V\" api # status and X-Mzizi-Source of /v1/brand\nbash \"$V\" mcp # catalogue version, registry commit, evil-origin probe\nbash \"$V\" release mzizi-dev/agent-tools '@nyuchi/mzizi-skills@0.18.0' # tag and release exist (a draft shows draft=true), notes length\nbash \"$V\" http https://docs.mzizi.dev/ https://mzizi.dev/ # status per page\n```\n\n## 1. Cut the release branch\n\nA release PR is `main` plus `staging`'s exact tree in **one commit**. Work in your own clone\n(see the `digital-hygiene` skill), never in another agent's.\n\n```bash\ngit fetch origin main staging\ngit checkout -B release/2026-11-04-skills origin/main # release/<date>-<name>\ngit read-tree --reset -u origin/staging # the index and working tree become staging's\ngit commit -m \"chore(release): staging to main, 2026-11-04 (@nyuchi/mzizi-skills 0.18.0)\"\ntest \"$(git rev-parse 'HEAD^{tree}')\" = \"$(git rev-parse 'origin/staging^{tree}')\" && echo \"trees equal\"\ngit push -u origin release/2026-11-04-skills\n```\n\n- If `git commit` says `nothing to commit`, `main` already holds `staging`'s tree: there is\n nothing to release, and you say so instead of opening a PR.\n- **Title:** `chore(release): staging to main, <date> (<package versions>)`.\n- **Body:** a line that starts with `Refs <owner>/<repo>#n`, `Refs #n`, `Closes #n` or\n `No issue: <reason>`, on its own line. A mention in the middle of a sentence does not count.\n Where the repo's `AGENTS.md` asks for release notes in the PR body, paste what the release\n will carry (the CHANGELOG sections), and the `Site/docs/skills impact` heading if it asks.\n- **Never** a merge commit or a rebase here: the point of one commit is that the diff against\n `main` is exactly what `staging` holds. If the trees differ, stop and find out why.\n- After a re-cut or a force-push of the branch, check the labels on the PR again (part 3).\n\n## 2. Versions\n\n- A package version must be the **next minor** above the highest on npm: x.y.z becomes\n x.(y+1).0. Read npm, not the file: `npm view <package> version` (for a list,\n `npm view <package> versions --json`). The release job refuses anything else.\n- **Majors need the owner**, and go only through a manual workflow run with `bump: major`.\n- `CHANGELOG.md` needs a section for **each bumped version** (a `### <package> <version>`\n heading under the release); the release notes are built from it, and a version with no\n section fails the job before `npm publish`.\n- If `package.json` was already bumped for a version that has since reached npm, bump again\n before the release PR, or the job has nothing valid to publish.\n- Do not bump a version a repo's rules leave to tags (a staging patch tag is the workflow's\n job, never yours).\n\n## 3. The main gate and the `forward-ported` label\n\nThe org's main gate (`release-version-check`, which runs `main-gate.sh`) can fail with:\n\n```text\n'main' has 1 commit(s) since the last release that staging may not have; this release would delete them\n```\n\nIt fails because `main`'s own squash or merge commits from earlier releases are never\nancestors of `staging`, so git counts them as commits `staging` may lack. When the trees are\nequal, nothing would be deleted; the documented lane for that is the **`forward-ported`\nlabel** on the release PR.\n\n**That label is an owner decision, made per release.** Add it only when the owner has said so\nfor this release, and only after you have checked both of these:\n\n1. The tree hashes are equal (part 1), and\n2. The main gate is the **only** failing check on the PR:\n\n```bash\nSHA=$(gh api repos/OWNER/REPO/pulls/N --jq .head.sha)\ngh api \"repos/OWNER/REPO/commits/$SHA/check-runs?per_page=100\" --jq '.check_runs[] | [(.conclusion // .status), .name] | @tsv'\n```\n\nThen label the PR and re-run only the failed jobs:\n\n```bash\ngh api -X POST repos/OWNER/REPO/issues/N/labels -f 'labels[]=forward-ported'\ngh api \"repos/OWNER/REPO/actions/runs?head_sha=$SHA&per_page=30\" --jq '.workflow_runs[] | [.id, .name, .conclusion] | @tsv'\ngh api -X POST repos/OWNER/REPO/actions/runs/RUN_ID/rerun-failed-jobs\n```\n\n- **Never `--admin`**, never skip or disable the gate, never merge past a red check. Org admins\n can bypass rulesets; agents never do.\n- If any other check fails, fix that first. The label does not cover it.\n- Prevention: forward-port `main`-only commits into `staging` before cutting, and keep bots\n pointed at `staging`. After the release, sync `main` back into `staging` as the repo's\n `AGENTS.md` says, so the trees match before more work lands. Compare trees, not\n ahead/behind counts.\n\n## 4. Merge queues\n\n- `staging` takes **squash**; `main` takes the method its queue is set to (the owner's note says merge; some repos' `AGENTS.md` say\n squash: read the repo's, and compare trees, not ancestry, afterwards). Do not\n choose a method by hand: enqueue and let the queue use its own.\n- GraphQL is blocked in cloud sessions, so `gh pr merge --auto` fails there. Use the GitHub\n MCP `enable_pr_auto_merge` tool, or the REST route (it is the one the cloud session's own GraphQL error names):\n\n```bash\ngh api -X PUT repos/OWNER/REPO/pulls/N/ccr/auto_merge\ngh api -X DELETE repos/OWNER/REPO/pulls/N/ccr/auto_merge # to take it back\n```\n\n- Enqueue only past the merge gate: the work is complete, CI is green, it is verified at\n runtime, and `/code-review` has run with its findings resolved.\n- A **dequeue with `MERGE_CONFLICT`** means the base moved. Merge `staging` (or `main`, for a\n sync PR) into your branch and resolve. In `CHANGELOG.md`, a conflict under\n `## [Unreleased]` almost always means two PRs each added entries: **keep both sides**.\n Push, then enqueue again.\n\n## 5. Stale checks\n\n`issue attached` reads the PR body from the **event snapshot**, so fixing the body does not\nchange a check that already ran. After you fix the body, save it again to fire a fresh event\n(change it by a character and back, or `gh api -X PATCH repos/OWNER/REPO/pulls/N -F body=@body.md`),\nthen read the new run. The same goes for a label: a check that read the labels before you\nadded one needs a re-run.\n\n## 6. Publish\n\nOn a merge to `main` the release workflow runs `npm publish` (trusted publishing, no token)\nand then a tag step.\n\n- npm can take **minutes** to list a version after `npm publish` prints `+ pkg@version`. The\n tag step can run first, print `not on npm, not tagged`, and make **no tag and no release**.\n Wait until npm lists it (`bash \"$V\" npm <package>@<version>` says `listed`), then re-run the\n workflow run **once**:\n\n```bash\ngh api \"repos/OWNER/REPO/actions/runs?branch=main&per_page=10\" --jq '.workflow_runs[] | [.id, .name, .status, .conclusion] | @tsv'\ngh api -X POST repos/OWNER/REPO/actions/runs/RUN_ID/rerun\n```\n\n- **Releases are immutable.** The workflow creates a release as a draft with its notes, then\n publishes it once. Never edit a published release, never move or delete and re-create a tag,\n never re-upload an asset. A wrong release is corrected by the next version, not by editing\n this one.\n\n## 7. Verify LIVE\n\nMerged is not live. Cloudflare deploys lag a minute or two behind a merge to `main`, so a\nstale answer in the first minutes is not a failure; read again before you report. Check what\nthe release claims, by its own value:\n\n```bash\nnpm view @nyuchi/mzizi-skills version # npm has the version\nbash \"$V\" release mzizi-dev/agent-tools '@nyuchi/mzizi-skills@0.18.0' # tag, release (draft=false) and notes\ncurl -sI https://api.mzizi.dev/v1/brand | grep -i x-mzizi-source # registry=<sha> the API serves\ncurl -s https://mcp.mzizi.dev/catalogue.json | head -c 400 # version, source.registry\ncurl -s -o /dev/null -w '%{http_code}\\n' -X POST -H 'Origin: https://evil.example' \\\n -H 'Content-Type: application/json' -d '{}' https://mcp.mzizi.dev/mcp # 403\ncurl -s -o /dev/null -w '%{http_code}\\n' https://docs.mzizi.dev/ # 200\n```\n\n`bash \"$V\" api`, `bash \"$V\" mcp` and `bash \"$V\" http <url>...` print the same facts without\nthe greps. For a site, the spacing guard runs from a `mzizi-site` checkout (Playwright\ninstalled; `PW_EXECUTABLE` names a Chrome or Chromium binary). Point it at the live URL or a\nCloudflare preview, never a bare `python3 -m http.server` (the site's clean URLs 404 there), and\nwrite its output outside the repo:\n\n```bash\nPW_EXECUTABLE=/opt/pw-browsers/chromium node scripts/audit-ui.mjs --url https://mzizi.dev --guard --out \"${TMPDIR:-/tmp}/audit-out\"\n```\n\nReport each product as **live** only with the value you read this minute, and say what is\nstill waiting (a queued deploy, a bot PR, a registry pin). Use the status format in the\n`progress-report` skill.\n\n## Do not\n\n- Do not use `--admin`, add `forward-ported` without the owner's word for this release, or\n skip or disable a test or gate to go green.\n- Do not cut a release you were not authorised to cut, or promote `staging` while it holds work\n the owner has not approved.\n- Do not edit or move a published tag or release.\n- Do not write a secret, a token or credential status into a PR body, a report or an issue.\n\n## Checklist\n\n- [ ] Branch cut off `origin/main` with `staging`'s tree in one commit; the two tree hashes are equal.\n- [ ] Title is `chore(release): staging to main, <date> (<package versions>)`; the body has an issue line at the start of its own line.\n- [ ] Each bumped version is the next minor above npm and has a CHANGELOG section; no major without the owner.\n- [ ] If the main gate is the only failure and the owner said so: `forward-ported` added, failed jobs re-run, label still there after any re-cut.\n- [ ] Enqueued through the REST route or the MCP tool, past the merge gate; no `--admin`.\n- [ ] Publish confirmed on npm before the one workflow re-run; no release edited.\n- [ ] Verified live with the commands above, a minute or two after the deploy; each product reported with the value read.",
260
+ "body_mdx": "# Ship a release: staging to main, then prove it is live\n\n> **Covenant:** _A release is done when the live commands say so, not when the PR merges._\n\nReleases go live without sitting around. The owner's direction is to choose the practice that\nkeeps releases moving and every claim checkable: cut, check, queue, publish, verify, report.\nThis skill is the runbook, written from the releases that stalled for the same few reasons.\n\nIt has seven parts: **cut**, **versions**, **the main gate**, **queues**, **stale checks**,\n**publish**, and **verify live**. `verify-live.sh` in this skill folder prints the live state.\n\n**Scope.** Which repos release this way: the org's repos with a `staging` branch that deploys\nor publishes from `main` (agent-tools, mzizi-registry, mzizi-api, mzizi-docs, mzizi-site,\npackages-npm). Each repo's own `AGENTS.md` wins where it differs. Who may release: only the\nowner, or an agent the owner authorised for that release. Without that authority, stop at a\nmerged PR into `staging` and say so in \"Needs you:\".\n\n## Get the script\n\n`verify-live.sh` is portable bash (3.2+) and **read-only**: it reads npm, the GitHub API\n(through `gh`) and HTTP responses, and never publishes, tags or deploys.\n\n| Where you are | Path |\n| ------------------------------- | ------------------------------------------------------------------------------------------ |\n| `@nyuchi/mzizi-skills` from npm | `node_modules/@nyuchi/mzizi-skills/skills/ship-release/verify-live.sh` |\n| The `mzizi-dev` plugin | `skills/ship-release/verify-live.sh` in the plugin |\n| Only the MCP | Install the package, then use the first path: `npm install --no-save @nyuchi/mzizi-skills` |\n\nRun it as `bash \"$V\" ...`. It is committed executable (like `hygiene.sh` and `progress.sh`), but\nnpm packs every file as mode 644, so the copy from npm has no execute bit: `bash` works\neverywhere, `./verify-live.sh` only from a git checkout. `bash \"$V\" --help` prints the usage.\n\n```bash\nV=\"${TMPDIR:-/tmp}/verify-live.sh\" # or one of the paths above\nbash \"$V\" npm @nyuchi/mzizi-skills @nyuchi/mzizi-skills@0.19.0 # latest, and is 0.19.0 listed yet\nbash \"$V\" api # status and X-Mzizi-Source of /v1/brand\nbash \"$V\" mcp # catalogue version, registry commit, evil-origin probe\nbash \"$V\" release mzizi-dev/agent-tools '@nyuchi/mzizi-skills@0.19.0' # tag and release exist (a draft shows draft=true), notes length\nbash \"$V\" http https://docs.mzizi.dev/ https://mzizi.dev/ # status per page\n```\n\n## 1. Cut the release branch\n\nA release PR is `main` plus `staging`'s exact tree in **one commit**. Work in your own clone\n(see the `digital-hygiene` skill), never in another agent's.\n\n```bash\ngit fetch origin main staging\ngit checkout -B release/2026-11-04-skills origin/main # release/<date>-<name>\ngit read-tree --reset -u origin/staging # the index and working tree become staging's\ngit commit -m \"chore(release): staging to main, 2026-11-04 (@nyuchi/mzizi-skills 0.19.0)\"\ntest \"$(git rev-parse 'HEAD^{tree}')\" = \"$(git rev-parse 'origin/staging^{tree}')\" && echo \"trees equal\"\ngit push -u origin release/2026-11-04-skills\n```\n\n- If `git commit` says `nothing to commit`, `main` already holds `staging`'s tree: there is\n nothing to release, and you say so instead of opening a PR.\n- **Title:** `chore(release): staging to main, <date> (<package versions>)`.\n- **Body:** a line that starts with `Refs <owner>/<repo>#n`, `Refs #n`, `Closes #n` or\n `No issue: <reason>`, on its own line. A mention in the middle of a sentence does not count.\n Where the repo's `AGENTS.md` asks for release notes in the PR body, paste what the release\n will carry (the CHANGELOG sections), and the `Site/docs/skills impact` heading if it asks.\n- **Never** a merge commit or a rebase here: the point of one commit is that the diff against\n `main` is exactly what `staging` holds. If the trees differ, stop and find out why.\n- After a re-cut or a force-push of the branch, check the labels on the PR again (part 3).\n\n## 2. Versions\n\n- A package version must be the **next minor** above the highest on npm: x.y.z becomes\n x.(y+1).0. Read npm, not the file: `npm view <package> version` (for a list,\n `npm view <package> versions --json`). The release job refuses anything else.\n- **Majors need the owner**, and go only through a manual workflow run with `bump: major`.\n- `CHANGELOG.md` needs an entry for **each bumped version**, and **the heading style is the\n repo's own**: read its `AGENTS.md` (and `CLAUDE.md`) and copy the newest entry. Do not carry\n one repo's heading into another. Read on 4 November 2026:\n\n | Repo | Heading | Where |\n | --------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |\n | `packages-npm` | `## [@bundu/ui 0.12.0] - 2026-11-04` | a dated section per version (its `CLAUDE.md`) |\n | `agent-tools` | `### mzizi-skills 0.19.0 — what changed (2026-11-04, [#343](<issue URL>))` | under `## [Unreleased]`, one `###` per package |\n | `mzizi-registry` | `### Changed — what changed (2026-11-04, #PR)`, then `## [4.25.0] - 2026-11-04` | entries under Unreleased; a release dates them |\n | `mzizi-api`, `mzizi-docs`, `mzizi-site` | `### Changed — what changed (2026-11-04, [#n](<issue URL>))` | under `## [Unreleased]`; no version in the heading |\n\n In `agent-tools` the publish job builds the release notes from the `###` headings that name\n the package and version, and a version without one fails before `npm publish`. In\n `packages-npm` the release job needs the `## [<name> <version>]` section. For the others, read the\n \"Release notes\" section of the repo's `AGENTS.md`. Whatever the style, the heading must be unique under\n `## [Unreleased]` (markdownlint MD024).\n\n- If `package.json` was already bumped for a version that has since reached npm, bump again\n before the release PR, or the job has nothing valid to publish.\n- Do not bump a version a repo's rules leave to tags (a staging patch tag is the workflow's\n job, never yours).\n\n## 3. The main gate and the `forward-ported` label\n\nThe org's main gate (`release-version-check`, which runs `main-gate.sh`) can fail with:\n\n```text\n'main' has 1 commit(s) since the last release that staging may not have; this release would delete them\n```\n\nIt fails when `main` holds commits that `staging`'s history does not. The repos' `AGENTS.md`\nname the usual cause, commits that exist only on `main` and that `staging` lacks (forward-port them first). A\nrelease itself also leaves commits on `main` that `staging` never contains (see part 4), and the\ngate has been seen to fire after one. Which commits it counts is defined in `main-gate.sh` in\nthe org's `.github` repository, which a session often cannot read: read the failing job's log\nrather than assuming. When the trees are equal, nothing would be deleted; the documented lane\nfor that is the **`forward-ported` label** on the release PR.\n\n**That label is an owner decision, made per release.** Add it only when the owner has said so\nfor this release, and only after you have checked both of these:\n\n1. The tree hashes are equal (part 1), and\n2. The main gate is the **only** failing check on the PR:\n\n```bash\nSHA=$(gh api repos/OWNER/REPO/pulls/N --jq .head.sha)\ngh api \"repos/OWNER/REPO/commits/$SHA/check-runs?per_page=100\" --jq '.check_runs[] | [(.conclusion // .status), .name] | @tsv'\n```\n\nThen label the PR and re-run only the failed jobs:\n\n```bash\ngh api -X POST repos/OWNER/REPO/issues/N/labels -f 'labels[]=forward-ported'\ngh api \"repos/OWNER/REPO/actions/runs?head_sha=$SHA&per_page=30\" --jq '.workflow_runs[] | [.id, .name, .conclusion] | @tsv'\ngh api -X POST repos/OWNER/REPO/actions/runs/RUN_ID/rerun-failed-jobs\n```\n\n- **Never `--admin`**, never skip or disable the gate, never merge past a red check. Org admins\n can bypass rulesets; agents never do.\n- If any other check fails, fix that first. The label does not cover it.\n- Prevention: forward-port `main`-only commits into `staging` before cutting, and open\n every pin bump into `staging` (see `registry-propagation`). After the release, sync `main` back into `staging` as the repo's\n `AGENTS.md` says, so the trees match before more work lands. Compare trees, not\n ahead/behind counts.\n\n## 4. Merge queues\n\n- **The two queues use different methods.** In all six repos (read from the rulesets on\n 4 November 2026: `gh api repos/OWNER/REPO/rulesets`, then `.../rulesets/ID`, field\n `rules[].parameters.merge_method` of the `merge_queue` rule), `merge-queue-staging` is\n **SQUASH** and `merge-queue-main` is **MERGE**. The `AGENTS.md` files agree once read closely:\n \"squash merges through the queue\" is the `staging` rule, and they say `merge-queue-main` stays\n MERGE. Where they say a release \"lands as one squash commit\", they mean the **release branch**\n is one commit (part 1). The queue then lands it on `main` as a **merge commit** with two\n parents (mzizi-registry `main` `c3e6e87`, \"Merge pull request #623\", is one; its second parent\n is the one-commit release branch). Neither commit is in `staging`'s history, so after a\n release the trees match and the ancestry does not: compare trees, never ahead/behind counts,\n and sync `main` back into `staging` (the repo's `AGENTS.md`). Do not choose a method by hand;\n enqueue and let the queue use its own. Ruleset reads can be refused for an agent: say so\n rather than guessing.\n- **The repo convention is `gh pr merge <n> --squash --auto`** (every repo's `AGENTS.md`, \"Merge\n convention\"): it adds the PR to the queue, and the queue applies its own method. Never\n `--admin`.\n- **The fallback, where GraphQL is blocked.** Cloud sessions block GraphQL, so\n `gh pr merge --auto` fails there. Then enable auto-merge with the GitHub MCP\n `enable_pr_auto_merge` tool or the REST route that session's own GraphQL error\n names. It does the same job, and it is the fallback, not the convention:\n\n```bash\ngh api -X PUT repos/OWNER/REPO/pulls/N/ccr/auto_merge -f merge_method=squash # a staging PR\ngh api -X DELETE repos/OWNER/REPO/pulls/N/ccr/auto_merge # to take it back\n```\n\nFor a release PR into `main`, do not argue with the queue: its method is MERGE, and release\nPRs there have landed as merge commits (mzizi-registry #623, agent-tools #358).\n\n- Enqueue only past the merge gate: the work is complete, CI is green, it is verified at\n runtime, and `/code-review` has run with its findings resolved.\n- A **dequeue with `MERGE_CONFLICT`** means the base moved. Merge `staging` (or `main`, for a\n sync PR) into your branch and resolve. In `CHANGELOG.md`, a conflict under\n `## [Unreleased]` almost always means two PRs each added entries: **keep both sides**.\n Push, then enqueue again.\n\n## 5. Stale checks\n\n`issue attached` reads the PR body from the **event snapshot**, so fixing the body does not\nchange a check that already ran. After you fix the body, save it again to fire a fresh event\n(change it by a character and back, or `gh api -X PATCH repos/OWNER/REPO/pulls/N -F body=@body.md`),\nthen read the new run. The same goes for a label: a check that read the labels before you\nadded one needs a re-run.\n\n## 6. Publish\n\nOn a merge to `main` the release workflow runs `npm publish` (trusted publishing, no token)\nand then a tag step.\n\n- npm can take **minutes** to list a version after `npm publish` prints `+ pkg@version`. The\n tag step can run first, print `not on npm, not tagged`, and make **no tag and no release**.\n Wait until npm lists it (`bash \"$V\" npm <package>@<version>` says `listed`), then re-run the\n workflow run **once**:\n\n```bash\ngh api \"repos/OWNER/REPO/actions/runs?branch=main&per_page=10\" --jq '.workflow_runs[] | [.id, .name, .status, .conclusion] | @tsv'\ngh api -X POST repos/OWNER/REPO/actions/runs/RUN_ID/rerun\n```\n\n- **Releases are immutable.** The workflow creates a release as a draft with its notes, then\n publishes it once. Never edit a published release, never move or delete and re-create a tag,\n never re-upload an asset. A wrong release is corrected by the next version, not by editing\n this one.\n\n## 7. Verify LIVE\n\nMerged is not live. Cloudflare deploys lag a minute or two behind a merge to `main`, so a\nstale answer in the first minutes is not a failure; read again before you report. Check what\nthe release claims, by its own value:\n\n```bash\nnpm view @nyuchi/mzizi-skills version # npm has the version\nbash \"$V\" release mzizi-dev/agent-tools '@nyuchi/mzizi-skills@0.19.0' # tag, release (draft=false) and notes\ncurl -sI https://api.mzizi.dev/v1/brand | grep -i x-mzizi-source # registry=<sha> the API serves\ncurl -s https://mcp.mzizi.dev/catalogue.json | head -c 400 # version, source.registry\ncurl -s -o /dev/null -w '%{http_code}\\n' -X POST -H 'Origin: https://evil.example' \\\n -H 'Content-Type: application/json' -d '{}' https://mcp.mzizi.dev/mcp # 403\ncurl -s -o /dev/null -w '%{http_code}\\n' https://docs.mzizi.dev/ # 200\n```\n\n`bash \"$V\" api`, `bash \"$V\" mcp` and `bash \"$V\" http <url>...` print the same facts without\nthe greps. For a site, the spacing guard runs from a `mzizi-site` checkout (Playwright\ninstalled; `PW_EXECUTABLE` names a Chrome or Chromium binary). Point it at the live URL or a\nCloudflare preview, never a bare `python3 -m http.server` (the site's clean URLs 404 there), and\nwrite its output outside the repo:\n\n```bash\nPW_EXECUTABLE=/opt/pw-browsers/chromium node scripts/audit-ui.mjs --url https://mzizi.dev --guard --out \"${TMPDIR:-/tmp}/audit-out\"\n```\n\nReport each product as **live** only with the value you read this minute, and say what is\nstill waiting (a queued deploy, a bot PR, a registry pin). Use the status format in the\n`progress-report` skill.\n\n## Do not\n\n- Do not use `--admin`, add `forward-ported` without the owner's word for this release, or\n skip or disable a test or gate to go green.\n- Do not cut a release you were not authorised to cut, or promote `staging` while it holds work\n the owner has not approved.\n- Do not edit or move a published tag or release.\n- Do not write a secret, a token or credential status into a PR body, a report or an issue.\n\n## Checklist\n\n- [ ] Branch cut off `origin/main` with `staging`'s tree in one commit; the two tree hashes are equal.\n- [ ] Title is `chore(release): staging to main, <date> (<package versions>)`; the body has an issue line at the start of its own line.\n- [ ] Each bumped version is the next minor above npm and has a CHANGELOG section; no major without the owner.\n- [ ] If the main gate is the only failure and the owner said so: `forward-ported` added, failed jobs re-run, label still there after any re-cut.\n- [ ] Enqueued with `gh pr merge <n> --squash --auto` (or, where GraphQL is blocked, the MCP tool or the REST route), past the merge gate; no `--admin`.\n- [ ] Publish confirmed on npm before the one workflow re-run; no release edited.\n- [ ] Verified live with the commands above, a minute or two after the deploy; each product reported with the value read.",
261
261
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/ship-release",
262
262
  "files": [
263
263
  {
264
264
  "path": "verify-live.sh",
265
265
  "mimeType": "text/x-shellscript",
266
- "bytes": 6123,
266
+ "bytes": 7810,
267
267
  "encoding": "utf8",
268
- "content": "#!/usr/bin/env bash\n# verify-live.sh — print what is LIVE (the Mzizi `ship-release` skill).\n#\n# Portable bash (3.2+, macOS and Linux). READ-ONLY: it only reads npm, the GitHub\n# API (through gh) and HTTP responses, and never writes, publishes, tags or deploys.\n# It prints facts. It does not decide that a release is good: compare the output\n# with what you expect, and say \"not live\" until it matches. Cloudflare deploys\n# lag a minute or two behind a merge to main, so a stale answer is not yet a failure.\n#\n# verify-live.sh npm PKG[@VERSION]... latest on npm, or whether PKG@VERSION is listed\n# verify-live.sh api [BASE] status and X-Mzizi-Source of BASE/v1/brand\n# (default https://api.mzizi.dev)\n# verify-live.sh mcp [BASE] catalogue.json version and registry commit, and the\n# status of a POST with Origin https://evil.example\n# (default https://mcp.mzizi.dev; want 403)\n# verify-live.sh release OWNER/REPO TAG whether the tag and its release exist, draft state\n# and the length of the notes\n# verify-live.sh http URL... the HTTP status of each page\n#\n# Environment: VERIFY_CURL, VERIFY_GH, VERIFY_NPM, VERIFY_NODE name the binaries\n# (defaults: curl, gh, npm, node).\n#\n# Exit codes: 0 ok (a down site or an unlisted version is a fact, not a failure);\n# 1 bad usage or a missing tool.\n\nset -u\nset -o pipefail\n\ndie() {\n printf 'verify-live: %s\\n' \"$*\" >&2\n exit 1\n}\n\nCURL=\"${VERIFY_CURL:-curl}\"\nGH=\"${VERIFY_GH:-gh}\"\nNPM=\"${VERIFY_NPM:-npm}\"\nNODE=\"${VERIFY_NODE:-node}\"\n\nneed() { command -v \"$1\" >/dev/null 2>&1 || die \"$2: '$1' not found\"; }\n\nis_url() { case \"$1\" in http://*|https://*) return 0 ;; *) return 1 ;; esac; }\n\ncmd_npm() {\n [ $# -ge 1 ] || die \"usage: verify-live.sh npm PKG[@VERSION]...\"\n need \"$NPM\" npm\n local spec v\n for spec in \"$@\"; do\n case \"$spec\" in\n *[!A-Za-z0-9@/._-]*|'') die \"npm: '$spec' is not a package spec\" ;;\n esac\n # A scoped name starts with @, so a version is an @ after the first character.\n case \"${spec#?}\" in\n *@*)\n v=\"$(\"$NPM\" view \"$spec\" version 2>/dev/null | tail -n 1)\" || v=\"\"\n if [ -n \"$v\" ]; then printf '%s listed\\n' \"$spec\"; else printf '%s not listed on npm yet\\n' \"$spec\"; fi\n ;;\n *)\n v=\"$(\"$NPM\" view \"$spec\" version 2>/dev/null | tail -n 1)\" || v=\"\"\n if [ -n \"$v\" ]; then printf '%s latest=%s\\n' \"$spec\" \"$v\"; else printf '%s ? (npm view failed)\\n' \"$spec\"; fi\n ;;\n esac\n done\n}\n\ncmd_api() {\n local base=\"${1:-https://api.mzizi.dev}\" url hdr code src\n is_url \"$base\" || die \"api: '$base' is not an http(s) URL\"\n need \"$CURL\" curl\n url=\"${base%/}/v1/brand\"\n hdr=\"$(\"$CURL\" -sI --max-time 15 \"$url\" 2>/dev/null | tr -d '\\r')\" || hdr=\"\"\n code=\"$(printf '%s\\n' \"$hdr\" | awk 'toupper($1) ~ /^HTTP/ { c = $2 } END { print c }')\"\n src=\"$(printf '%s\\n' \"$hdr\" | awk 'tolower($1) == \"x-mzizi-source:\" { sub(/^[^:]*: */, \"\"); print; exit }')\"\n printf '%s %s\\n' \"${code:-000}\" \"$url\"\n printf 'x-mzizi-source %s\\n' \"${src:-?}\"\n}\n\ncmd_mcp() {\n local base=\"${1:-https://mcp.mzizi.dev}\" body parsed code\n is_url \"$base\" || die \"mcp: '$base' is not an http(s) URL\"\n need \"$CURL\" curl\n need \"$NODE\" node\n base=\"${base%/}\"\n body=\"$(\"$CURL\" -s --max-time 15 \"$base/catalogue.json\" 2>/dev/null)\" || body=\"\"\n parsed=\"$(printf '%s' \"$body\" | \"$NODE\" -e '\n let t = \"\";\n process.stdin.on(\"data\", (c) => (t += c)).on(\"end\", () => {\n try {\n const d = JSON.parse(t);\n const r = (d.source && d.source.registry) || \"\";\n const sha = r.split(\"/\").pop();\n console.log(\"version \" + (d.version ?? \"?\"));\n console.log(\"registry \" + (sha ? sha.slice(0, 12) : \"?\"));\n } catch {\n console.log(\"version ?\");\n console.log(\"registry ?\");\n }\n });' 2>/dev/null)\" || parsed=\"version ?\nregistry ?\"\n printf 'catalogue.json %s\\n' \"$(printf '%s' \"$parsed\" | sed -n 1p)\"\n printf 'catalogue.json %s\\n' \"$(printf '%s' \"$parsed\" | sed -n 2p)\"\n code=\"$(\"$CURL\" -s -o /dev/null --max-time 15 -w '%{http_code}' -X POST -H 'Origin: https://evil.example' \\\n -H 'Content-Type: application/json' -d '{}' \"$base/mcp\" 2>/dev/null)\" || code=\"${code:-000}\"\n printf 'origin probe %s (want 403)\\n' \"${code:-000}\"\n}\n\ncmd_release() {\n [ $# -eq 2 ] || die \"usage: verify-live.sh release OWNER/REPO TAG\"\n need \"$GH\" gh\n local repo=\"$1\" tag=\"$2\" sha info\n case \"$repo\" in */*) ;; *) die \"release: '$repo' is not OWNER/REPO\" ;; esac\n case \"$tag\" in *[!A-Za-z0-9@/._-]*|'') die \"release: '$tag' is not a tag name\" ;; esac\n sha=\"$(\"$GH\" api \"repos/$repo/git/ref/tags/$tag\" --jq '.object.sha' 2>/dev/null)\" || sha=\"\"\n if [ -n \"$sha\" ]; then printf 'tag %s -> object %s\\n' \"$tag\" \"$sha\"; else printf 'tag %s not found\\n' \"$tag\"; fi\n # releases/tags/<tag> answers 404 for a draft, so list the releases and match the tag name:\n # a draft that was never published shows draft=true instead of \"not found\".\n info=\"$(\"$GH\" api \"repos/$repo/releases?per_page=100\" \\\n --jq \"[.[] | select(.tag_name == \\\"$tag\\\")][0] | select(. != null) | \\\"draft=\\\\(.draft) prerelease=\\\\(.prerelease) notes_chars=\\\\(.body | length)\\\"\" 2>/dev/null)\" || info=\"\"\n if [ -n \"$info\" ]; then printf 'release %s %s\\n' \"$tag\" \"$info\"; else printf 'release %s not found\\n' \"$tag\"; fi\n}\n\ncmd_http() {\n [ $# -ge 1 ] || die \"usage: verify-live.sh http URL...\"\n need \"$CURL\" curl\n local u code\n for u in \"$@\"; do\n is_url \"$u\" || die \"http: '$u' is not an http(s) URL\"\n code=\"$(\"$CURL\" -s -o /dev/null -L --max-time 15 -w '%{http_code}' \"$u\" 2>/dev/null)\" || code=\"${code:-000}\"\n printf '%s %s\\n' \"${code:-000}\" \"$u\"\n done\n}\n\nCMD=\"${1:-}\"\n[ $# -gt 0 ] && shift\ncase \"$CMD\" in\n npm) cmd_npm \"$@\" ;;\n api) cmd_api \"$@\" ;;\n mcp) cmd_mcp \"$@\" ;;\n release) cmd_release \"$@\" ;;\n http) cmd_http \"$@\" ;;\n -h|--help) sed -n '2,21p' \"$0\" ;;\n *) die \"unknown command: ${CMD:-<none>} (npm, api, mcp, release, http)\" ;;\nesac\n"
268
+ "content": "#!/usr/bin/env bash\n# verify-live.sh — print what is LIVE (the Mzizi `ship-release` skill).\n#\n# Portable bash (3.2+, macOS and Linux). READ-ONLY: it only reads npm, the GitHub\n# API (through gh) and HTTP responses, and never writes, publishes, tags or deploys.\n# It prints facts. It does not decide that a release is good: compare the output\n# with what you expect, and say \"not live\" until it matches. Cloudflare deploys\n# lag a minute or two behind a merge to main, so a stale answer is not yet a failure.\n#\n# verify-live.sh npm PKG[@VERSION]... latest on npm, or whether PKG@VERSION is listed\n# verify-live.sh api [BASE] status and X-Mzizi-Source of BASE/v1/brand\n# (default https://api.mzizi.dev)\n# verify-live.sh mcp [BASE] catalogue.json version and registry commit, and the\n# status of a POST with Origin https://evil.example\n# (default https://mcp.mzizi.dev; want 403)\n# verify-live.sh release OWNER/REPO TAG whether the tag and its release exist, draft state\n# and the length of the notes\n# verify-live.sh http URL... the HTTP status of each page\n#\n# Environment: VERIFY_CURL, VERIFY_GH, VERIFY_NPM, VERIFY_NODE name the binaries\n# (defaults: curl, gh, npm, node). VERIFY_RELEASE_PAGES caps the release pages read (20).\n#\n# Exit codes: 0 ok (a down site or an unlisted version is a fact, not a failure);\n# 1 bad usage or a missing tool.\n\nset -u\nset -o pipefail\n\ndie() {\n printf 'verify-live: %s\\n' \"$*\" >&2\n exit 1\n}\n\nCURL=\"${VERIFY_CURL:-curl}\"\nGH=\"${VERIFY_GH:-gh}\"\nNPM=\"${VERIFY_NPM:-npm}\"\nNODE=\"${VERIFY_NODE:-node}\"\nRELEASE_MAX_PAGES=\"${VERIFY_RELEASE_PAGES:-20}\" # 20 pages of 100 releases\n\nusage() {\n cat <<'USAGE'\nverify-live.sh: print what is LIVE (read-only; the Mzizi `ship-release` skill)\n\n verify-live.sh npm PKG[@VERSION]... latest on npm, or whether PKG@VERSION is listed\n verify-live.sh api [BASE] status and X-Mzizi-Source of BASE/v1/brand\n (default https://api.mzizi.dev)\n verify-live.sh mcp [BASE] catalogue.json version and registry commit, and the\n status of a POST with Origin https://evil.example\n (default https://mcp.mzizi.dev; want 403)\n verify-live.sh release OWNER/REPO TAG whether the tag and its release exist, draft state\n and the length of the notes\n verify-live.sh http URL... the HTTP status of each page\n\nEnvironment: VERIFY_CURL, VERIFY_GH, VERIFY_NPM, VERIFY_NODE name the binaries\n(defaults: curl, gh, npm, node).\n\nExit codes: 0 ok (a down site or an unlisted version is a fact, not a failure);\n1 bad usage or a missing tool.\nUSAGE\n}\n\nneed() { command -v \"$1\" >/dev/null 2>&1 || die \"$2: '$1' not found\"; }\n\nis_url() { case \"$1\" in http://*|https://*) return 0 ;; *) return 1 ;; esac; }\n\ncmd_npm() {\n [ $# -ge 1 ] || die \"usage: verify-live.sh npm PKG[@VERSION]...\"\n need \"$NPM\" npm\n local spec v\n for spec in \"$@\"; do\n case \"$spec\" in\n *[!A-Za-z0-9@/._-]*|'') die \"npm: '$spec' is not a package spec\" ;;\n esac\n # A scoped name starts with @, so a version is an @ after the first character.\n case \"${spec#?}\" in\n *@*)\n v=\"$(\"$NPM\" view \"$spec\" version 2>/dev/null | tail -n 1)\" || v=\"\"\n if [ -n \"$v\" ]; then printf '%s listed\\n' \"$spec\"; else printf '%s not listed on npm yet\\n' \"$spec\"; fi\n ;;\n *)\n v=\"$(\"$NPM\" view \"$spec\" version 2>/dev/null | tail -n 1)\" || v=\"\"\n if [ -n \"$v\" ]; then printf '%s latest=%s\\n' \"$spec\" \"$v\"; else printf '%s ? (npm view failed)\\n' \"$spec\"; fi\n ;;\n esac\n done\n}\n\ncmd_api() {\n local base=\"${1:-https://api.mzizi.dev}\" url hdr code src\n is_url \"$base\" || die \"api: '$base' is not an http(s) URL\"\n need \"$CURL\" curl\n url=\"${base%/}/v1/brand\"\n hdr=\"$(\"$CURL\" -sI --max-time 15 \"$url\" 2>/dev/null | tr -d '\\r')\" || hdr=\"\"\n code=\"$(printf '%s\\n' \"$hdr\" | awk 'toupper($1) ~ /^HTTP/ { c = $2 } END { print c }')\"\n src=\"$(printf '%s\\n' \"$hdr\" | awk 'tolower($1) == \"x-mzizi-source:\" { sub(/^[^:]*: */, \"\"); print; exit }')\"\n printf '%s %s\\n' \"${code:-000}\" \"$url\"\n printf 'x-mzizi-source %s\\n' \"${src:-?}\"\n}\n\ncmd_mcp() {\n local base=\"${1:-https://mcp.mzizi.dev}\" body parsed code\n is_url \"$base\" || die \"mcp: '$base' is not an http(s) URL\"\n need \"$CURL\" curl\n need \"$NODE\" node\n base=\"${base%/}\"\n body=\"$(\"$CURL\" -s --max-time 15 \"$base/catalogue.json\" 2>/dev/null)\" || body=\"\"\n parsed=\"$(printf '%s' \"$body\" | \"$NODE\" -e '\n let t = \"\";\n process.stdin.on(\"data\", (c) => (t += c)).on(\"end\", () => {\n try {\n const d = JSON.parse(t);\n const r = (d.source && d.source.registry) || \"\";\n const sha = r.split(\"/\").pop();\n console.log(\"version \" + (d.version ?? \"?\"));\n console.log(\"registry \" + (sha ? sha.slice(0, 12) : \"?\"));\n } catch {\n console.log(\"version ?\");\n console.log(\"registry ?\");\n }\n });' 2>/dev/null)\" || parsed=\"version ?\nregistry ?\"\n printf 'catalogue.json %s\\n' \"$(printf '%s' \"$parsed\" | sed -n 1p)\"\n printf 'catalogue.json %s\\n' \"$(printf '%s' \"$parsed\" | sed -n 2p)\"\n code=\"$(\"$CURL\" -s -o /dev/null --max-time 15 -w '%{http_code}' -X POST -H 'Origin: https://evil.example' \\\n -H 'Content-Type: application/json' -d '{}' \"$base/mcp\" 2>/dev/null)\" || code=\"${code:-000}\"\n printf 'origin probe %s (want 403)\\n' \"${code:-000}\"\n}\n\ncmd_release() {\n [ $# -eq 2 ] || die \"usage: verify-live.sh release OWNER/REPO TAG\"\n need \"$GH\" gh\n local repo=\"$1\" tag=\"$2\" sha info out page\n case \"$repo\" in */*) ;; *) die \"release: '$repo' is not OWNER/REPO\" ;; esac\n case \"$tag\" in *[!A-Za-z0-9@/._-]*|'') die \"release: '$tag' is not a tag name\" ;; esac\n sha=\"$(\"$GH\" api \"repos/$repo/git/ref/tags/$tag\" --jq '.object.sha' 2>/dev/null)\" || sha=\"\"\n if [ -n \"$sha\" ]; then printf 'tag %s -> object %s\\n' \"$tag\" \"$sha\"; else printf 'tag %s not found\\n' \"$tag\"; fi\n # releases/tags/<tag> answers 404 for a draft, so list the releases and match the tag name:\n # a draft that was never published shows draft=true instead of \"not found\". The list is\n # newest first and 100 a page, so read pages until the tag turns up or a page is short.\n # (Explicit page numbers, not `gh api --paginate`: a proxy may refuse paginate's next links.)\n info=\"\"\n page=1\n while [ \"$page\" -le \"$RELEASE_MAX_PAGES\" ]; do\n out=\"$(\"$GH\" api \"repos/$repo/releases?per_page=100&page=$page\" \\\n --jq \"\\\"\\(length)|\\\" + ([.[] | select(.tag_name == \\\"$tag\\\")][0] | if . then \\\"draft=\\\\(.draft) prerelease=\\\\(.prerelease) notes_chars=\\\\(.body | length)\\\" else \\\"\\\" end)\" 2>/dev/null)\" || out=\"\"\n [ -n \"$out\" ] || break\n info=\"${out#*|}\"\n [ -z \"$info\" ] || break\n [ \"${out%%|*}\" -ge 100 ] 2>/dev/null || break\n page=$((page + 1))\n done\n if [ -n \"$info\" ]; then printf 'release %s %s\\n' \"$tag\" \"$info\"; else printf 'release %s not found\\n' \"$tag\"; fi\n}\n\ncmd_http() {\n [ $# -ge 1 ] || die \"usage: verify-live.sh http URL...\"\n need \"$CURL\" curl\n local u code\n for u in \"$@\"; do\n is_url \"$u\" || die \"http: '$u' is not an http(s) URL\"\n code=\"$(\"$CURL\" -s -o /dev/null -L --max-time 15 -w '%{http_code}' \"$u\" 2>/dev/null)\" || code=\"${code:-000}\"\n printf '%s %s\\n' \"${code:-000}\" \"$u\"\n done\n}\n\nCMD=\"${1:-}\"\n[ $# -gt 0 ] && shift\ncase \"$CMD\" in\n npm) cmd_npm \"$@\" ;;\n api) cmd_api \"$@\" ;;\n mcp) cmd_mcp \"$@\" ;;\n release) cmd_release \"$@\" ;;\n http) cmd_http \"$@\" ;;\n -h|--help) usage ;;\n *) die \"unknown command: ${CMD:-<none>} (npm, api, mcp, release, http)\" ;;\nesac\n"
269
269
  }
270
270
  ]
271
271
  },
272
272
  {
273
273
  "name": "registry-propagation",
274
274
  "category": "dev",
275
- "description": "Use this skill when a change in mzizi-registry has to reach the products that read it, or when someone asks whether a registry change is live. It maps the chain: a registry release on main, then the pins on staging (mzizi-api scripts/registry-ref.json with build:data, parity and PIN_ entries in EXPECTED; mzizi-mcp registry.pin.json; @bundu/ui scripts/registry-ref.json with registry:sync, registry:check and contracts:check), docs (check-freshness.mjs), the site (builds from the API, so verify-rendered.py counts move), and the skills (agent-tools mzizi-skills, then the registry's skills:generate and plugin:skills). It gives the rules (pin a released registry main commit, bots open PRs against staging, parity non-strict into staging and strict into main), the order, what each product shows once live, and a live alignment table to fill with commands. Releasing each repo uses ship-release.",
276
- "body_mdx": "# Registry propagation: from a registry release to every product, proven live\n\n> **Covenant:** _A registry change is live in a product when that product says so, not when its PR merges._\n\n`mzizi-dev/mzizi-registry` is the source. The API, the MCP server, `@bundu/ui`, the docs, the\nsite and the skills each read it a different way, each through its own pin or build. When one\nlags, an agent or a visitor gets a different answer depending on which product they ask. This\nskill is the map and the proof: the order, the commands, and a table you fill with what you read.\n\n## The chain\n\n```text\nmzizi-registry main (a release, e.g. v4.25.0)\n |\n +--> mzizi-api scripts/registry-ref.json -> api.mzizi.dev (build:data, parity)\n +--> mzizi-mcp mzizi-mcp/registry.pin.json -> mcp.mzizi.dev (agent-tools)\n +--> @bundu/ui scripts/registry-ref.json -> npm @bundu/ui (packages-npm; registry:sync)\n |\n +--> mzizi-site builds from api.mzizi.dev -> mzizi.dev (counts in verify-rendered.py)\n +--> mzizi-docs facts checked against the API -> docs.mzizi.dev (check-freshness.mjs)\n |\n +--> skills agent-tools mzizi-skills (npm) -> the registry's skills:generate and plugin:skills\n -> api /v1/skills, mcp mzizi_get_skills, the plugin\n```\n\nEach arrow is a PR on `staging` of the receiving repo, then a release of that repo to `main`\n(use the `ship-release` skill). Nothing below is live until that repo's release is live.\n\n## Rules\n\n- **Pin a registry `main` commit that is released.** A pin is a full commit SHA. Read the\n latest release tag and the commit it points at; do not pin a branch tip that is not released,\n and do not pin a commit from a PR branch.\n- **Pin bots open PRs against `staging`**, not `main`, where the repo has moved them (mzizi-api\n api#120 merged; the agent-tools workflow sets `PIN_BASE_BRANCH: staging` from the release that\n carries it). Older copies of a repo's guide may still say `main`: read the base branch of the\n bot's PR and `PIN_BASE_BRANCH` in its workflow, and report where the bump actually landed. Do not push to `bot/registry-pin`\n unless you take the bump over (then it is yours, with a CHANGELOG entry).\n- **Parity is non-strict on PRs into `staging`** (the recorded `EXPECTED` differences are\n honoured) and **strict on PRs into `main`** (`EXPECTED` is ignored, so every difference is\n unexplained). A bump that passes on `staging` can still fail the release PR: prune or fix\n `EXPECTED` before releasing, never edit the check to pass.\n- **Never skip a gate to land a pin.** A pin that fails a gate means a handler, a count or a\n contract moved: fix that, or stop and report.\n- **Pin first, then release, then verify.** Report a product as live only when the table below\n shows its value.\n\n## Order\n\n1. **Registry released.** Read the tag and the commit: `gh api repos/mzizi-dev/mzizi-registry/releases/latest --jq .tag_name`\n and `gh api repos/mzizi-dev/mzizi-registry/commits/main --jq .sha`. Pin the released commit.\n2. **API** (mzizi-api, base `staging`). Move `ref` in `scripts/registry-ref.json` (and `pinned`,\n `note`). Then:\n\n ```bash\n npm ci\n npm run build:data # clones the pinned registry, writes src/data/*.json\n npm run typecheck && npm run check && npm test\n npm run dev # in a second shell: wrangler dev on http://localhost:8787\n npm run parity # live api.mzizi.dev against that candidate; `npm run parity -- --strict` ignores EXPECTED\n ```\n\n An intentional difference goes into `EXPECTED` in `scripts/parity.mjs` as a `PIN_<sha>`\n constant (the first characters of the commit) with its reason, and into the PR body. Add a\n `CHANGELOG.md` entry saying what a caller of the API sees differently.\n\n3. **MCP** (agent-tools, base `staging`). Move `ref` in `mzizi-mcp/registry.pin.json`; run\n `pnpm --filter @nyuchi/mzizi-mcp generate` (or the package's `test`) so the bundled data\n rebuilds, and fix any test that counts registry data. A change to what the MCP serves is a\n package version bump (next minor above npm) and a CHANGELOG section.\n4. **`@bundu/ui`** (packages-npm, base `staging`). Move `ref` in `scripts/registry-ref.json`, then:\n\n ```bash\n pnpm install --frozen-lockfile\n pnpm registry:sync # rebuild the components from the pin\n pnpm contracts:fetch # refresh packages/bundu-ui/contracts/\n pnpm registry:check && pnpm contracts:check && pnpm tokens:check\n pnpm test\n ```\n\n Generated files are never hand-edited. The package version is the next minor above npm,\n with a CHANGELOG section; the release publishes it.\n\n5. **Skills.** A skills change starts in agent-tools `mzizi-skills` (a release to npm, see\n `ship-release`). The registry then takes it: its `@nyuchi/mzizi-skills` range lets\n `pnpm skills:generate` and `pnpm plugin:skills` regenerate the bundled copies (check with\n `pnpm skills:generate:check` and `pnpm plugin:skills:check`). The API serves skills through\n the registry's dependency and its pin, the MCP bundles them from agent-tools at build time,\n and the public plugin carries the generated copy. A skills release is done only when npm,\n the MCP and the API all serve it.\n6. **Site** (mzizi-site, base `staging`). It builds from the live API, so its counts move\n when the API changes and the rendered gate fails on every branch until they are updated:\n `python3 scripts/verify-rendered.py dist` (after `pnpm build`). Update the expected numbers\n there and the same counts in `AGENTS.md` and `README.md`, with a CHANGELOG entry. Move the\n `@bundu/ui` range in `package.json` when a new version should land (`npm view @bundu/ui version`).\n7. **Docs** (mzizi-docs, base `staging`). Run `node scripts/check-freshness.mjs` (network),\n update the pages it names, keep the sentence shapes it matches (or update the script in the\n same change), and add a `CHANGELOG.md` entry naming the facts and pages that moved.\n8. **Release each repo** with `ship-release`, in the order the data flows: API, MCP and\n `@bundu/ui` first, then the site and docs that read them.\n\n## What each product shows once live\n\n| Product | Live signal |\n| ------- | ------------------------------------------------------------------------------------------------ |\n| API | `x-mzizi-source: mzizi-api-gateway; registry=<sha>` on every response; new fields in `/v1/brand` |\n| MCP | `catalogue.json`: `version`, `source.registry` (the commit), `skills.version` |\n| Skills | npm `latest`; `catalogue.json` `skills.version`; `/v1/skills` `meta.version` |\n| UI | npm `@bundu/ui` `latest`; the pin file on `main` equals the commit you pinned |\n| Site | the built page shows the new counts; `verify-rendered.py` passes on `main` |\n| Docs | `check-freshness.mjs` reports no drift against the live sources |\n\nFor the Bundu floor in 4.23.0 and the spacing floor in 4.24.0, the signal in `/v1/brand` is the\n`accessibility` block: `minTouchTarget`, `defaultTouchTarget`, `minInset`, `minControlGap`,\n`minLabelGap`, `minItemGap` and `minBodyLeading` present and not null.\n\n## Live alignment check\n\nFill one row per product, with the value you read in this session. A row with no value is\n\"not checked\", never \"aligned\". Expected is the registry release's commit (first 12 characters\nof the SHA) or the version the release carried. Cloudflare deploys lag a minute or two after a\nmerge to `main`, so read again before calling a row stale.\n\n```bash\nV=node_modules/@nyuchi/mzizi-skills/skills/ship-release/verify-live.sh # or fetch it, see ship-release\ngh api repos/mzizi-dev/mzizi-registry/commits/main --jq '.sha[0:12]'\n```\n\n| Product | Where its version or pin is read | Expected | Command |\n| ------------ | ---------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------- |\n| Registry | latest release tag and the commit it points at | the release being shipped | `gh api repos/mzizi-dev/mzizi-registry/releases/latest --jq .tag_name` |\n| API live | `x-mzizi-source` header | `registry=<sha>` | `curl -sI https://api.mzizi.dev/v1/brand \\| grep -i x-mzizi-source` |\n| API pin | `scripts/registry-ref.json` `ref` on `main` and on `staging` | the released commit | `gh api \"repos/mzizi-dev/mzizi-api/contents/scripts/registry-ref.json?ref=main\" --jq .content \\| base64 -d` |\n| MCP live | `catalogue.json` `source.registry` and `version` | the released commit | `bash \"$V\" mcp` |\n| MCP pin | `mzizi-mcp/registry.pin.json` `ref` on `main` | the released commit | `gh api \"repos/mzizi-dev/agent-tools/contents/mzizi-mcp/registry.pin.json?ref=main\" --jq .content \\| base64 -d` |\n| UI pin | `scripts/registry-ref.json` `ref` in packages-npm | the released commit | `gh api \"repos/mzizi-dev/packages-npm/contents/scripts/registry-ref.json?ref=main\" --jq .content \\| base64 -d` |\n| UI on npm | `@bundu/ui` `latest` | the version that was cut | `npm view @bundu/ui version` |\n| Skills (npm) | `@nyuchi/mzizi-skills` `latest` | the version that was cut | `npm view @nyuchi/mzizi-skills version` |\n| Skills (MCP) | `catalogue.json` `skills.version` | the same version | `curl -s https://mcp.mzizi.dev/catalogue.json \\| grep -o '\"skills\":{\"version\":\"[^\"]*\"'` |\n| Skills (API) | `/v1/skills` `meta.version` (it can lag npm: it follows the pin) | the same version | `curl -s https://api.mzizi.dev/v1/skills \\| grep -o '\"meta\":{[^}]*}'` |\n| Components | `/v1/ui` `meta.total` (the count the site and docs state) | the registry's count | `curl -s https://api.mzizi.dev/v1/ui \\| grep -o '\"meta\":{\"total\":[0-9]*'` |\n| Site | the rendered gate on `main`, and the page itself | counts equal the API's | `python3 scripts/verify-rendered.py dist` (in mzizi-site, after `pnpm build`) |\n| Docs | the freshness check | no drift | `node scripts/check-freshness.mjs` (in mzizi-docs; needs the network) |\n\nRead the pin files from `main` for what is live and from `staging` for what is queued; if\nthey differ, the release is still to do. A pin on `main` that equals the header the API serves\nis the proof that API deploy finished.\n\n## Do not\n\n- Do not pin an unreleased registry commit, or edit generated data (`src/data/`,\n `packages/bundu-ui` components, the plugin's `skills/`) by hand: change the source or the pin.\n- Do not loosen parity, a count or a contract check to pass; fix the cause or stop.\n- Do not claim a product is live from a merged PR. Read the header, the catalogue or npm.\n- Do not push to a pin bot's branch unless you are taking the bump over.\n\n## Checklist\n\n- [ ] The registry commit is a released one; its tag and SHA are written down.\n- [ ] Each receiving repo has a `staging` PR with the new pin, its generator or build run, its gates green and a CHANGELOG entry.\n- [ ] Parity on the API PR: non-strict into `staging`, strict into `main`, with `PIN_<sha>` entries pruned or recorded.\n- [ ] Each repo released with `ship-release`, data producers before the site and docs.\n- [ ] The live alignment table is filled with values read this session; a row not read says \"not checked\".",
275
+ "description": "Use this skill when a change in mzizi-registry has to reach the products that read it, or when someone asks whether a registry change is live. It maps the chain: a registry release on main, then the pins on staging (mzizi-api scripts/registry-ref.json with build:data, parity and PIN_ entries in EXPECTED; mzizi-mcp registry.pin.json; @bundu/ui scripts/registry-ref.json with registry:sync, registry:check and contracts:check), docs (check-freshness.mjs), the site (builds from the API, so verify-rendered.py counts move), and the skills (agent-tools mzizi-skills, then the registry's skills:generate and plugin:skills). It gives the rules (pin a released registry main commit, pins are bumped by PR into staging, parity non-strict into staging and strict into main), the order, what each product shows once live, and a live alignment table to fill with commands. Releasing each repo uses ship-release.",
276
+ "body_mdx": "# Registry propagation: from a registry release to every product, proven live\n\n> **Covenant:** _A registry change is live in a product when that product says so, not when its PR merges._\n\n`mzizi-dev/mzizi-registry` is the source. The API, the MCP server, `@bundu/ui`, the docs, the\nsite and the skills each read it a different way, each through its own pin or build. When one\nlags, an agent or a visitor gets a different answer depending on which product they ask. This\nskill is the map and the proof: the order, the commands, and a table you fill with what you read.\n\n## The chain\n\n```text\nmzizi-registry main (a release, e.g. v4.25.0)\n |\n +--> mzizi-api scripts/registry-ref.json -> api.mzizi.dev (build:data, parity)\n +--> mzizi-mcp mzizi-mcp/registry.pin.json -> mcp.mzizi.dev (agent-tools)\n +--> @bundu/ui scripts/registry-ref.json -> npm @bundu/ui (packages-npm; registry:sync)\n |\n +--> mzizi-site builds from api.mzizi.dev -> mzizi.dev (counts in verify-rendered.py)\n +--> mzizi-docs facts checked against the API -> docs.mzizi.dev (check-freshness.mjs)\n |\n +--> skills agent-tools mzizi-skills (npm) -> the registry's skills:generate and plugin:skills\n -> api /v1/skills, mcp mzizi_get_skills, the plugin\n```\n\nEach arrow is a PR on `staging` of the receiving repo, then a release of that repo to `main`\n(use the `ship-release` skill). Nothing below is live until that repo's release is live.\n\n## Rules\n\n- **Pin a registry `main` commit that is released.** A pin is a full commit SHA. Read the\n latest release tag and the commit it points at; do not pin a branch tip that is not released,\n and do not pin a commit from a PR branch.\n- **In agent-tools the pin is moved by hand, in a PR into `staging`; there is no pin bot.** The\n owner removed it on 4 November 2026 (steps in `mzizi-mcp/README.md`). Other repos may still\n run one: read the repo's `AGENTS.md` and workflows on the branch you act on, read the base\n branch of any bump PR, and report where the bump actually landed. A bump merged only on `main`\n is a commit `staging` lacks and trips the main gate at the next release.\n- **Parity in mzizi-api is non-strict on PRs and merge queues into `staging`** (the recorded\n `EXPECTED` differences are honoured, because `staging` is ahead of production; #126) and\n **strict on PRs into `main`** (`EXPECTED` is ignored, so every difference is unexplained;\n a PR that edits `scripts/parity.mjs` is also non-strict). Until a release carries #120 and #126\n to `main`, `main`'s own workflow runs parity only on PRs into `main`. A bump that passes on\n `staging` can still fail the release PR: prune or fix `EXPECTED` before releasing, never edit the\n check to pass.\n- **Never skip a gate to land a pin.** A pin that fails a gate means a handler, a count or a\n contract moved: fix that, or stop and report.\n- **Pin first, then release, then verify.** Report a product as live only when the table below\n shows its value.\n\n## Order\n\n1. **Registry released.** Read the tag and the commit: `gh api repos/mzizi-dev/mzizi-registry/releases/latest --jq .tag_name`\n and `gh api repos/mzizi-dev/mzizi-registry/commits/main --jq .sha`. Pin the released commit.\n2. **API** (mzizi-api, base `staging`). Move `ref` in `scripts/registry-ref.json` (and `pinned`,\n `note`). Then:\n\n ```bash\n npm ci\n npm run build:data # clones the pinned registry, writes src/data/*.json\n npm run typecheck && npm run check && npm test\n npm run dev # in a second shell: wrangler dev on http://localhost:8787\n npm run parity # live api.mzizi.dev against that candidate; `npm run parity -- --strict` ignores EXPECTED\n ```\n\n An intentional difference goes into `EXPECTED` in `scripts/parity.mjs` as a `PIN_<sha>`\n constant (the first characters of the commit) with its reason, and into the PR body. Add a\n `CHANGELOG.md` entry saying what a caller of the API sees differently.\n\n3. **MCP** (agent-tools, base `staging`). Move `ref` in `mzizi-mcp/registry.pin.json`; run\n `pnpm --filter @nyuchi/mzizi-mcp generate` (or the package's `test`) so the bundled data\n rebuilds, and fix any test that counts registry data. A change to what the MCP serves is a\n package version bump (next minor above npm) and a CHANGELOG section.\n4. **`@bundu/ui`** (packages-npm, base `staging`). Move `ref` in `scripts/registry-ref.json`, then:\n\n ```bash\n pnpm install --frozen-lockfile\n pnpm registry:sync # rebuild the components from the pin\n pnpm contracts:fetch # refresh packages/bundu-ui/contracts/\n pnpm registry:check && pnpm contracts:check && pnpm tokens:check\n pnpm test\n ```\n\n Generated files are never hand-edited. The package version is the next minor above npm,\n with a CHANGELOG section; the release publishes it.\n\n5. **Skills.** A skills change starts in agent-tools `mzizi-skills` (a release to npm, see\n `ship-release`). The registry then takes it: its `@nyuchi/mzizi-skills` range lets\n `pnpm skills:generate` and `pnpm plugin:skills` regenerate the bundled copies (check with\n `pnpm skills:generate:check` and `pnpm plugin:skills:check`). The API serves skills through\n the registry's dependency and its pin, the MCP bundles them from agent-tools at build time,\n and the public plugin carries the generated copy. A skills release is done only when npm,\n the MCP and the API all serve it.\n6. **Site** (mzizi-site, base `staging`). It builds from the live API, so its counts move\n when the API changes and the rendered gate fails on every branch until they are updated:\n `python3 scripts/verify-rendered.py dist` (after `pnpm build`). Update the expected numbers\n there and the same counts in `AGENTS.md` and `README.md`, with a CHANGELOG entry. Move the\n `@bundu/ui` range in `package.json` when a new version should land (`npm view @bundu/ui version`).\n7. **Docs** (mzizi-docs, base `staging`). Run `node scripts/check-freshness.mjs` (network),\n update the pages it names, keep the sentence shapes it matches (or update the script in the\n same change), and add a `CHANGELOG.md` entry naming the facts and pages that moved.\n8. **Release each repo** with `ship-release`, in the order the data flows: API, MCP and\n `@bundu/ui` first, then the site and docs that read them.\n\n## What each product shows once live\n\n| Product | Live signal |\n| ------- | ------------------------------------------------------------------------------------------------ |\n| API | `x-mzizi-source: mzizi-api-gateway; registry=<sha>` on every response; new fields in `/v1/brand` |\n| MCP | `catalogue.json`: `version`, `source.registry` (the commit), `skills.version` |\n| Skills | npm `latest`; `catalogue.json` `skills.version`; `/v1/skills` `meta.version` |\n| UI | npm `@bundu/ui` `latest`; the pin file on `main` equals the commit you pinned |\n| Site | the built page shows the new counts; `verify-rendered.py` passes on `main` |\n| Docs | `check-freshness.mjs` reports no drift against the live sources |\n\nFor the Bundu floor in 4.23.0 and the spacing floor in 4.24.0, the signal in `/v1/brand` is the\n`accessibility` block: `minTouchTarget`, `defaultTouchTarget`, `minInset`, `minControlGap`,\n`minLabelGap`, `minItemGap` and `minBodyLeading` present and not null.\n\n## Live alignment check\n\nFill one row per product, with the value you read in this session. A row with no value is\n\"not checked\", never \"aligned\". Expected is the registry release's commit (first 12 characters\nof the SHA) or the version the release carried. Cloudflare deploys lag a minute or two after a\nmerge to `main`, so read again before calling a row stale.\n\n```bash\nV=node_modules/@nyuchi/mzizi-skills/skills/ship-release/verify-live.sh # or fetch it, see ship-release\ngh api repos/mzizi-dev/mzizi-registry/commits/main --jq '.sha[0:12]'\n```\n\n| Product | Where its version or pin is read | Expected | Command |\n| ------------ | ---------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------- |\n| Registry | latest release tag and the commit it points at | the release being shipped | `gh api repos/mzizi-dev/mzizi-registry/releases/latest --jq .tag_name` |\n| API live | `x-mzizi-source` header | `registry=<sha>` | `curl -sI https://api.mzizi.dev/v1/brand \\| grep -i x-mzizi-source` |\n| API pin | `scripts/registry-ref.json` `ref` on `main` and on `staging` | the released commit | `gh api \"repos/mzizi-dev/mzizi-api/contents/scripts/registry-ref.json?ref=main\" --jq .content \\| base64 -d` |\n| MCP live | `catalogue.json` `source.registry` and `version` | the released commit | `bash \"$V\" mcp` |\n| MCP pin | `mzizi-mcp/registry.pin.json` `ref` on `main` | the released commit | `gh api \"repos/mzizi-dev/agent-tools/contents/mzizi-mcp/registry.pin.json?ref=main\" --jq .content \\| base64 -d` |\n| UI pin | `scripts/registry-ref.json` `ref` in packages-npm | the released commit | `gh api \"repos/mzizi-dev/packages-npm/contents/scripts/registry-ref.json?ref=main\" --jq .content \\| base64 -d` |\n| UI on npm | `@bundu/ui` `latest` | the version that was cut | `npm view @bundu/ui version` |\n| Skills (npm) | `@nyuchi/mzizi-skills` `latest` | the version that was cut | `npm view @nyuchi/mzizi-skills version` |\n| Skills (MCP) | `catalogue.json` `skills.version` | the same version | `curl -s https://mcp.mzizi.dev/catalogue.json \\| grep -o '\"skills\":{\"version\":\"[^\"]*\"'` |\n| Skills (API) | `/v1/skills` `meta.version` (it can lag npm: it follows the pin) | the same version | `curl -s https://api.mzizi.dev/v1/skills \\| grep -o '\"meta\":{[^}]*}'` |\n| Components | `/v1/ui` `meta.total` (the count the site and docs state) | the registry's count | `curl -s https://api.mzizi.dev/v1/ui \\| grep -o '\"meta\":{\"total\":[0-9]*'` |\n| Site | the rendered gate on `main`, and the page itself | counts equal the API's | `python3 scripts/verify-rendered.py dist` (in mzizi-site, after `pnpm build`) |\n| Docs | the freshness check | no drift | `node scripts/check-freshness.mjs` (in mzizi-docs; needs the network) |\n\nRead the pin files from `main` for what is live and from `staging` for what is queued; if\nthey differ, the release is still to do. A pin on `main` that equals the header the API serves\nis the proof that API deploy finished.\n\n## Do not\n\n- Do not pin an unreleased registry commit, or edit generated data (`src/data/`,\n `packages/bundu-ui` components, the plugin's `skills/`) by hand: change the source or the pin.\n- Do not loosen parity, a count or a contract check to pass; fix the cause or stop.\n- Do not claim a product is live from a merged PR. Read the header, the catalogue or npm.\n\n## Checklist\n\n- [ ] The registry commit is a released one; its tag and SHA are written down.\n- [ ] Each receiving repo has a `staging` PR with the new pin, its generator or build run, its gates green and a CHANGELOG entry.\n- [ ] Parity on the API PR: non-strict into `staging`, strict into `main`, with `PIN_<sha>` entries pruned or recorded.\n- [ ] Each repo released with `ship-release`, data producers before the site and docs.\n- [ ] The live alignment table is filled with values read this session; a row not read says \"not checked\".",
277
277
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/registry-propagation",
278
278
  "files": []
279
279
  },
@@ -281,11 +281,11 @@ export const BUNDLED_SKILLS = [
281
281
  "name": "pr-gates",
282
282
  "category": "dev",
283
283
  "description": "Use this skill before the first push of any pull request in a Mzizi repo, and whenever a required check is red. It makes the first push green by naming the checks that failed again and again and the fix for each: issue attached (the line must start with Closes, Fixes, Refs or No issue and sit on its own line), changelog entry required (an entry under Unreleased with a descriptive heading, so markdownlint MD024 does not flag duplicates; no-changelog only for CI, lint or typo changes), markdownlint, yamllint line length, no agent-local files (never commit CLAUDE.md or .claude), oxfmt and vp fmt (CI is the authority, never format a whole package locally), CodeQL (bad HTML filtering regexp, polynomial regular expression), parity (strict ignores EXPECTED), rendered-content gates, the version policy on staging, CHANGELOG merge conflicts and merge methods. Never skip a test, never --admin.",
284
- "body_mdx": "# PR gates: make the first push green\n\n> **Covenant:** _A red check is information. Fix its cause; never switch the check off._\n\nMost red first pushes in the Mzizi repos fail on the same dozen checks. Each has a cause you can\nread before you push and a fix that leaves the check meaning what it meant. This skill lists\nthem. It is not a way around a gate: **never skip, disable or loosen a test or a check to go\ngreen, and never `--admin`.** If a gate is wrong, say so in the PR and let a person change it.\n\nEach repo's own `AGENTS.md` is the authority where it differs, and CI is the authority over\nanything you ran locally.\n\n## Before you push\n\n```bash\ngit status --short # only the files you meant\ngit diff --name-only origin/staging...HEAD | grep -E '(^|/)(CLAUDE\\.local\\.md|\\.claude/)' && echo \"STOP: agent-local file\"\nCHANGELOG_BASE=origin/staging bash scripts/changelog-gate.sh # where the repo has the gate (see below)\npnpm check # the repo's own check script, where it has one\n```\n\nBefore every push, read what the commit holds, not what you meant it to hold:\n\n```bash\ngit show --stat HEAD # every file and line count in the commit you are about to push\ngit diff --stat origin/staging...HEAD\n```\n\nA file you did not mean to touch, or a huge line count, is the signal. `git add -A` sweeps\nin whatever a tool rewrote: in one session an audit run rewrote a tracked file\n(`audit-out/run.json`, about 160,000 lines) and it landed in a PR, which failed the formatter\ncheck. Add files by name, and run audits and generators with their output **outside** the\nrepo (a `--out \"${TMPDIR:-/tmp}/audit-out\"` style option, or a temp directory), or first\nconfirm the tool does not rewrite tracked files. If a tracked file was changed by accident,\nrestore the original from the base branch:\n\n```bash\ngit checkout origin/staging -- path/to/file\n```\n\nNever delete a tracked file to make the diff smaller; that is a change too.\n\nThen write the PR body (below) and read the checks the REST way, because GraphQL is blocked in\ncloud sessions (so `gh pr checks` fails there):\n\n```bash\nSHA=$(gh api repos/OWNER/REPO/pulls/N --jq .head.sha)\ngh api \"repos/OWNER/REPO/commits/$SHA/check-runs?per_page=100\" --jq '.check_runs[] | [(.conclusion // .status), .name] | @tsv'\ngh api repos/OWNER/REPO/actions/jobs/JOB_ID/logs # a failed job's log (or the GitHub MCP get_job_logs tool)\n```\n\n## The checks that failed, and the fix\n\n| Check | Why it fails | Fix |\n| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `issue attached` | No line in the PR body **starts with** `Closes`, `Fixes`, `Refs` or `No issue:`. A mention in the middle of a sentence does not count. | Put `Refs #n` (or `Refs owner/repo#n`, `Closes #n`, `No issue: <reason>`) alone on its own line at the start of a line. It reads the body snapshot of the event: after fixing it, save the body again. |\n| `changelog / entry required` | No new line under `## [Unreleased]` in `CHANGELOG.md`, for a change outside `.github/`, lockfiles and lint config. | Add an entry in the repo's style (a `###` heading per package and version, then what a user sees). The `no-changelog` label is only for pure CI, lint or typo changes. |\n| markdownlint (MD024) | Two headings with the same text next to each other (siblings), usually two `### mzizi-skills` entries under Unreleased. | Make each heading descriptive and unique: package, version and what changed, as in `### mzizi-skills 0.18.0 - the dev skills for releases`. |\n| markdownlint (other) | A skipped heading level, a bad list marker, trailing spaces, a missing blank line around a list or fence. | `npx markdownlint-cli2 path/to/file.md` with the repo's `.markdownlint.jsonc` and fix what it names. |\n| yamllint | A line over 140 characters in a workflow, or a bad indent. | Break the line (a `run: \\|` block, or a folded `>-` scalar). `yamllint -c .yamllint.yaml .github/workflows` where yamllint is installed. |\n| `no agent-local files` | A personal or local agent file is in the diff: a `.claude/` path, `CLAUDE.local.md`, or a `CLAUDE.md` in a repo that gitignores it (agent-tools does). Some repos track `CLAUDE.md` and `AGENTS.md` as committed guides; the check there is the repo's own, and an edit to them needs a changelog entry. | `git rm --cached` the file and keep it local. Never commit a `.claude/` path, and do not edit a repo's guide just to make a PR pass. |\n| `oxfmt` / `vp fmt` | The file is not formatted the way CI's formatter writes it (tables, wrapping, trailing commas). | CI is the authority. Format only the files you changed, with the repo's own formatter, and read the diff. **Do not run `vp fmt` over a whole package**: it reformatted about 40 files in one session. |\n| CodeQL: bad HTML filtering regexp | A regex strips or matches an HTML comment with `-->` only; the browser also closes a comment with `--!>`. | Handle `--!>` as well as `-->`, or stop using a regex: find the opener with `indexOf` and the closer with `indexOf`, and cut between them. |\n| CodeQL: polynomial regular expression | A regex with nested or overlapping quantifiers (a trim like `\\s+$`, or `(a+)+`) backtracks on long input. | Replace the backtracking trim with a loop (`while (end > start && isSpace(s[end - 1])) end--`) or `indexOf`; keep any remaining regex linear and anchored. |\n| CodeQL: incomplete sanitization | A replace escapes one character (`*`) but not the backslash that escapes it. | Escape the backslash too (`/[\\\\*]/g`), or build the string without replacing. |\n| parity | `--strict` ignores `EXPECTED`, so every difference counts as unexplained. Strict runs on automated pin bumps and on PRs into `main`. | Run `npm run parity -- --strict` yourself. Fix the cause, or record the intended difference as a `PIN_<sha>` entry in `EXPECTED` and in the PR body. Prune stale entries before a release PR. |\n| rendered-content gate (`verify-rendered.py`, `check-facts.py`) | The live API changed (a count, a node total), so a number the gate expects moved on every branch. | Update the expected numbers there and the same counts in `AGENTS.md` and `README.md`, with a CHANGELOG entry. Fix the site from upstream, not the check. |\n| version policy on `staging` | `package.json` moved backwards, or a version was bumped that the workflow owns. The next staging patch comes from the tags. | Never lower a version. Read the latest tag and npm (`npm view <pkg> version`), keep `package.json` at or above them, and bump only the package you are releasing, to the next minor. |\n| merge conflict in `CHANGELOG.md` | Two PRs added entries under `## [Unreleased]`. | Merge the base branch into yours and **keep both sides**; keep headings unique. Never resolve by dropping the other PR's entry. |\n| rendered-geometry guard (`audit-ui.mjs --guard`) | Run against a plain static server such as `python3 -m http.server`: the site uses clean URLs (`build.format` file, so `/ecosystem` is `ecosystem.html`), which that server cannot resolve, so pages answer 404 and the guard reports false line-height failures (an \"Error code: 404\" page). | Run the guard against a Cloudflare preview URL, `wrangler dev` or `astro preview`, or the live site with `--url`, never a bare file server. A failure on a 404 page is a bad harness, not a pass or a real failure. |\n| `release-version-check` (release PRs) | `main` has commits `staging` may not have. | That is the `ship-release` skill: an owner decision (`forward-ported` label), never `--admin`. |\n\n## The PR body\n\n```markdown\nRefs #343\n\nWhat changed and why, in two or three sentences.\n\n## Site/docs/skills impact\n\nNone. (Or: what a reader of the site, docs or skills sees differently, and which pages or skills.)\n```\n\n- The issue line starts at the beginning of a line, alone. Substantial work has a tracking issue;\n a tiny fix may say `No issue: <reason>`.\n- Some repos ask for an impact heading in the body (agent-tools does); keep the exact heading.\n- Say how you tested it, and what you did not.\n- No secrets, tokens or credential status, and no personal information.\n\n## Merging\n\n- `staging` takes a **squash** through the merge queue; `main` takes its own method (see\n `ship-release`). Do not pick a method by hand.\n- GraphQL is blocked in cloud sessions, so enqueue with the GitHub MCP `enable_pr_auto_merge`\n tool or `gh api -X PUT repos/OWNER/REPO/pulls/N/ccr/auto_merge`.\n- Enqueue only past the merge gate: the work is complete, CI is green, it is verified at\n runtime, and `/code-review` has run with its findings resolved.\n- A dequeue with `MERGE_CONFLICT` means the base moved: merge the base into your branch,\n resolve (CHANGELOG: keep both sides) and enqueue again.\n\n## Do not\n\n- Do not skip, `.skip`, delete or loosen a test, a lint rule or a gate to go green.\n- Do not use `--admin`, or add a label (`forward-ported`, `no-changelog`) that the checks above\n do not support.\n- Do not run a repo-wide formatter locally, or edit generated files by hand.\n- Do not retry a red check without reading why it failed. A flake is proven by a re-run that\n goes green with no change, and you say so in the PR.\n\n## Checklist\n\n- [ ] `Refs #n` (or `Closes`, `Fixes`, `No issue: reason`) alone at the start of a line in the body.\n- [ ] A `## [Unreleased]` entry with a unique, descriptive heading; `no-changelog` only for CI, lint or typo.\n- [ ] No `.claude/` path, `CLAUDE.local.md` or gitignored `CLAUDE.md` in the diff.\n- [ ] Only the files I changed are formatted; the repo's `check` script passes locally.\n- [ ] No regex that can backtrack on user input; HTML comments handle `--!>`.\n- [ ] Parity and rendered-content gates run where they apply, and recorded differences are in the PR.\n- [ ] `package.json` versions did not move backwards.\n- [ ] `git show --stat HEAD` reviewed before the push; audit and generator output went outside the repo; no tracked file rewritten by a tool.\n- [ ] Nothing skipped, disabled or bypassed.",
284
+ "body_mdx": "# PR gates: make the first push green\n\n> **Covenant:** _A red check is information. Fix its cause; never switch the check off._\n\nMost red first pushes in the Mzizi repos fail on the same dozen checks. Each has a cause you can\nread before you push and a fix that leaves the check meaning what it meant. This skill lists\nthem. It is not a way around a gate: **never skip, disable or loosen a test or a check to go\ngreen, and never `--admin`.** If a gate is wrong, say so in the PR and let a person change it.\n\nEach repo's own `AGENTS.md` is the authority where it differs, and CI is the authority over\nanything you ran locally.\n\n## Before you push\n\n```bash\ngit status --short # only the files you meant\ngit diff --name-only origin/staging...HEAD | grep -E '(^|/)(CLAUDE\\.local\\.md|\\.claude/)' && echo \"STOP: agent-local file\"\nCHANGELOG_BASE=origin/staging bash scripts/changelog-gate.sh # where the repo has the gate (see below)\npnpm check # the repo's own check script, where it has one\n```\n\nBefore every push, read what the commit holds, not what you meant it to hold:\n\n```bash\ngit show --stat HEAD # every file and line count in the commit you are about to push\ngit diff --stat origin/staging...HEAD\n```\n\nA file you did not mean to touch, or a huge line count, is the signal. `git add -A` sweeps\nin whatever a tool rewrote: in one session an audit run rewrote a tracked file\n(`audit-out/run.json`, about 160,000 lines) and it landed in a PR, which failed the formatter\ncheck. Add files by name, and run audits and generators with their output **outside** the\nrepo (a `--out \"${TMPDIR:-/tmp}/audit-out\"` style option, or a temp directory), or first\nconfirm the tool does not rewrite tracked files. If a tracked file was changed by accident,\nrestore the original from the base branch:\n\n```bash\ngit checkout origin/staging -- path/to/file\n```\n\nNever delete a tracked file to make the diff smaller; that is a change too.\n\nThen write the PR body (below) and read the checks the REST way, because GraphQL is blocked in\ncloud sessions (so `gh pr checks` fails there):\n\n```bash\nSHA=$(gh api repos/OWNER/REPO/pulls/N --jq .head.sha)\ngh api \"repos/OWNER/REPO/commits/$SHA/check-runs?per_page=100\" --jq '.check_runs[] | [(.conclusion // .status), .name] | @tsv'\ngh api repos/OWNER/REPO/actions/jobs/JOB_ID/logs # a failed job's log (or the GitHub MCP get_job_logs tool)\n```\n\n## The checks that failed, and the fix\n\n| Check | Why it fails | Fix |\n| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `issue attached` | No line in the PR body **starts with** `Closes`, `Fixes`, `Refs` or `No issue:`. A mention in the middle of a sentence does not count. | Put `Refs #n` (or `Refs owner/repo#n`, `Closes #n`, `No issue: <reason>`) alone on its own line at the start of a line. It reads the body snapshot of the event: after fixing it, save the body again. |\n| `changelog / entry required` | No new line under `## [Unreleased]` in `CHANGELOG.md`, for a change outside `.github/`, lockfiles and lint config. | Add an entry in the repo's own heading style: copy the newest entry (`agent-tools`: `### <package> <version> — what changed (date, [#n](url))`; `mzizi-registry`, `mzizi-api`, `mzizi-docs`, `mzizi-site`: `### <Kind> — what changed (date, ref)`; `packages-npm`: `## [<name> <version>] - <date>` for a release, entries under Unreleased naming the package). `ship-release` has the table. The `no-changelog` label is only for pure CI, lint or typo changes. |\n| markdownlint (MD024) | Two headings with the same text next to each other (siblings), usually two `### mzizi-skills` entries under Unreleased. | Make each heading descriptive and unique: package, version and what changed, as in `### mzizi-skills 0.19.0 — corrections to the release skills`. |\n| markdownlint (other) | A skipped heading level, a bad list marker, trailing spaces, a missing blank line around a list or fence. | `npx markdownlint-cli2 path/to/file.md` with the repo's `.markdownlint.jsonc` and fix what it names. |\n| yamllint | A line over 140 characters in a workflow, or a bad indent. | Break the line (a `run: \\|` block, or a folded `>-` scalar). `yamllint -c .yamllint.yaml .github/workflows` where yamllint is installed. |\n| `no agent-local files` | A personal or local agent file is in the diff: a `.claude/` path, `CLAUDE.local.md`, or a `CLAUDE.md` in a repo that gitignores it (agent-tools does). Some repos track `CLAUDE.md` and `AGENTS.md` as committed guides; the check there is the repo's own, and an edit to them needs a changelog entry. | `git rm --cached` the file and keep it local. Never commit a `.claude/` path, and do not edit a repo's guide just to make a PR pass. |\n| `oxfmt` / `vp fmt` | The file is not formatted the way CI's formatter writes it (tables, wrapping, trailing commas). | CI is the authority. Format only the files you changed, with the repo's own formatter, and read the diff. **Do not run `vp fmt` over a whole package**: it reformatted about 40 files in one session. |\n| CodeQL: bad HTML filtering regexp | A regex strips or matches an HTML comment with `-->` only; the browser also closes a comment with `--!>`. | Handle `--!>` as well as `-->`, or stop using a regex: find the opener with `indexOf` and the closer with `indexOf`, and cut between them. |\n| CodeQL: polynomial regular expression | A regex with nested or overlapping quantifiers (a trim like `\\s+$`, or `(a+)+`) backtracks on long input. | Replace the backtracking trim with a loop (`while (end > start && isSpace(s[end - 1])) end--`) or `indexOf`; keep any remaining regex linear and anchored. |\n| CodeQL: incomplete sanitization | A replace escapes one character (`*`) but not the backslash that escapes it. | Escape the backslash too (`/[\\\\*]/g`), or build the string without replacing. |\n| parity | `--strict` ignores `EXPECTED`, so every difference counts as unexplained. In mzizi-api, strict runs on PRs into `main`; PRs and merge queues into `staging` run without it (mzizi-api #126), because `staging` is ahead of production. Check the repo's `parity.yml` on the branch you target. | Run `npm run parity -- --strict` yourself. Fix the cause, or record the intended difference as a `PIN_<sha>` entry in `EXPECTED` and in the PR body. Prune stale entries before a release PR. |\n| rendered-content gate (`verify-rendered.py`, `check-facts.py`) | The live API changed (a count, a node total), so a number the gate expects moved on every branch. | Update the expected numbers there and the same counts in `AGENTS.md` and `README.md`, with a CHANGELOG entry. Fix the site from upstream, not the check. |\n| version policy on `staging` | `package.json` moved backwards, or a version was bumped that the workflow owns. The next staging patch comes from the tags. | Never lower a version. Read the latest tag and npm (`npm view <pkg> version`), keep `package.json` at or above them, and bump only the package you are releasing, to the next minor. |\n| merge conflict in `CHANGELOG.md` | Two PRs added entries under `## [Unreleased]`. | Merge the base branch into yours and **keep both sides**; keep headings unique. Never resolve by dropping the other PR's entry. |\n| rendered-geometry guard (`audit-ui.mjs --guard`) | Run against a plain static server such as `python3 -m http.server`: the site uses clean URLs (`build.format` file, so `/ecosystem` is `ecosystem.html`), which that server cannot resolve, so pages answer 404 and the guard reports false line-height failures (an \"Error code: 404\" page). | Run the guard against a Cloudflare preview URL, `wrangler dev` or `astro preview`, or the live site with `--url`, never a bare file server. A failure on a 404 page is a bad harness, not a pass or a real failure. |\n| `release-version-check` (release PRs) | `main` has commits `staging` may not have. | That is the `ship-release` skill: an owner decision (`forward-ported` label), never `--admin`. |\n\n## The PR body\n\n```markdown\nRefs #343\n\nWhat changed and why, in two or three sentences.\n\n## Site/docs/skills impact\n\nNone. (Or: what a reader of the site, docs or skills sees differently, and which pages or skills.)\n```\n\n- The issue line starts at the beginning of a line, alone. Substantial work has a tracking issue;\n a tiny fix may say `No issue: <reason>`.\n- Some repos ask for an impact heading in the body (agent-tools does); keep the exact heading.\n- Say how you tested it, and what you did not.\n- No secrets, tokens or credential status, and no personal information.\n\n## Merging\n\n- `staging` takes a **squash** through the merge queue; `main`'s queue is set to MERGE (see\n `ship-release`). Do not pick a method by hand.\n- Enqueue with `gh pr merge <n> --squash --auto`: that is every repo's convention. GraphQL is\n blocked in cloud sessions, so there `gh pr merge --auto` fails; the fallback is the GitHub MCP\n `enable_pr_auto_merge` tool or `gh api -X PUT repos/OWNER/REPO/pulls/N/ccr/auto_merge -f merge_method=squash`.\n- Enqueue only past the merge gate: the work is complete, CI is green, it is verified at\n runtime, and `/code-review` has run with its findings resolved.\n- A dequeue with `MERGE_CONFLICT` means the base moved: merge the base into your branch,\n resolve (CHANGELOG: keep both sides) and enqueue again.\n\n## Do not\n\n- Do not skip, `.skip`, delete or loosen a test, a lint rule or a gate to go green.\n- Do not use `--admin`, or add a label (`forward-ported`, `no-changelog`) that the checks above\n do not support.\n- Do not run a repo-wide formatter locally, or edit generated files by hand.\n- Do not retry a red check without reading why it failed. A flake is proven by a re-run that\n goes green with no change, and you say so in the PR.\n\n## Checklist\n\n- [ ] `Refs #n` (or `Closes`, `Fixes`, `No issue: reason`) alone at the start of a line in the body.\n- [ ] A `## [Unreleased]` entry with a unique, descriptive heading; `no-changelog` only for CI, lint or typo.\n- [ ] No `.claude/` path, `CLAUDE.local.md` or gitignored `CLAUDE.md` in the diff.\n- [ ] Only the files I changed are formatted; the repo's `check` script passes locally.\n- [ ] No regex that can backtrack on user input; HTML comments handle `--!>`.\n- [ ] Parity and rendered-content gates run where they apply, and recorded differences are in the PR.\n- [ ] `package.json` versions did not move backwards.\n- [ ] `git show --stat HEAD` reviewed before the push; audit and generator output went outside the repo; no tracked file rewritten by a tool.\n- [ ] Nothing skipped, disabled or bypassed.",
285
285
  "source": "mzizi-dev/agent-tools/mzizi-skills/skills/pr-gates",
286
286
  "files": []
287
287
  }
288
288
  ];
289
289
  /** Bundle version, from mzizi-skills/index.json. */
290
- export const SKILLS_VERSION = "0.18.0";
290
+ export const SKILLS_VERSION = "0.20.0";
291
291
  //# sourceMappingURL=skills.js.map