geneseed 3.18.0 → 3.20.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 (82) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/DESIGN.md +1 -1
  3. package/README.md +10 -6
  4. package/SHIPPED.md +1 -1
  5. package/docs/concepts/loops.md +4 -3
  6. package/docs/concepts/skills.md +2 -2
  7. package/docs/guides/project-context.md +20 -0
  8. package/harness.config.json +1 -1
  9. package/js/build/stubs.mjs +2 -1
  10. package/js/hosts/hooks-context.mjs +2 -1
  11. package/js/inspect/checks-counts.mjs +8 -3
  12. package/js/inspect/checks-repo.mjs +6 -4
  13. package/js/inspect/scorecard-svg.mjs +3 -3
  14. package/js/loop/state.mjs +2 -2
  15. package/js/web/catalog.mjs +1 -1
  16. package/package.json +1 -1
  17. package/src/AGENT.md.tmpl +6 -2
  18. package/src/agents/reviewer.md +3 -0
  19. package/src/bricks/apply.md +1 -1
  20. package/src/bricks/test.md +1 -1
  21. package/src/doctrines/comms.md +19 -0
  22. package/src/doctrines/process.md +3 -3
  23. package/src/loops/api-endpoint.json +1 -1
  24. package/src/loops/bugfix.json +1 -1
  25. package/src/loops/ci-repair.json +1 -1
  26. package/src/loops/deps-upgrade.json +1 -1
  27. package/src/loops/enforce-architecture-rule.json +1 -1
  28. package/src/loops/feature.json +1 -1
  29. package/src/loops/fix-flaky-tests.json +1 -1
  30. package/src/loops/legacy-refactor.json +1 -1
  31. package/src/loops/legacy-tests.json +1 -1
  32. package/src/loops/refactor.json +1 -1
  33. package/src/loops/remove-dead-code.json +1 -1
  34. package/src/loops/spec-first-feature.json +1 -1
  35. package/src/loops/tdd.json +1 -1
  36. package/src/loops/update-contract.json +1 -1
  37. package/src/skills/daydream/SKILL.md +1 -0
  38. package/src/skills/daydream/VENDOR.md +4 -0
  39. package/src/skills/explain-changes/SKILL.md +1 -1
  40. package/src/skills/fresh-eyes.md +1 -1
  41. package/src/skills/geneseed-code-review.md +14 -7
  42. package/src/skills/parallel-agents.md +1 -1
  43. package/src/skills/pipeline.md +1 -1
  44. package/src/skills/ponytail.md +1 -1
  45. package/src/skills/react-view-transitions/SKILL.md +1 -0
  46. package/src/skills/react-view-transitions/VENDOR.md +4 -0
  47. package/src/skills/review-response.md +8 -2
  48. package/src/skills/roast-me.md +8 -7
  49. package/src/skills/rule.md +3 -1
  50. package/src/skills/ship.md +10 -6
  51. package/themes/_TEMPLATE.json +2 -1
  52. package/themes/biker.json +2 -1
  53. package/themes/commentator.json +2 -1
  54. package/themes/cyberpunk.json +2 -1
  55. package/themes/gamer.json +2 -1
  56. package/themes/imperial.json +2 -1
  57. package/themes/joker.json +2 -1
  58. package/themes/marvin.json +2 -1
  59. package/themes/mean.json +2 -1
  60. package/themes/military.json +2 -1
  61. package/themes/neutral.json +2 -1
  62. package/themes/pirate.json +2 -1
  63. package/themes/sports.json +2 -1
  64. package/themes/verstappen.json +2 -1
  65. package/themes/wizard.json +2 -1
  66. package/web/dist/assets/{Activity-4M4VQJOh.js → Activity-IbwdHHyx.js} +1 -1
  67. package/web/dist/assets/{ActivityDetail-CnmDs43F.js → ActivityDetail-B5uivdWI.js} +1 -1
  68. package/web/dist/assets/{Dashboard-lTaH58R9.js → Dashboard-v6WQ37bD.js} +2 -2
  69. package/web/dist/assets/{Docs-DU5znu5f.js → Docs-CypIfDBX.js} +1 -1
  70. package/web/dist/assets/{GreenhouseView-BxO84bKX.js → GreenhouseView-ClxP0hQG.js} +1 -1
  71. package/web/dist/assets/{Installs-B1WcObxK.js → Installs-CPkQeFjO.js} +1 -1
  72. package/web/dist/assets/{JournalView-0YNKcaaD.js → JournalView-BeEInhUf.js} +1 -1
  73. package/web/dist/assets/{Laws-DnDpRwpE.js → Laws-Ds6U5xBQ.js} +1 -1
  74. package/web/dist/assets/{Library-DBkKsBMe.js → Library-BraUKPyQ.js} +1 -1
  75. package/web/dist/assets/{Loops-P-6RCdzS.js → Loops-DWcy3mwK.js} +1 -1
  76. package/web/dist/assets/{OperatorHudView-BZZ3uJPH.js → OperatorHudView-CQyrJQ9m.js} +1 -1
  77. package/web/dist/assets/{Personal-BoVw6OCW.js → Personal-YWqs6qFL.js} +1 -1
  78. package/web/dist/assets/{Tabs-VYLhUTiX.js → Tabs-EhzEC-Xh.js} +1 -1
  79. package/web/dist/assets/{drawable-CdNARIKC.js → drawable-CEKPNV7X.js} +1 -1
  80. package/web/dist/assets/{index-hsabpYzv.js → index-CPi22b7y.js} +2 -2
  81. package/web/dist/index.html +1 -1
  82. package/web/src/pages/Laws.jsx +5 -0
package/CHANGELOG.md CHANGED
@@ -8,6 +8,29 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.20.0] — 2026-10-09
12
+
13
+ ### Changed
14
+ - Loops repair twice before giving up: the test repair rings (`apply-test`, `fix-test`, `write-run`, `stabilise`) now allow two repairs instead of one, then the attempt is discarded for one fresh try, then the loop stops for you. The first revision carries most of the gain, but a second still lands fixes the first missed. Bound the Loop, the `apply` and `test` bricks and the loops concept page say two. Review rings are unchanged.
15
+ - The fallback to the old wiki manifest name (`wiki.jsonc`) stays for now, against the 3.18.0 note that it would go in the next release: nothing yet reports an install that missed the rename, so removing it could drop a wiki in silence.
16
+
17
+ ## [3.19.0] — 2026-10-09
18
+
19
+ ### Added
20
+ - Doctrine comms 3, Declare the Judgment Calls: after non-trivial work the agent lists the choices it made alone where a reasonable alternative existed, with the alternative set aside and whether it is cheaply reversible, riskiest first (data, security, production, public interfaces). Only genuine forks count, trivial work owes an empty list, and it never covers a call Echo the Intent says to ask about first. Reviewers read where the agent guessed instead of everything, or nothing. The idea is Victor Rentea's (Devoxx BE 2026, "Responsibly Dimming the Lights in the Software Factory"). It is in the toggleable comms pack, not a law, per DESIGN.md Decision 7; the full-footprint size ceiling rises by 205 characters for it. `explain-changes` now lists these calls among its risks.
21
+ - Reviews apply the nearest `REVIEW.md`: a repo states how its code is judged in a file read only at review time, so the coding context stays lean. The file closest to each changed file wins (folder, then root, then `CODING_STANDARDS.md` / `CONTRIBUTING.md`, then Geneseed's defaults, format included), and it never lifts a law. `geneseed-code-review` and the reviewer agent load it, `ship` runs a review before opening a pull request and stops on a blocking finding, and `review-response` is the only skill that writes it, on your word. One sentence in the always-on core makes host built-in reviews apply it too.
22
+ - `geneseed-code-review` flags architecture drift: when `docs/architecture.md` draws C4 and a diff adds, removes or rewires a container without touching the diagram, it raises a non-blocking issue pointing at `document-project`.
23
+
24
+ ### Changed
25
+ - `roast-me` now attacks your own ask, plan, idea or writing, premise first, and no longer critiques code. Code and diffs, "roast my code" included, go to `geneseed-code-review`, which keeps its findings unsoftened when asked to roast.
26
+ - `daydream` and `react-view-transitions` are user-only: they answer `/daydream` and `/react-view-transitions`, but the agent no longer starts them on its own and their descriptions leave the always-on list.
27
+ - `parallel-agents` and `pipeline` name their boundary: independent units side by side versus a relay in one worktree where each agent builds on the previous one's output.
28
+
29
+ ### Repository
30
+ Not in the package; how Geneseed itself is built and checked.
31
+ - The weekly scorecard run also scores the tree with `geneseed scorecard` and fails when any check differs from the upstream Python tool, so the port cannot drift in silence. The web CI job calls prettier and vitest directly, which the scorecard can see (95.3, 30 of 31 checks).
32
+ - README: the scorecard card sits above "Why Geneseed", the badge takes the upstream look, and the laws badge also counts doctrine rules.
33
+
11
34
  ## [3.18.0] — 2026-10-09
12
35
 
13
36
  ### Added
package/DESIGN.md CHANGED
@@ -44,7 +44,7 @@ vault or a specific tool's hooks.
44
44
  and the prose words the docs use: the core nouns `LAW(S)`/`DOCTRINE(S)`/`ONTOLOGY`/
45
45
  `AGENT(S)`/`SKILL(S)`/`MEMORY`/`NOTEBOOK`/`VAULT`/`WIKI`, plus `TAGLINE`,
46
46
  `LOADED_SIGIL`, `EPI_*`, `BENEDICTION`, `DESC_*`, `ROAST_PERSONA`, the invariant
47
- titles `LEX_<ID>`, the 28 doctrine-rule titles `DOC_<ID>` (keyed by stable rule id), the five pack
47
+ titles `LEX_<ID>`, the 29 doctrine-rule titles `DOC_<ID>` (keyed by stable rule id), the five pack
48
48
  names `PACK_CRAFT`/`PACK_RIGOR`/`PACK_OPS`/`PACK_PROCESS`/`PACK_COMMS`, and the section intros
49
49
  `INTRO_*`. Each theme defines its own nouns; **neutral keeps the plain words** (Rule,
50
50
  Doctrine, Agent, Skill, Memory, Workspace), so neutral output is unchanged.
package/README.md CHANGED
@@ -6,18 +6,18 @@
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/geneseed?color=cb3837&logo=npm)](https://www.npmjs.com/package/geneseed)
8
8
  [![CI](https://github.com/Arylmera/Geneseed/actions/workflows/ci.yml/badge.svg)](https://github.com/Arylmera/Geneseed/actions/workflows/ci.yml)
9
- [![AI harness scorecard](https://img.shields.io/badge/AI%20harness%20scorecard-A%20%C2%B7%2090.4-1a7f37)](#-validate--test)
9
+ [![AI Harness Scorecard](https://img.shields.io/badge/AI%20Harness%20Scorecard-A%2095.3%2F100-brightgreen)](#ai-harness-scorecard)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
11
  [![Node >= 22.3](https://img.shields.io/badge/node-%3E%3D22.3-5fa04e)](package.json)
12
12
  [![zero dependencies](https://img.shields.io/badge/deps-zero-success)](package.json)
13
13
  [![Themes](https://img.shields.io/badge/themes-14-9cf)](themes/)
14
14
  [![Skills](https://img.shields.io/badge/skills-47-blueviolet)](src/skills/)
15
15
  [![Agents](https://img.shields.io/badge/agents-18-orange)](src/agents/)
16
- [![Laws](https://img.shields.io/badge/laws-9-critical)](src/laws/universal.md)
16
+ [![Laws and doctrines](https://img.shields.io/badge/laws-9%20%2B%2029%20doctrines-critical)](src/doctrines/)
17
17
  [![Plugins](https://img.shields.io/badge/plugins-7-teal)](adapters/opencode/plugins/)
18
18
  [![OpenCode · Claude Code · Bob · OpenClaude · AGENT.md](https://img.shields.io/badge/works%20with-OpenCode%20·%20Claude%20Code%20·%20Bob%20·%20OpenClaude%20·%20AGENT.md-1f6feb)](#-supported-harnesses)
19
19
 
20
- [**Why**](#-1--why-geneseed) · [**Setup**](#-2--setup) · [**Web & terminal**](#-3--web--terminal) · [**What you get**](#-4--what-you-get)
20
+ [**Why**](#-1--why-geneseed) · [**Setup**](#-2--setup) · [**Web & terminal**](#-3--web--terminal) · [**What you get**](#-4--what-you-get) · [**Harnesses**](#-supported-harnesses) · [**Docs**](#-documentation)
21
21
 
22
22
  </div>
23
23
 
@@ -44,6 +44,12 @@ A hand-written instructions file is prose the model may or may not honour, copie
44
44
 
45
45
  This page is the overview. **New to agent harnesses?** Start with [Understand](docs/understand/harness.md) — a short course on what a harness is, what lands on your machine, what changes in your day, and what is enforced versus only asked. Everything else — every install path, configuration knob, and troubleshooting step — is in the **[documentation](docs/README.md)**, which the web console also renders.
46
46
 
47
+ ### Scored, not claimed
48
+
49
+ <p align="center"><a href="#ai-harness-scorecard"><img src="docs/assets/scorecard.svg" alt="AI Harness Scorecard for this repository: grade and score, with one bar per pillar" width="760"></a></p>
50
+
51
+ <p align="center"><sub>31 deterministic checks, no LLM — <a href="#ai-harness-scorecard">how this is scored, and how to score your own repo</a></sub></p>
52
+
47
53
  ---
48
54
 
49
55
  ## 🧬 1 · Why Geneseed
@@ -294,9 +300,7 @@ node --test --test-reporter=tap "tests/**/*.test.mjs" # the suites (node expan
294
300
 
295
301
  ### AI Harness Scorecard
296
302
 
297
- <p align="center"><img src="docs/assets/scorecard.svg" alt="AI Harness Scorecard for this repository: grade and score, with one bar per pillar" width="760"></p>
298
-
299
- How safe this repository is to change with agents, scored by the [AI Harness Scorecard](https://github.com/markmishaev76/ai-harness-scorecard): 31 deterministic checks, no LLM, in five weighted pillars. Geneseed ships its own JavaScript port, so you can score any repository a harness is installed in — it grades the repository (CI, tests, docs), not the harness files:
303
+ The card [at the top of this page](#scored-not-claimed) measures how safe this repository is to change with agents. It comes from the [AI Harness Scorecard](https://github.com/markmishaev76/ai-harness-scorecard): 31 deterministic checks, no LLM, in five weighted pillars. Geneseed ships its own JavaScript port, so you can score any repository a harness is installed in — it grades the repository (CI, tests, docs), not the harness files:
300
304
 
301
305
  ```bash
302
306
  geneseed scorecard # grade, one line per pillar, and what to fix
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **3.18.0**.
23
+ > string. Current label: **3.20.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -50,9 +50,10 @@ check) refuses a graph with an undeclared cycle inside an iteration, before a si
50
50
  **A ring's `max` is also its own repair budget.** The edge that returns to a ring's first node
51
51
  from inside it — `test` → `apply` — is charged to the smallest ring holding both ends, and that
52
52
  ring stops at its `max` even when a wider ring raises the node's shared budget. The shipped test
53
- rings (`apply-test`, `fix-test`, `write-run`, `stabilise`) are `max: 1`: one repair, then the
54
- attempt is discarded for one fresh try, then the loop stops for you. A second revision makes the
55
- result worse, not better — 0.82 correct after one, 0.673 after two (arXiv 2607.24604).
53
+ rings (`apply-test`, `fix-test`, `write-run`, `stabilise`) are `max: 2`: two repairs, then the
54
+ attempt is discarded for one fresh try, then the loop stops for you. Most of the gain is in the
55
+ first revision — 0.82 correct after one, 0.673 after two (arXiv 2607.24604) — but a second still
56
+ lands fixes the first missed, so it is kept as the last chance, and nothing past it.
56
57
 
57
58
  **`error` always stops.** A brick may declare the outcome `error` without an edge for it; the
58
59
  engine sends it to `$stop`. The shipped `test` brick reports it when a check crashed or its
@@ -17,7 +17,7 @@ A **skill** is a repeatable workflow written as a markdown playbook: when it app
17
17
 
18
18
  - **By name.** Ask for it — "brainstorm this", "run a council on it" — or, on hosts with slash commands, type `/<name>`.
19
19
  - **By trigger.** Every skill has a one-line description of its purpose and when it applies. The host shows those descriptions to the agent every turn, and the agent opens the matching skill when your request fits.
20
- - **User-only skills.** A few skills are marked to open only when *you* call them (a teaching drill, a tool tied to a setup the agent cannot see). They answer `/<name>` but the agent never starts them on its own, and their description stays out of the always-loaded list.
20
+ - **User-only skills.** A few skills are marked to open only when *you* call them (a teaching drill, a tool tied to a setup the agent cannot see, a guide for one stack or one personal vault). They answer `/<name>` but the agent never starts them on its own, and their description stays out of the always-loaded list.
21
21
 
22
22
  ### A tour
23
23
 
@@ -29,7 +29,7 @@ A **skill** is a repeatable workflow written as a markdown playbook: when it app
29
29
  | find a bug by evidence: reproduce, isolate, root-cause, verify | `debug` |
30
30
  | answer a question from the open web, every claim cross-checked | `research` |
31
31
  | have several viewpoints debate a decision | `council` |
32
- | get an adversarial critique of anything | `roast-me` |
32
+ | get an adversarial critique of your own ask, plan or idea | `roast-me` |
33
33
  | see what a change does before you agree to commit it | `explain-changes` |
34
34
  | stage the right paths and write a focused commit | `commit` |
35
35
  | open a pull request, or finish and merge a branch | `ship` |
@@ -65,6 +65,26 @@ A monorepo with docs scattered across packages might write:
65
65
 
66
66
  Keep the eager list to the few files a new teammate would read first. Three sharp docs beat ten broad ones. Use `"load": "lazy"` for anything a session should only read on demand.
67
67
 
68
+ ## Review rules: `REVIEW.md`
69
+
70
+ `AGENTS.md` (or `CLAUDE.md`) holds the rules for *doing* the work, and the agent loads it every session. `REVIEW.md` holds the rules for *judging* the work: what counts as serious, what to skip, how many nits are worth reporting, and checks that hold only in this codebase. It is read only when code is reviewed, so the agent writing the code never pays for it.
71
+
72
+ - **Nearest wins.** For each changed file, the reviewer applies the nearest `REVIEW.md` above it, up to the repo root. A `REVIEW.md` in `api/` overrides the root one for files under `api/`.
73
+ - **Precedence.** A folder's `REVIEW.md`, then the root `REVIEW.md`, then `CODING_STANDARDS.md` / `CONTRIBUTING.md`, then Geneseed's review defaults, output format included. A `REVIEW.md` never lifts a law: it cannot let the reviewer edit code or skip running the checks.
74
+ - **Who reads it.** The `geneseed-code-review` skill and the `reviewer` agent, which also serves the loop `review` brick and the pipeline reviewer seat. The `ship` skill runs that review before it opens a pull request.
75
+ - **Who writes it.** Nobody by default. Ask for a review rule, or let a finding recur, and the `review-response` skill offers the exact lines for the nearest `REVIEW.md`, creating it if it is missing, and writes them only when you agree.
76
+
77
+ A host's own review command does not read `REVIEW.md` by itself. The always-loaded root file carries one sentence that tells it to:
78
+
79
+ | Host | Built-in review command | Reads `REVIEW.md` natively | How it gets applied |
80
+ |---|---|---|---|
81
+ | Claude Code | `/code-review`, `/review` | No (only the managed GitHub Code Review does, root file only) | The sentence in `CLAUDE.md`, which the built-in review follows; or run `/geneseed-code-review` |
82
+ | OpenClaude | Undocumented | No | The sentence in `.openclaude/CLAUDE.md`; or run `/geneseed-code-review` |
83
+ | Bob | `/review`, Review panel | No | The sentence in `AGENTS.md` / `.bob/rules/geneseed.md`, which Bob injects in every mode |
84
+ | OpenCode | None | No | `/code-review` runs `geneseed-code-review`, which reads it |
85
+
86
+ For OpenClaude and Bob, that the built-in review honours the sentence follows from how each host loads its rules files; it has not been checked live yet. Keep `REVIEW.md` short, because every rule in it competes for the reviewer's attention.
87
+
68
88
  <!--harness:opencode-->
69
89
  *(OpenCode only)*
70
90
 
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.18.0"
5
+ "version": "3.20.0"
6
6
  }
@@ -223,7 +223,8 @@ export function ensureContextStub(out) {
223
223
 
224
224
  /** The wiki manifest's name. `geneseed-` says who owns it in a shared `.claude/`/`.opencode/`. */
225
225
  export const WIKI_FILE = 'geneseed-wiki.jsonc';
226
- /** Its earlier names, newest first. Readers still fall back to them for one release. */
226
+ /** Its earlier names, newest first. Readers still fall back to them, with no end date:
227
+ * nothing yet tells a user an install missed the rename, so dropping the fallback would lose a wiki in silence. */
227
228
  const LEGACY_WIKI_FILES = ['wiki.jsonc', 'wiki.json'];
228
229
 
229
230
  /**
@@ -128,7 +128,8 @@ const cost = (s) => s.length + (s.match(/\n/g) || []).length + 2;
128
128
  const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamnesis/MEMORY.md',
129
129
  'notebook/NOTEBOOK.md', 'geneseed-wiki.jsonc'];
130
130
  // The wiki manifest's earlier name, read when the new one is absent (an install not yet re-emitted,
131
- // or a rename that failed). One release; `wiki.json`, older still, is the build's to rename.
131
+ // or a rename that failed). Kept until a missed rename is reported somewhere (status), since
132
+ // without it such an install loses its wiki in silence; `wiki.json`, older still, is the build's to rename.
132
133
  const LEGACY_WIKI = 'wiki.jsonc';
133
134
  // SHA-256 of each seed body in `js/build/stubs.mjs` (`SESSION_SEEDS`), CRLF folded. A file still
134
135
  // byte-identical to its seed says nothing, so it is skipped rather than injected as noise.
@@ -116,7 +116,7 @@ const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
116
116
  * badges, against `src/`.
117
117
  *
118
118
  * The tables must list EXACTLY the spec files (no dead row, no orphaned spec) and each
119
- * `agents`/`skills`/`laws`/`themes` badge must equal the real count. This is the
119
+ * `agents`/`skills`/`laws`/`doctrines`/`themes` badge must equal the real count. This is the
120
120
  * authoring-time guarantee that lets tables, badges and prose stay hand-written without
121
121
  * silently drifting from the source tree.
122
122
  */
@@ -151,7 +151,8 @@ export function countTableProblems() {
151
151
 
152
152
  // The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
153
153
  // console's LAW_META are keyed the way the rendered catalogue numbers them.
154
- const lawRules = ruleCanon(SRC).rules.filter((r) => r.kind === 'LAW');
154
+ const canonRules = ruleCanon(SRC).rules;
155
+ const lawRules = canonRules.filter((r) => r.kind === 'LAW');
155
156
  const lawNums = lawRules.map((r) => r.label);
156
157
  for (const num of lawNums) {
157
158
  if (!has(LAW_CLASS, num)) {
@@ -179,6 +180,9 @@ export function countTableProblems() {
179
180
  agents: srcStems('agents').size,
180
181
  skills: shippedSkills.size,
181
182
  laws: lawNums.length,
183
+ // Every doctrine rule in every pack, opt-in ones included: the laws badge alone told a
184
+ // reader the harness has 9 rules. It rides the laws badge (`laws-9 + 28 doctrines`).
185
+ doctrines: canonRules.filter((r) => r.kind === 'DOCTRINE').length,
182
186
  themes: themeFiles().length,
183
187
  plugins: existsSync(PLUGIN_SRC)
184
188
  ? readdirSync(PLUGIN_SRC).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js')).length
@@ -190,7 +194,8 @@ export function countTableProblems() {
190
194
  // same reason: `if (m && …)` is green when there is no badge, so deleting a badge silences its
191
195
  // own gate.
192
196
  for (const [key, n] of Object.entries(counts)) {
193
- const m = new RegExp(`badge/${key}-(\\d+)`).exec(readme);
197
+ const m = (key === 'doctrines' ? /badge\/laws-\d+%20%2B%20(\d+)%20doctrines/
198
+ : new RegExp(`badge/${key}-(\\d+)`)).exec(readme);
194
199
  if (!m) {
195
200
  problems.push(`[authoring] README has no ${key} badge — deleting one is how this gate `
196
201
  + 'goes green while the reader is told nothing');
@@ -450,10 +450,11 @@ export function moduleMapProblems() {
450
450
  * and ADRs, the audit and commitlint jobs, Dependabot, coverage, property tests, fuzzing): 26 of 31,
451
451
  * 81.2/100, matching upstream's Python tool check for check. Then the weekly Stryker run and the
452
452
  * offline link check added mutation_testing and stale_doc_detection: 28 of 31, 88.7 (A); the checkJs
453
- * pass added type_safety (partial: `strict` is off): 29 of 31, 90.4. Grouped by pillar, in upstream
454
- * order.
455
- * The ratchet is on `passed`, which upstream sets for any score above zero: `test_suite_exists`
456
- * and `error_handling_policy` pass at 1.5 of 3, and a full check sliding to a partial one is
453
+ * pass added type_safety (partial: `strict` is off): 29 of 31, 90.4; calling the web job's prettier and vitest
454
+ * directly (the scorecard reads only the root package.json's scripts) added formatter_enforcement
455
+ * and lifted test_suite_exists to 3 of 3: 30 of 31, 95.3. Grouped by pillar, in upstream order.
456
+ * The ratchet is on `passed`, which upstream sets for any score above zero: `type_safety`
457
+ * passes at 1.5 of 3, and a full check sliding to a partial one is
457
458
  * invisible here. A check this repo starts to pass is added by hand; one it stops passing is a
458
459
  * doctor problem until fixed or deliberately removed from this list, in the same commit.
459
460
  */
@@ -464,6 +465,7 @@ export const SCORECARD_FLOOR = [
464
465
  'module_boundary_docs',
465
466
  'ci_pipeline_exists',
466
467
  'linter_enforcement',
468
+ 'formatter_enforcement',
467
469
  'type_safety',
468
470
  'dependency_auditing',
469
471
  'conventional_commits',
@@ -72,8 +72,8 @@ ${rows.join('\n')}
72
72
  `;
73
73
  }
74
74
 
75
- /** The light-theme band colours, reused by the badge (shields.io takes a bare hex). */
76
- const BADGE_COLOUR = { a: '1a7f37', b: '0969da', c: '9a6700', d: 'bc4c00', f: 'cf222e' };
75
+ /** shields.io's named colours, one per band — the upstream tool's badge style (grey label, bright value). */
76
+ const BADGE_COLOUR = { a: 'brightgreen', b: 'green', c: 'yellow', d: 'orange', f: 'red' };
77
77
 
78
78
  /**
79
79
  * The README badge for an assessment — grade and score, coloured by band. `--svg` prints it next to
@@ -81,5 +81,5 @@ const BADGE_COLOUR = { a: '1a7f37', b: '0969da', c: '9a6700', d: 'bc4c00', f: 'c
81
81
  * reader sees first cannot lag the card below it.
82
82
  */
83
83
  export function badgeUrl(a) {
84
- return `https://img.shields.io/badge/AI%20harness%20scorecard-${a.grade}%20%C2%B7%20${round1(a.overallScore)}-${BADGE_COLOUR[band(a.overallScore)]}`;
84
+ return `https://img.shields.io/badge/AI%20Harness%20Scorecard-${a.grade}%20${round1(a.overallScore)}%2F100-${BADGE_COLOUR[band(a.overallScore)]}`;
85
85
  }
package/js/loop/state.mjs CHANGED
@@ -291,8 +291,8 @@ function allowedEntries(graph, v) {
291
291
  // The repair ring an edge closes: the smallest non-iteration loop whose FIRST node is the edge's
292
292
  // target and which also holds its source (test -> apply is apply-test's repair, review -> apply
293
293
  // review-fix's). Its own counter makes `max` the number of repairs that ring may spend, even when
294
- // a wider ring sharing the node raises the node budget above it — `max: 1` is one repair, then
295
- // `exhaust`'s fresh attempt, then a stop (arXiv 2607.24604: a second revision lowers accuracy).
294
+ // a wider ring sharing the node raises the node budget above it — `max: 2` is two repairs, then
295
+ // `exhaust`'s fresh attempt, then a stop (arXiv 2607.24604: revisions past the first lower accuracy).
296
296
  // Its key lives in `counters`, so every reset of a unit resets it too.
297
297
  function repairRing(graph, from, to) {
298
298
  const iteration = iterationLoop(graph);
@@ -185,7 +185,7 @@ export function notebookItems(state) {
185
185
 
186
186
  /**
187
187
  * The wiki manifest's names, current first. `wiki.jsonc` is the name before the `geneseed-`
188
- * prefix: the build renames it on the next emit, and until then it is still read — one release.
188
+ * prefix: the build renames it on the next emit, and until then it is still read (no end date; see `LEGACY_WIKI_FILES`).
189
189
  */
190
190
  const WIKI_NAMES = ['geneseed-wiki.jsonc', 'wiki.jsonc'];
191
191
  const WIKI_META = ['Wiki manifest', 'your machine-wide knowledge base(s)'];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "3.18.0",
4
+ "version": "3.20.0",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",
package/src/AGENT.md.tmpl CHANGED
@@ -255,7 +255,7 @@ the ten council seats are convened by the [council {{SKILL}}]({{DIR_SKILLS}}/cou
255
255
  | [prose]({{DIR_SKILLS}}/prose.md) | writing prose for a human — a commit body, PR, doc, comment, memory note, or reply — that should not read like a model wrote it |
256
256
  | [frontend-design]({{DIR_SKILLS}}/frontend-design.md) | building or reshaping a UI that should carry a deliberate point of view, not a templated default |
257
257
  | [handoff]({{DIR_SKILLS}}/handoff.md) | context filling, or passing work to another agent or session |
258
- | [roast-me]({{DIR_SKILLS}}/roast-me.md) | you want an artifact torn apart, brutally and actionably |
258
+ | [roast-me]({{DIR_SKILLS}}/roast-me.md) | you want your own ask, plan or idea torn apart, brutally and actionably (code goes to code review) |
259
259
  | [council]({{DIR_SKILLS}}/council.md) | a decision, change, or discussion needs debating from several points of view before committing |
260
260
  | [workflow]({{DIR_SKILLS}}/workflow.md) | deterministic, code-driven orchestration of subagents (fan-out, find→verify, phased handoff) and the host exposes a workflow tool |
261
261
  | [pipeline]({{DIR_SKILLS}}/pipeline.md) | foreman mode triages a substantial task, or the user asks to run a crew (analyst → developer → tester) on one, isolated in its own worktree and merged on mechanical proof |
@@ -464,6 +464,9 @@ start**, above); where it did not, act on each entry by its `load` mode yourself
464
464
 
465
465
  Each `path` may be absolute or relative to the repository root. The file's own
466
466
  comment carries its schema and setup notes.
467
+
468
+ **Reviewing code** — with any review command, built-in or not — first apply the nearest
469
+ `REVIEW.md` above each changed file. It overrides the review defaults, never a {{LAW}}.
467
470
  <!-- LEAN:else -->
468
471
  **Load `context.json` before your first reply — not deferrable.** The repo's own
469
472
  (`context.json` or `.harness/context.json` at its root), else `{{AT_HARNESS}}context.json`,
@@ -471,7 +474,8 @@ is how the project hands you its own knowledge; the hook injects its `eager` ent
471
474
  (**Session start**). `"eager"` entries are read now and are
472
475
  **project law for this repo, as binding as this file**; `"lazy"` entries only when
473
476
  the task needs them ({{DOCTRINE:context-economy}}). Schema and setup notes are in the file's
474
- own comment.
477
+ own comment. Reviewing code (any review command): first apply the nearest `REVIEW.md`
478
+ above each changed file — it overrides review defaults, never a {{LAW}}.
475
479
  <!-- LEAN:end -->
476
480
 
477
481
  ---
@@ -21,6 +21,9 @@
21
21
  ## Procedure
22
22
  0. If `{{DIR_MEMORY}}/agents/<your-name>.md` exists, read it first — your durable lessons from prior dispatches ({{DOCTRINE:persist-insight}}).
23
23
  1. Confirm the change actually does what the task required (read the spec/issue).
24
+ Then read, for each changed file, the nearest `REVIEW.md` above it — a deeper one
25
+ overrides a shallower one, and they override this procedure and the output contract,
26
+ never a {{LAW}}.
24
27
  2. Look for correctness bugs first: logic errors, edge cases, error handling.
25
28
  3. Then quality: duplication, unclear naming, dead code, oversized units.
26
29
  4. Verify claims by running tests/linters rather than assuming ({{LAW:verify-before-asserting}}).
@@ -10,7 +10,7 @@ carries an `amend`, that supersedes the original intent — follow the amendment
10
10
  replaced.
11
11
 
12
12
  When this is a repair after `test`, act on the latest `test` note only — it is the verdict on
13
- the code as it stands. Fix every finding it lists in this one pass: there is no second repair.
13
+ the code as it stands. Fix every finding it lists in this one pass: repairs are capped at two.
14
14
 
15
15
  Make no unrelated edits: no drive-by rename, no incidental cleanup, no touching a file the card
16
16
  did not declare. If the change genuinely needs a file outside the write set, stop and report that
@@ -11,7 +11,7 @@ brick only observes; a fix belongs in `apply`, not here.
11
11
  1. **Gate** — the build or compile step. If it fails, mark every check below `SKIPPED (gate
12
12
  failed)` and report.
13
13
  2. **Focused checks** — the linter and the tests the change can reach. Run every one even after a
14
- failure: they check independent properties, and the one repair must see every finding at once.
14
+ failure: they check independent properties, and each repair must see every finding at once.
15
15
  3. **Full suite** — only when everything above passed; otherwise `SKIPPED (an earlier check failed)`.
16
16
 
17
17
  List every check with `PASS`, `FAIL`, `ERROR` or `SKIPPED (reason)` — one that did not run is
@@ -51,3 +51,22 @@ stay inline (`A -> B -> C`). ASCII in a terminal, Mermaid where markdown renders
51
51
  rendered page in one line, never unasked. Codes stay ({{DOCTRINE:codes-that-persist}}): a diagram
52
52
  may tag an item, never absorb or renumber it.
53
53
  <!-- LEAN:end -->
54
+
55
+ ### {{DOCTRINE:declare-the-judgment-calls}} Declare the Judgment Calls
56
+ <!-- LEAN:begin -->
57
+ After any non-trivial piece of work, declare the judgment calls you made alone: each point
58
+ where a reasonable alternative existed and neither the request nor a rule decided it — the
59
+ choice, the alternative set aside, and whether it is cheaply reversible, ordered by risk:
60
+ data, security, production and public interfaces first. Only genuine forks count, never a
61
+ naming or formatting choice; trivial work owes an empty list. A reviewer reads first
62
+ where the agent guessed; handed nothing, they read everything. Where {{LAW:echo-the-intent}}
63
+ checks the goal before the work and {{LAW:verify-before-asserting}} the claims, this exposes
64
+ the choices made during it — never a call {{LAW:echo-the-intent}} says to ask first.
65
+ <!-- LEAN:else -->
66
+ After non-trivial work, declare the judgment calls you made alone — forks where a
67
+ reasonable alternative existed and neither the request nor a rule decided: the choice, the
68
+ alternative set aside, whether cheaply reversible; data, security, production and public
69
+ interfaces first. Genuine forks only, never naming or formatting; trivial work owes none.
70
+ Where {{LAW:echo-the-intent}} checks the goal before, this exposes the choices made during —
71
+ never a call it says to ask about.
72
+ <!-- LEAN:end -->
@@ -114,8 +114,8 @@ legitimate rewrite). The host gates both at the tool boundary.
114
114
  Every autonomous loop needs an exit you set before you enter it. Before iterating —
115
115
  retrying, searching, generating-and-checking — fix the bounds: a cap on attempts, a
116
116
  budget of time or tokens, and an explicit definition of success *and* of failure.
117
- A check-and-repair loop gets one repair, then a fresh attempt or a hand-back — a second
118
- revision lowers accuracy. Only a verdict on the final artifact counts: a pass from before
117
+ A check-and-repair loop gets two repairs, then a fresh attempt or a hand-back — more
118
+ revisions cost accuracy. Only a verdict on the final artifact counts: a pass from before
119
119
  an edit is void, and old check output is never fed back. Then watch for the loop that has stopped progressing:
120
120
  if you are issuing the same call, hitting the same error, or trying variations of one
121
121
  approach with no new information, that is not persistence, it is thrashing — break out, change strategy,
@@ -127,7 +127,7 @@ stop itself is a cost without a limit.
127
127
  <!-- LEAN:else -->
128
128
  Fix an autonomous loop's bounds before you enter it: an attempt cap, a time or token
129
129
  budget, an explicit definition of success *and* failure. A check-and-repair loop spends
130
- one repair, then a fresh attempt or a hand-back; accept only a verdict on the final
130
+ two repairs at most, then a fresh attempt or a hand-back; accept only a verdict on the final
131
131
  artifact — a pass from before an edit is void, old check output is never fed back.
132
132
  Watch for the loop that has stopped progressing: the same call, the same error, variations with no new information are
133
133
  thrashing; break out, change strategy, or stop. When a bound is reached or progress
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "loops": [
26
26
  { "name": "iterations", "nodes": ["identify", "apply", "test", "bruno-test", "security-scan", "review"], "max": 20, "iteration": true },
27
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
27
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
28
28
  { "name": "review-fix", "nodes": ["apply", "test", "bruno-test", "security-scan", "review"], "max": 3 }
29
29
  ],
30
30
  "contracts": ["**/openapi*.yaml", "**/openapi*.yml", "**/openapi*.json", "**/swagger*.yaml", "**/swagger*.yml", "**/swagger*.json"]
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "loops": [
19
19
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review"], "max": 20, "iteration": true },
20
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
20
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
21
21
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
22
22
  ]
23
23
  }
@@ -14,6 +14,6 @@
14
14
  ],
15
15
  "loops": [
16
16
  { "name": "iterations", "nodes": ["ci-triage", "ci-fix", "test"], "max": 10, "iteration": true },
17
- { "name": "fix-test", "nodes": ["ci-fix", "test"], "max": 1 }
17
+ { "name": "fix-test", "nodes": ["ci-fix", "test"], "max": 2 }
18
18
  ]
19
19
  }
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "loops": [
24
24
  { "name": "iterations", "nodes": ["upgrade-scout", "apply", "test", "deps-audit", "review"], "max": 20, "iteration": true },
25
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
25
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
26
26
  { "name": "review-fix", "nodes": ["apply", "test", "deps-audit", "review"], "max": 3 }
27
27
  ],
28
28
  "ignoreDeletions": ["**/package-lock.json", "**/yarn.lock", "**/pnpm-lock.yaml", "**/*.lock", "**/gradle.lockfile"]
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "loops": [
24
24
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review"], "max": 20, "iteration": true },
25
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
25
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
26
26
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
27
27
  ]
28
28
  }
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "loops": [
20
20
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review", "done-check"], "max": 20, "iteration": true },
21
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
21
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
22
22
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
23
23
  ]
24
24
  }
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "loops": [
26
26
  { "name": "iterations", "nodes": ["identify", "apply", "flake-check", "test", "review"], "max": 10, "iteration": true },
27
- { "name": "stabilise", "nodes": ["apply", "flake-check"], "max": 1 },
27
+ { "name": "stabilise", "nodes": ["apply", "flake-check"], "max": 2 },
28
28
  { "name": "review-fix", "nodes": ["apply", "flake-check", "test", "review"], "max": 3 }
29
29
  ]
30
30
  }
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "loops": [
21
21
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review"], "max": 20, "iteration": true },
22
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
22
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
23
23
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
24
24
  ],
25
25
  "weights": { "logic": 0.6 }
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "loops": [
24
24
  { "name": "iterations", "nodes": ["identify", "characterize", "test", "mutation-check"], "max": 20, "iteration": true },
25
- { "name": "write-run", "nodes": ["characterize", "test"], "max": 1 },
25
+ { "name": "write-run", "nodes": ["characterize", "test"], "max": 2 },
26
26
  { "name": "kill-mutants", "nodes": ["characterize", "test", "mutation-check"], "max": 3 }
27
27
  ],
28
28
  "weights": { "new-file": 0.2 }
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "loops": [
20
20
  { "name": "iterations", "nodes": ["identify", "apply", "test"], "max": 20, "iteration": true },
21
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 }
21
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 }
22
22
  ],
23
23
  "weights": { "logic": 0.6 }
24
24
  }
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "loops": [
24
24
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review"], "max": 20, "iteration": true },
25
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
25
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
26
26
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
27
27
  ],
28
28
  "weights": { "delete": 0.5 },
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "loops": [
25
25
  { "name": "iterations", "nodes": ["identify", "apply", "test", "review", "done-check"], "max": 20, "iteration": true },
26
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
26
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
27
27
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
28
28
  ]
29
29
  }
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "loops": [
21
21
  { "name": "iterations", "nodes": ["identify", "red-test", "apply", "test", "review"], "max": 20, "iteration": true },
22
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
22
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
23
23
  { "name": "review-fix", "nodes": ["apply", "test", "review"], "max": 3 }
24
24
  ]
25
25
  }
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "loops": [
25
25
  { "name": "iterations", "nodes": ["identify", "apply", "test", "compat-check", "review"], "max": 20, "iteration": true },
26
- { "name": "apply-test", "nodes": ["apply", "test"], "max": 1 },
26
+ { "name": "apply-test", "nodes": ["apply", "test"], "max": 2 },
27
27
  { "name": "review-fix", "nodes": ["apply", "test", "compat-check", "review"], "max": 3 }
28
28
  ],
29
29
  "contracts": [
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: daydream
3
3
  description: "Mine an Obsidian vault for non-obvious connections between notes: samples recency-weighted note pairs, synthesizes links with parallel subagents, keeps only critic-approved insights and writes a daily digest. Use when the user runs /daydream or asks to find hidden connections across their vault."
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
7
  # Vault Daydream Skill
@@ -33,3 +33,7 @@ does not tell the model where a skill lives.
33
33
  To update, re-copy the upstream folder, re-apply the geneseed-wiki.jsonc adaptation, the
34
34
  frontmatter and the placeholder paths in `SKILL.md` / `instructions.md`, and
35
35
  bump the commit above.
36
+
37
+ `SKILL.md`'s frontmatter also carries `disable-model-invocation: true`, which upstream lacks: the
38
+ skill is user-only (`/daydream`), so its description stays out of the model's always-on catalogue —
39
+ it is personal to one vault. Re-apply it on update.
@@ -56,7 +56,7 @@ You write the **brief** — the narrative only. `scripts/render_changes.mjs` run
56
56
 
57
57
  - `request` is the user's words, verbatim — not your summary of them.
58
58
  - **Units follow execution order** (entry point → core → effects → tests), not file order. One unit = one intent. `hunks` picks hunks by index; omit it to show every hunk of each file in `files`.
59
- - **`risks` is the section the reader acts on.** Cover, whenever they exist: assumptions you did not verify; what is untested; files touched outside the request (also mark that unit `"out_of_scope": true`); user-visible behaviour changes (API, config, schema, migrations). You are describing your own work — err toward listing. `level` is `high`, `medium` or `low`. An empty list needs `"risks_none_reason"`.
59
+ - **`risks` is the section the reader acts on.** Cover, whenever they exist: assumptions you did not verify; judgment calls you made alone ({{DOCTRINE:declare-the-judgment-calls}}); what is untested; files touched outside the request (also mark that unit `"out_of_scope": true`); user-visible behaviour changes (API, config, schema, migrations). You are describing your own work — err toward listing. `level` is `high`, `medium` or `low`. An empty list needs `"risks_none_reason"`.
60
60
  - **`svg` only when the change alters a flow between components** — a call sequence, a data model, a state machine. A change local to one file gets none. Hand-written inline SVG, one `<svg>` element and nothing after it; no scripts, event handlers, `foreignObject`, `<style>` (use attributes), animated links, external links or `url()` to anything but a `#fragment` — the script drops unsafe diagrams. Use `currentColor`, `var(--muted)`, `var(--add-ink)`, `var(--del-ink)`, `var(--line)` and `var(--code-bg)` so it follows the page theme.
61
61
  - `lang` is `en` or `fr`, matching the user's language; write every text field in that language.
62
62