@freetesh/zeno 0.7.0 → 0.8.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.
@@ -7,8 +7,8 @@
7
7
  {
8
8
  "name": "zeno",
9
9
  "source": "./",
10
- "description": "Shared standards, searchable product knowledge and decision checks for every product repo."
10
+ "description": "Keeps your domain knowledge and AI rules local and enforces them, so Claude hallucinates less."
11
11
  }
12
12
  ],
13
- "description": "zeno: standards, searchable product knowledge and a decision gate for Claude Code."
13
+ "description": "zeno: Keep your domain knowledge and AI rules in your repo, and enforce them. Less AI guessing, faster delivery."
14
14
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "zeno",
4
- "version": "0.7.0",
5
- "description": "Shared standards, searchable product knowledge and decision checks for every product repo.",
4
+ "version": "0.8.0",
5
+ "description": "Keeps your domain knowledge and AI rules local and enforces them, so Claude hallucinates less.",
6
6
  "author": {
7
7
  "name": "Ritesh Firodiya",
8
8
  "email": "40817224+ritesh-firodiya@users.noreply.github.com"
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # zeno
2
2
 
3
- Standards, searchable product knowledge and a decision gate for teams that
4
- build with Claude Code. zeno keeps several products consistent as they evolve,
5
- keeps what you decided findable, and makes Claude weigh a request against your
6
- rules and history before it acts on it.
3
+ A productivity tool for the whole product development lifecycle (PDLC), from
4
+ idea to release. zeno keeps your domain knowledge, decisions and AI rules local,
5
+ in your own repos, and enforces them: Claude searches them before it answers,
6
+ weighs a request against them before it acts, and `zeno check` fails when code
7
+ or docs break them. The AI hallucinates less, and your team ships faster.
7
8
 
8
9
  It has two halves that read the same config:
9
10
 
@@ -42,7 +43,7 @@ It has two halves that read the same config:
42
43
 
43
44
  ```bash
44
45
  # plugin (from GitHub)
45
- claude plugin marketplace add ritesh-firodiya/zeno-ai
46
+ claude plugin marketplace add ritesh-firodiya/zeno
46
47
  claude plugin install zeno@zeno
47
48
 
48
49
  # CLI (Node 22+)
@@ -83,6 +84,11 @@ Run the CLI from source (Node 23.6+ runs the TypeScript directly):
83
84
  `pnpm install && ln -s "$PWD/cli/src/index.ts" ~/.local/bin/zeno`.
84
85
  `pnpm test`, `pnpm typecheck` and `pnpm build` run from the repo root.
85
86
 
87
+ **Releasing.** Bump `version` in `package.json` and `.claude-plugin/plugin.json`,
88
+ then push or merge to `main`. The `release` workflow runs the tests, publishes
89
+ `@freetesh/zeno` to npm, tags `vX.Y.Z` and creates the GitHub release. A push
90
+ without a bump releases nothing. It needs the `NPM_TOKEN` Actions secret.
91
+
86
92
  ## CLI
87
93
 
88
94
  ```bash
@@ -117,13 +123,22 @@ commands: # your own checkers, run by `zeno check`
117
123
  doctor:
118
124
  tools:
119
125
  - { tool: pnpm, why: product scripts, hint: npm i -g pnpm }
126
+ wiki:
127
+ types: # added to the six built-in page types
128
+ - type: postmortem
129
+ folder: postmortems
130
+ reason: incidents need a timeline and a fix
131
+ sections: [Summary, Timeline, Root cause, Fix, Sources] # zeno check requires each `## ` heading
132
+ context:
133
+ folders: # added to the ten .context folders
134
+ - { folder: research, reason: interview notes, kept apart from raw/ }
120
135
  ```
121
136
 
122
137
  ```yaml
123
138
  # <repo>/.claude/zeno.yaml — written by `zeno init`
124
139
  zeno: 0.6.0
125
140
  kind: expo-app # expo-app | web | content
126
- modules: [docs, knowledge, metrics, code, listing] # all optional; design is added when .context/designs exists
141
+ modules: [docs, knowledge, code, listing] # all optional; design is added when .context/designs exists, metrics only when asked
127
142
  packs: [expo]
128
143
  checks:
129
144
  stale-copies: { enabled: false, reason: "Cursor is still used here" }
@@ -132,7 +147,8 @@ weigh:
132
147
  ```
133
148
 
134
149
  Scalars: the most specific layer wins. `packs` and `standards` add up across
135
- layers; `commands` and `doctor.tools` merge by id. A check turned off must
150
+ layers; `commands`, `doctor.tools`, `wiki.types` and `context.folders` merge
151
+ by name. Built-in types and folders cannot be removed or redefined. A check turned off must
136
152
  carry a `reason:`, and `zeno check` lists every override.
137
153
 
138
154
  ## Standards
@@ -152,12 +168,12 @@ your own.
152
168
 
153
169
  ## Evals
154
170
 
155
- `evals/` runs five requests against a fixture product (`evals/fixtures/streaks`)
171
+ `evals/` runs seven requests against a fixture product (`evals/fixtures/streaks`)
156
172
  with and without the plugin, each in a sandbox seeded with the fixture and a
157
173
  local QMD index:
158
174
 
159
175
  ```bash
160
- claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd__*' --runs 2
176
+ claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd__*' Write Edit --runs 2
161
177
  ```
162
178
 
163
179
  ## Layout
@@ -165,7 +181,7 @@ claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd_
165
181
  ```
166
182
  .claude-plugin/ plugin manifest and marketplace
167
183
  skills/ weigh, standards, ask, ingest, decision, wiki-lint
168
- agents/ challenger — independent second opinion for large requests
184
+ agents/ challenger — independent second opinion for too-large requests
169
185
  hooks/ session start: constitution titles, qmd refresh
170
186
  standards/ default standards
171
187
  packs/ vendor doc packs
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- const ALLOWED_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
3
+ import { CONTEXT_FOLDERS } from "../config/schema.js";
4
4
  const KNOWLEDGE_CONTEXT = ["raw", "wiki", "features"];
5
5
  /** What .context must hold: README.md and tasks.md always, the knowledge folders only when docs or knowledge is on. */
6
6
  export function requiredContext(modules) {
@@ -11,7 +11,7 @@ const ROOT_FILES = ["README.md", "tasks.md"];
11
11
  export const contextLayout = {
12
12
  id: "context-layout",
13
13
  module: null,
14
- description: ".context/ has the required folders, only the ten allowed ones, and only README.md + tasks.md at its root",
14
+ description: ".context/ has the required folders, only the allowed ones (the ten plus context.folders), and only README.md + tasks.md at its root",
15
15
  run({ repo, config }) {
16
16
  const context = join(repo, ".context");
17
17
  const finding = (level, file, message) => ({
@@ -23,11 +23,12 @@ export const contextLayout = {
23
23
  if (!existsSync(context))
24
24
  return [finding("error", ".context", "no .context/ folder — run `zeno init`")];
25
25
  const findings = requiredContext(config.modules).filter((name) => !existsSync(join(context, name))).map((name) => finding("error", `.context/${name}`, `required ${name} is missing`));
26
+ const allowed = [...CONTEXT_FOLDERS, ...config.context.folders.map((entry) => entry.folder)];
26
27
  for (const entry of readdirSync(context, { withFileTypes: true })) {
27
28
  if (entry.name === ".DS_Store")
28
29
  continue;
29
- if (entry.isDirectory() && !ALLOWED_FOLDERS.includes(entry.name)) {
30
- findings.push(finding("error", `.context/${entry.name}`, `${entry.name}/ is not one of the ten folders`));
30
+ if (entry.isDirectory() && !allowed.includes(entry.name)) {
31
+ findings.push(finding("error", `.context/${entry.name}`, `${entry.name}/ is not an allowed folder — move it, or add it under context.folders in zeno.yaml`));
31
32
  }
32
33
  if (entry.isFile() && !ROOT_FILES.includes(entry.name)) {
33
34
  findings.push(finding("error", `.context/${entry.name}`, "only README.md and tasks.md may sit at .context/ root"));
@@ -3,6 +3,7 @@ import { canonicalValues } from "./canonical-values.js";
3
3
  import { contextLayout } from "./context-layout.js";
4
4
  import { staleCopies, standardsCopies } from "./copies.js";
5
5
  import { commandChecks } from "./external.js";
6
+ import { metricsDefinitions } from "./metrics.js";
6
7
  import { wikiFrontmatter } from "./wiki-frontmatter.js";
7
8
  export const ALL_CHECKS = [
8
9
  contextLayout,
@@ -11,5 +12,6 @@ export const ALL_CHECKS = [
11
12
  standardsCopies,
12
13
  staleCopies,
13
14
  architecture,
15
+ metricsDefinitions,
14
16
  commandChecks,
15
17
  ];
@@ -0,0 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const DEFINITIONS = ".context/metrics/definitions.yml";
4
+ export const metricsDefinitions = {
5
+ id: "metrics-definitions",
6
+ module: "metrics",
7
+ description: "metrics/definitions.yml names the numbers the weigh gate measures value against",
8
+ run({ repo }) {
9
+ if (existsSync(join(repo, DEFINITIONS)))
10
+ return [];
11
+ // A warning: most products turn metrics on before they define a single number.
12
+ return [{ check: "metrics-definitions", level: "warn", file: DEFINITIONS, message: "no metric definitions — the weigh gate cannot name the metric a request moves" }];
13
+ },
14
+ };
@@ -2,15 +2,8 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { listFiles } from "../lib/files.js";
4
4
  import { parseFrontmatter } from "../lib/frontmatter.js";
5
+ import { WIKI_FOLDER_BY_TYPE } from "../config/schema.js";
5
6
  const REQUIRED_FIELDS = ["id", "type", "status", "last-verified"];
6
- const FOLDER_BY_TYPE = {
7
- concept: "concepts",
8
- entity: "entities",
9
- surface: "surfaces",
10
- flow: "flows",
11
- decision: "decisions",
12
- synthesis: "synthesis",
13
- };
14
7
  const STALE_AFTER_DAYS = 90;
15
8
  const DAY_MS = 86_400_000;
16
9
  const NOT_PAGES = ["index.md", "CLAUDE.md"];
@@ -26,13 +19,16 @@ export const wikiFrontmatter = {
26
19
  id: "wiki-frontmatter",
27
20
  module: "docs",
28
21
  description: "every wiki page has id, type, status and last-verified; type matches folder; ids are unique; nothing stale",
29
- run({ repo, today }) {
22
+ run({ repo, config, today }) {
23
+ const folderByType = { ...WIKI_FOLDER_BY_TYPE, ...Object.fromEntries(config.wiki.types.map((entry) => [entry.type, entry.folder])) };
24
+ // Built-in types' sections are linted by the wiki-lint skill; added types carry theirs in config.
25
+ const sectionsByType = new Map(config.wiki.types.map((entry) => [entry.type, entry.sections]));
30
26
  const findings = [];
31
27
  const add = (level, file, message) => findings.push({ check: "wiki-frontmatter", level, file, message });
32
28
  const filesById = new Map();
33
29
  const stale = [];
34
30
  for (const file of wikiPages(repo)) {
35
- const { data } = parseFrontmatter(readFileSync(join(repo, file), "utf8"));
31
+ const { data, body } = parseFrontmatter(readFileSync(join(repo, file), "utf8"));
36
32
  if (!data) {
37
33
  add("error", file, "no frontmatter");
38
34
  continue;
@@ -41,9 +37,16 @@ export const wikiFrontmatter = {
41
37
  if (missing.length > 0)
42
38
  add("error", file, `missing ${missing.join(", ")}`);
43
39
  const folder = file.split("/")[2];
44
- const expected = FOLDER_BY_TYPE[String(data.type)];
40
+ const expected = folderByType[String(data.type)];
45
41
  if (data.type && expected !== folder)
46
42
  add("error", file, `type ${data.type} does not belong in ${folder}/`);
43
+ const sections = sectionsByType.get(String(data.type));
44
+ if (sections) {
45
+ const headings = new Set(body.split("\n").flatMap((line) => (line.startsWith("## ") ? [line.slice(3).trim()] : [])));
46
+ const absent = sections.filter((section) => !headings.has(section));
47
+ if (absent.length > 0)
48
+ add("error", file, `missing sections: ${absent.join(", ")}`);
49
+ }
47
50
  if (data.status === "superseded" && !data["superseded-by"])
48
51
  add("error", file, "superseded without superseded-by");
49
52
  const verified = asDate(data["last-verified"]);
@@ -22,7 +22,7 @@ async function askAnswers(inferred, packs) {
22
22
  if (cancelled(kind))
23
23
  return null;
24
24
  const modules = await prompts.multiselect({
25
- message: "Which modules? All optional. (docs = wiki, design = HTML designs, knowledge = search, specs, metrics, code = LSP, architecture, listing)",
25
+ message: "Which modules? All optional. (docs = wiki, design = HTML designs, knowledge = search, metrics = definitions.yml, code = LSP, architecture, listing)",
26
26
  initialValues: inferred.config.modules,
27
27
  options: MODULES.map((value) => ({ value, label: value })),
28
28
  required: false,
@@ -62,7 +62,8 @@ function mergeBy(lists, key) {
62
62
  /**
63
63
  * Reads the estate, product and local zeno.yaml layers over defaults inferred
64
64
  * from the repo. Scalars: the most specific layer wins. Lists of names add up
65
- * (packs, standards); lists of records merge by their key (commands, tools).
65
+ * (packs, standards); lists of records merge by their key (commands, tools,
66
+ * wiki types, context folders).
66
67
  */
67
68
  export function loadConfig(repo, options = {}) {
68
69
  const estateRoot = options.estateRoot === undefined ? findEstate(dirname(resolve(repo))) : options.estateRoot;
@@ -86,6 +87,8 @@ export function loadConfig(repo, options = {}) {
86
87
  doctor: { tools: mergeBy(pick("doctor").map((doctor) => doctor?.tools ?? []), (tool) => tool.tool) },
87
88
  checks: Object.assign({}, ...pick("checks")),
88
89
  weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
90
+ wiki: { types: mergeBy(pick("wiki").map((wiki) => wiki?.types ?? []), (entry) => entry.type) },
91
+ context: { folders: mergeBy(pick("context").map((context) => context?.folders ?? []), (entry) => entry.folder) },
89
92
  };
90
93
  if (Object.keys(config.expo ?? {}).length === 0)
91
94
  delete config.expo;
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
2
  export const KINDS = ["expo-app", "web", "content"];
3
- export const MODULES = ["docs", "design", "knowledge", "specs", "metrics", "code", "architecture", "listing"];
3
+ export const MODULES = ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing"];
4
+ /** The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`. */
5
+ export const WIKI_FOLDER_BY_TYPE = {
6
+ concept: "concepts",
7
+ entity: "entities",
8
+ surface: "surfaces",
9
+ flow: "flows",
10
+ decision: "decisions",
11
+ synthesis: "synthesis",
12
+ };
13
+ /** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
14
+ export const CONTEXT_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
4
15
  const checkOverride = z
5
16
  .object({ enabled: z.boolean().default(true), reason: z.string().min(1).optional() })
6
17
  .refine((check) => check.enabled || check.reason !== undefined, {
@@ -16,6 +27,22 @@ const command = z
16
27
  if: z.string().optional(),
17
28
  })
18
29
  .strict();
30
+ const kebab = z.string().regex(/^[a-z0-9-]+$/, "use lowercase-kebab");
31
+ /** An addition to zeno's layout says why, so products drifting apart is written down. */
32
+ const reason = z.string().min(1);
33
+ const wikiType = z
34
+ .object({
35
+ type: kebab,
36
+ folder: kebab,
37
+ reason,
38
+ /** The `## ` headings every page of this type must have, as the schema lists them for the built-in six. */
39
+ sections: z.array(z.string().min(1)).min(1, "list the `## ` sections every page of this type must have"),
40
+ })
41
+ .strict()
42
+ .refine((entry) => !(entry.type in WIKI_FOLDER_BY_TYPE) && !Object.values(WIKI_FOLDER_BY_TYPE).includes(entry.folder), {
43
+ message: "a built-in wiki type or its folder cannot be redefined — add a new one",
44
+ });
45
+ const contextFolder = z.object({ folder: kebab, reason }).strict();
19
46
  const tool = z.object({ tool: z.string(), why: z.string(), hint: z.string() }).strict();
20
47
  /** One layer of `.claude/zeno.yaml` as written: every field optional. */
21
48
  export const layerSchema = z
@@ -33,11 +60,13 @@ export const layerSchema = z
33
60
  doctor: z.object({ tools: z.array(tool) }).partial(),
34
61
  checks: z.record(z.string(), checkOverride),
35
62
  weigh: z.object({ weights: z.record(principleId, z.number().nonnegative()) }).partial(),
63
+ wiki: z.object({ types: z.array(wikiType) }).partial(),
64
+ context: z.object({ folders: z.array(contextFolder) }).partial(),
36
65
  })
37
66
  .partial()
38
67
  .strict();
39
68
  export const MODULES_BY_KIND = {
40
- "expo-app": ["docs", "knowledge", "metrics", "code", "listing"],
41
- web: ["docs", "knowledge", "metrics", "code"],
69
+ "expo-app": ["docs", "knowledge", "code", "listing"],
70
+ web: ["docs", "knowledge", "code"],
42
71
  content: ["docs", "knowledge"],
43
72
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@freetesh/zeno",
3
- "version": "0.7.0",
4
- "description": "zeno CLI: set up, check and sync product repos against the shared standards.",
3
+ "version": "0.8.0",
4
+ "description": "zeno CLI: keep your domain knowledge and AI rules in your repo, and enforce them.",
5
5
  "license": "MIT",
6
6
  "author": "Ritesh Firodiya",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/ritesh-firodiya/zeno-ai.git"
9
+ "url": "git+https://github.com/ritesh-firodiya/zeno.git"
10
10
  },
11
11
  "type": "module",
12
12
  "bin": {
@@ -9,7 +9,7 @@ Decision to record: `$ARGUMENTS`
9
9
 
10
10
  ## Steps
11
11
 
12
- 1. Read the wiki schema (`../../standards/wiki-schema.md`, relative to this skill) § "`decision`" for the frontmatter and the required sections. If `.context/wiki/decisions/` already holds pages, open a recent one and match it.
12
+ 1. Get the shape. If `.context/wiki/decisions/` already holds pages, open the most recent one and match its frontmatter and sections — that is enough. Only when there is none, read two parts of the wiki schema (the `wiki-schema.md` that `zeno standards` lists — a team's own replaces zeno's; without the CLI, `../../standards/wiki-schema.md` relative to this skill): § "Frontmatter" and § "`decision`". Find their line numbers with Grep and Read just those ranges, not the whole file.
13
13
 
14
14
  2. Parse `$ARGUMENTS`:
15
15
  - If it contains ` — ` (em-dash with spaces), split on the first occurrence: left = title, right = body.
@@ -9,7 +9,7 @@ Source to ingest: `$ARGUMENTS`
9
9
 
10
10
  ## Steps
11
11
 
12
- 1. **Read the wiki schema (`../../standards/wiki-schema.md`, relative to this skill) first.** It defines page types, frontmatter, required sections, ID conventions, and lint rules. Every page you write must conform.
12
+ 1. **Read the wiki schema (the `wiki-schema.md` that `zeno standards` lists — a team's own replaces zeno's; without the CLI, `../../standards/wiki-schema.md` relative to this skill) first.** It defines page types, frontmatter, required sections, ID conventions, and lint rules. Every page you write must conform.
13
13
 
14
14
  2. **Read the raw source** at the path in `$ARGUMENTS`. Accepted: anything under `.context/raw/` or `.context/designs/`, and application source in this repository. Refuse paths outside the repo.
15
15
 
@@ -20,12 +20,13 @@ Source to ingest: `$ARGUMENTS`
20
20
  - A new tier / product / canonical value → `entity` page
21
21
  - A user journey description → `flow` page
22
22
  - Don't force one source into one page — a meeting note can produce multiple pages.
23
+ - Page types are the six in the schema plus any listed under `wiki.types` in the repo's or the estate's `.claude/zeno.yaml`, each with its folder and its required `sections` (use exactly those `## ` headings, in that order; its ID is `<TYPE>.<SLUG>`). Use an added type only when the source fits it.
23
24
 
24
25
  4. **For each page you will create:**
25
- - Pick the ID per the wiki schema (`../../standards/wiki-schema.md`, relative to this skill) § "ID conventions". IDs are uppercase, dot-separated, never change.
26
+ - Pick the ID per the wiki schema § "ID conventions". IDs are uppercase, dot-separated, never change.
26
27
  - Pick the filename: the slug portion of the ID, lowercase, kebab-case, `.md` extension.
27
28
  - Check whether the page already exists. If yes, **update** rather than overwrite: preserve `id`, `supersedes`, `superseded-by`; refresh `last-verified` to today.
28
- - Use the required sections for that page type — verbatim from the wiki schema (`../../standards/wiki-schema.md`, relative to this skill). Missing a required section is a lint failure.
29
+ - Use the required sections for that page type — verbatim from the wiki schema. Missing a required section is a lint failure.
29
30
  - Fill `Related` with at least two `[[wikilinks]]`. A link to a page that doesn't exist yet is OK — `/wiki-lint` will flag it as a TODO.
30
31
  - List the source path under `Sources`, so a future `/wiki-lint` can tell which raw files nothing has read.
31
32
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: weigh
3
- description: Decision gate run BEFORE acting on a non-trivial request in a product repo — a new feature, screen or product, a new dependency or library, a migration or rewrite, a schema, money, pricing or store change, a release or deploy, deleting something, or any change touching more than one file or more than one product. Weighs the request against the zeno constitution, the product's wiki and ADRs, existing features and tasks, cost vs value, and writing less code, then returns do it / do it differently / push back with citations. Also use when the user types /weigh or asks "should we…", "is it worth…", "does this make sense".
3
+ description: Decision gate run BEFORE acting on a non-trivial request in a product repo — a new feature, screen or product, a new dependency or library, a migration or rewrite, a schema, money, pricing or store change, a release or deploy, deleting something, or a change that spans more than one product. Weighs the request against the zeno constitution, the product's wiki and ADRs, existing features and tasks, cost vs value, and writing less code, then returns do it / do it differently / push back with citations. Also use when the user types /weigh or asks "should we…", "is it worth…", "does this make sense".
4
4
  ---
5
5
 
6
6
  # Weigh before acting
@@ -29,8 +29,10 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
29
29
  in its area.
30
30
 
31
31
  3. **Domain.** Search before you judge. Run `mcp__plugin_zeno_qmd__query` with
32
- the goal in plain words, then the key nouns (product name + topic). Read the
33
- top hits with `mcp__plugin_zeno_qmd__get`. Look for: an ADR that already
32
+ the goal in plain words, then the key nouns, scoped with `collections` to
33
+ this product's `<repo>-wiki` and `<repo>-features` (the names `zeno init`
34
+ gives them; add `pack-<name>` when the question is about a vendor's API). Other products' hits are noise here; step 4 is where
35
+ siblings are searched. Read the top hits with `mcp__plugin_zeno_qmd__get`. Look for: an ADR that already
34
36
  decided this, a canonical value it would change, a concept it would break.
35
37
  If qmd is unavailable, read `.context/wiki/index.md` and `decisions/`.
36
38
  **Architecture:** if `.claude/zeno.yaml` lists the `architecture` module, read
@@ -52,6 +54,10 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
52
54
  test, store review risk.
53
55
  - Value: name the user or the metric it moves (`metrics/definitions.yml` if
54
56
  the product has one). If neither can be named, say "value unproven".
57
+ When a number would settle it, check the product's `SOURCE.*` entity pages
58
+ for where that data lives and query it rather than guess.
59
+ - Evidence: say which evidence from `dev-methods.md` the work must leave — a
60
+ feature page, a failing test first, or a metric and snapshot.
55
61
 
56
62
  6. **Write less.** Climb the ladder and stop at the first rung that works:
57
63
  delete it → reuse something in this repo → reuse from a sibling product or
@@ -59,10 +65,13 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
59
65
  → fewer lines → new code. If the `ponytail` plugin is installed, follow its
60
66
  ladder for this step.
61
67
 
62
- 7. **Large or too-large?** Delegate steps 2–6 to the `challenger` agent for an
63
- independent read — pass it the request, the repo path and the constitution
64
- and standards paths from step 2 — then reconcile: where you and it disagree,
65
- show both.
68
+ 7. **Too-large, or the user asked for a second opinion?** Then, and only then,
69
+ ask the `challenger` agent for an independent read. It costs tens of
70
+ thousands of tokens, so hand it what you already have: the request, the repo
71
+ path, your draft verdict, and every file and wiki page you read with one line
72
+ on what each says. It checks your reasoning and looks only where you did
73
+ not. Where you and it disagree, show both. A large request is weighed by you
74
+ alone.
66
75
 
67
76
  ## Verdict
68
77
 
@@ -3,13 +3,13 @@ name: wiki-lint
3
3
  description: Audit .context/wiki/ for drift — broken [[links]], orphan pages, contradictions between entities, stale last-verified dates, unprocessed raw files. Optionally auto-fix safe issues and regenerate .context/wiki/index.md. Usage `/wiki-lint` (audit) or `/wiki-lint --fix` (audit + auto-fix).
4
4
  ---
5
5
 
6
- You are linting the wiki at `.context/wiki/` against the wiki schema (`../../standards/wiki-schema.md`, relative to this skill).
6
+ You are linting the wiki at `.context/wiki/` against the wiki schema (the `wiki-schema.md` that `zeno standards` lists — a team's own replaces zeno's; without the CLI, `../../standards/wiki-schema.md` relative to this skill).
7
7
 
8
8
  Mode: `$ARGUMENTS` (empty = audit only; `--fix` = audit + auto-fix safe issues).
9
9
 
10
10
  ## Steps
11
11
 
12
- 1. **Read the wiki schema (`../../standards/wiki-schema.md`, relative to this skill)** to refresh schema, ID conventions, required sections, and the lint-rules table.
12
+ 1. **Read the wiki schema** to refresh schema, ID conventions, required sections, and the lint-rules table.
13
13
 
14
14
  2. **Run `zeno check` first** if the CLI is installed. It checks frontmatter,
15
15
  duplicate IDs, staleness and prices held by no entity in under a second,
@@ -29,13 +29,13 @@ Mode: `$ARGUMENTS` (empty = audit only; `--fix` = audit + auto-fix safe issues).
29
29
 
30
30
  | Check | Rule |
31
31
  |---|---|
32
- | Missing required section | Per the wiki schema (`../../standards/wiki-schema.md`, relative to this skill) § "Page body — required sections" |
32
+ | Missing required section | Per the wiki schema § "Page body — required sections" |
33
33
  | Broken `[[wikilink]]` | Target page does not exist |
34
34
  | Duplicate `id` | Two pages claim the same ID |
35
35
  | Contradictory canonical value | Two `entity` pages list the same key with different values |
36
36
  | `status: superseded` with no `superseded-by` | Set or remove the status |
37
37
  | Filename ↔ id slug mismatch | The filename must match the slug portion of the ID |
38
- | `type` does not match folder | `type: concept` must live in `concepts/`, etc. |
38
+ | `type` does not match folder | `type: concept` must live in `concepts/`, etc. A type added under `wiki.types` in `.claude/zeno.yaml` lives in its configured folder and has every heading in its `sections`. |
39
39
 
40
40
  ### Warnings
41
41
 
@@ -9,5 +9,6 @@ replaces it; a configured `constitution.md` adds principles instead.
9
9
  |---|---|
10
10
  | `constitution.md` | The principles a request is weighed against (C-01 to C-13) |
11
11
  | `wiki-schema.md` | Wiki page types, frontmatter, required sections, lint rules |
12
- | `context-layout.md` | The `.context/` ten-folder layout, `features/` and `tasks.md` formats |
12
+ | `context-layout.md` | The `.context/` ten-folder layout, where each kind of document lives, `features/` and `tasks.md` formats |
13
+ | `dev-methods.md` | The evidence spec-, test- and data-driven work leaves behind |
13
14
  | `agent-contract.md` | What every agent does before, during and after work |
@@ -26,6 +26,9 @@ folder.
26
26
  1. **Ten folders, one job each.** There is no `documents/`, `research/`,
27
27
  `architecture/` or `decisions/` folder: a research brief is a wiki synthesis
28
28
  page, an architecture note is a wiki concept, an ADR is `wiki/decisions/`.
29
+ A product that truly needs another folder adds it in `.claude/zeno.yaml`,
30
+ with the reason: `context: { folders: [{ folder: research, reason: … }] }`.
31
+ The ten cannot be removed.
29
32
  2. **`README.md` and `tasks.md` are the only files at `.context/` root.**
30
33
  `README.md` lists every absent folder as `n/a — <why>`.
31
34
  3. **Nothing is created empty.** A folder appears with its first file — git does
@@ -54,6 +57,26 @@ folder.
54
57
  - Frontmatter follows the wiki schema: `id`, `type`, `status`, `last-verified`,
55
58
  `supersedes`, `superseded-by`. Kebab-case keys.
56
59
 
60
+ ## Where each kind of document lives
61
+
62
+ Teams name documents many ways. Each name maps to one home; there is no
63
+ separate folder per name.
64
+
65
+ | Document | Home |
66
+ |---|---|
67
+ | ADR | `wiki/decisions/`, `status: canonical` |
68
+ | RFC | `wiki/decisions/`, `status: draft` — the same page becomes the ADR when accepted |
69
+ | PRD | `features/`, `type: feature` |
70
+ | Experiment | `features/`, `type: experiment`; the result is a decision page |
71
+ | Initiative | `wiki/synthesis/`, linking its features and decisions |
72
+ | Finding | the dated data in `raw/`; what it means in `wiki/synthesis/` or a concept |
73
+ | Know-how | `wiki/concepts/`; `ops/` when it is a runbook |
74
+ | Bug | the issue tracker; a wiki concept only when it teaches a rule |
75
+ | Data source | `wiki/entities/` — see the wiki schema, "Data sources" |
76
+
77
+ If a document fits none of these for a real product, add a wiki type or a
78
+ folder in `.claude/zeno.yaml` with its reason — not a new convention by hand.
79
+
57
80
  ## `features/` — what is next
58
81
 
59
82
  One page per feature, `NNNN-<slug>.md`:
@@ -72,6 +95,9 @@ release: 3.4.0
72
95
  > One or two sentences users will read in the release notes.
73
96
  ```
74
97
 
98
+ An experiment adds `## Hypothesis`, `## Metric` (a name from
99
+ `metrics/definitions.yml`) and `## Result` once it has run.
100
+
75
101
  The wiki describes what *is*; `features/` describes what is *next*. A shipped
76
102
  feature goes `status: shipped` and is never deleted.
77
103
 
@@ -0,0 +1,29 @@
1
+ # Development methods
2
+
3
+ zeno does not run a method; it checks that the method left evidence a later
4
+ reader (or the weigh gate) can follow. A team replaces this file with its own
5
+ `dev-methods.md` in a configured standards folder.
6
+
7
+ | Method | The evidence it leaves |
8
+ |---|---|
9
+ | Spec-driven | A `features/` page with acceptance scenarios, written before the code. Code and tests cite its ID (`FEAT-0031`) or a wiki ID (`FLOW.…`, `DECISION.…`). |
10
+ | Test-driven | A test that failed before the change and passes after. Bug fixes start with the failing test. The test path goes in the feature page or decision. |
11
+ | Data-driven | A named metric in `metrics/definitions.yml`, the query that measured it, and a dated snapshot in `raw/`. The decision cites the snapshot, not a memory of a dashboard. |
12
+
13
+ ## Which one, when
14
+
15
+ - **New behaviour users see** → spec-driven: the feature page comes first.
16
+ - **Logic that can break** (money, parsing, data writes, a bug fix) → test-driven.
17
+ - **A bet on what users want** → data-driven: an experiment page with its metric
18
+ before the build, the result as a decision after.
19
+
20
+ They combine. A paywall change is a spec (what users see), tests (the price
21
+ logic) and an experiment (does conversion move).
22
+
23
+ ## Rules
24
+
25
+ 1. No evidence, not done. "It works" without a test, a scenario or a number is
26
+ an opinion.
27
+ 2. Evidence lives in the repo (C-03). A dashboard link alone is not evidence —
28
+ it changes; the snapshot in `raw/` does not.
29
+ 3. When a method keeps being skipped, make it a check (C-09), not a longer doc.
@@ -33,6 +33,44 @@ Every page in `.context/wiki/` belongs to exactly one type. The folder name is t
33
33
 
34
34
  A page cannot be two types. If it feels like it is, it's two pages — split it.
35
35
 
36
+ A product adds a type in `.claude/zeno.yaml`, with the reason and the `## `
37
+ sections every page of it must have:
38
+
39
+ ```yaml
40
+ wiki:
41
+ types:
42
+ - { type: postmortem, folder: postmortems, reason: …, sections: [Summary, Timeline, Root cause, Fix, Sources] }
43
+ ```
44
+
45
+ `zeno check` fails a page of that type that misses a section. Its ID is
46
+ `<TYPE>.<SLUG>` (`POSTMORTEM.MIDNIGHT-RESET`). The six built-in types and their
47
+ folders cannot be redefined. Try the six first — an added type is a shape every
48
+ reader and every product must learn.
49
+
50
+ ### Data sources
51
+
52
+ Each tool that holds product data (Datadog, Sentry, Snowflake, an analytics
53
+ console) is one `entity` page, `id: SOURCE.<NAME>`, in `entities/`:
54
+
55
+ ```markdown
56
+ ## Summary
57
+ What this source holds, in one sentence.
58
+
59
+ ## Canonical values
60
+ | Key | Value | Last changed |
61
+ |---|---|---|
62
+ | SOURCE.SENTRY.PROJECT | streaks-mobile | 2026-10-23 |
63
+ | SOURCE.SENTRY.ACCESS | Sentry MCP server | 2026-10-23 |
64
+
65
+ ## How to query
66
+ The MCP server or CLI to use, and one example query. Results that back a
67
+ decision are saved to `raw/YYYY-MM-DD-<source>-<slug>.md` so they can be re-read.
68
+ ```
69
+
70
+ zeno has no connector of its own: the vendor's MCP server or CLI does the
71
+ reading. Add it to `doctor.tools` in `.claude/zeno.yaml` so `zeno doctor`
72
+ reports when it is missing. Never put a key or token on the page.
73
+
36
74
  ---
37
75
 
38
76
  ## Frontmatter (mandatory)