@freetesh/zeno 0.10.0 → 0.11.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "zeno",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
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",
package/README.md CHANGED
@@ -54,8 +54,17 @@ It has two halves that read the same config:
54
54
  `.context` layout, agent contract). A team adds or replaces them from its own
55
55
  folder; a repo can override again. `zeno standards` shows what applies.
56
56
  - **Hard rules are checks.** `zeno check` validates layout, wiki frontmatter,
57
- stale pages, prices with no canonical home, copied standards, and runs every
58
- checker your config declares.
57
+ stale pages, placeholder text, page and `tasks.md` size caps, prices with no
58
+ canonical home, copied standards, and runs every checker your config declares.
59
+ - **Context stays small.** A line belongs in `.context/` only if an agent would
60
+ get something wrong without it. Decisions are one file per topic, overwritten
61
+ when the choice changes; `tasks.md` holds open work only; history is git's.
62
+ - **Stages stop at their gates.** A `pipeline:` in `zeno.yaml` lists your
63
+ stages in order, with the paths that show each has started. A gated stage
64
+ needs a person to run `zeno approve <stage>` before any later stage starts;
65
+ `zeno check` fails otherwise. Approvals are dated in
66
+ `.context/raw/zeno/gates.jsonl`, so you can see where work waits. A product
67
+ replaces the estate's pipeline by declaring its own.
59
68
  - **Each repo's architecture is enforced.** With the `architecture` module, a
60
69
  repo keeps its own import rules in `.dependency-cruiser.cjs` files: one at the
61
70
  root, or one per workspace under `apps/` and `packages/` (each runs from its
@@ -141,7 +150,7 @@ doctor:
141
150
  tools:
142
151
  - { tool: pnpm, why: product scripts, hint: npm i -g pnpm }
143
152
  wiki:
144
- types: # added to the six built-in page types
153
+ types: # added to the built-in folders (concepts, entities, decisions, surfaces, synthesis)
145
154
  - type: postmortem
146
155
  folder: postmortems
147
156
  reason: incidents need a timeline and a fix
@@ -153,7 +162,7 @@ context:
153
162
 
154
163
  ```yaml
155
164
  # <repo>/.claude/zeno.yaml — written by `zeno init`
156
- zeno: 0.9.0
165
+ zeno: 0.11.0
157
166
  kind: expo-app # expo-app | web | content
158
167
  modules: [docs, knowledge, code, listing] # all optional; design is added when .context/designs exists, metrics only when asked
159
168
  packs: [expo]
@@ -173,8 +182,8 @@ carry a `reason:`, and `zeno check` lists every override.
173
182
  | zeno default | What it holds |
174
183
  |---|---|
175
184
  | `constitution.md` | Principles C-01–C-15 a request is weighed against. A team adds C-20+ in its own `constitution.md`. |
176
- | `wiki-schema.md` | Wiki page types, frontmatter, required sections, lint rules |
177
- | `context-layout.md` | The `.context/` ten-folder layout, `features/` and `tasks.md` |
185
+ | `wiki-schema.md` | What belongs in the wiki, its folders, decisions, frontmatter and size caps |
186
+ | `context-layout.md` | The `.context/` ten-folder layout, `features/` and `tasks.md` (open work only) |
178
187
  | `agent-contract.md` | What every agent does before, during and after work |
179
188
 
180
189
  ## Packs
@@ -10,25 +10,27 @@ const PRICE_WORDS = /pric|cost|paywall|plan|tier|subscri|bundle|pack|purchase|bu
10
10
  function amounts(text) {
11
11
  return (text.match(PRICE) ?? []).map((price) => price.replace(/\s/g, ""));
12
12
  }
13
+ // A decision's `Rejected:` line names old prices on purpose; they are history, not a second home.
14
+ const REJECTED = /^\*\*Rejected:\*\*/;
13
15
  function pricesIn(text) {
14
- const paragraphs = text.replace(CODE_BLOCK, "").split(/\n\s*\n/);
16
+ const paragraphs = text.replace(CODE_BLOCK, "").split(/\n\s*\n/).filter((paragraph) => !REJECTED.test(paragraph.trim()));
15
17
  return [...new Set(paragraphs.filter((paragraph) => PRICE_WORDS.test(paragraph)).flatMap(amounts))];
16
18
  }
17
19
  function heldAmounts(text) {
18
20
  return amounts(text.replace(CODE_BLOCK, ""));
19
21
  }
22
+ const folderOf = (file) => file.split("/")[2];
20
23
  export const canonicalValues = {
21
24
  id: "canonical-values",
22
25
  module: "docs",
23
- description: "a price shown on a surface page is held by an entity page — one home per fact (C-04)",
26
+ description: "a price stated in a decision or screen note is held by an entity page — one home per fact (C-02)",
24
27
  run({ repo }) {
25
28
  const pages = wikiPages(repo).map((file) => ({ file, ...parseFrontmatter(readFileSync(join(repo, file), "utf8")) }));
26
- const live = pages.filter((page) => page.data?.status !== "superseded");
27
- const held = new Set(live.filter((page) => page.data?.type === "entity").flatMap((page) => heldAmounts(page.body)));
28
- // Surfaces are where store prices get restated. Concepts legitimately carry
29
+ const held = new Set(pages.filter((page) => folderOf(page.file) === "entities").flatMap((page) => heldAmounts(page.body)));
30
+ // Decisions and screen notes are where prices get restated. Concepts legitimately carry
29
31
  // example amounts (a tax product explains slabs with them).
30
- return live
31
- .filter((page) => page.data?.type === "surface")
32
+ return pages
33
+ .filter((page) => ["decisions", "surfaces"].includes(folderOf(page.file)))
32
34
  .flatMap((page) => {
33
35
  const orphans = pricesIn(page.body).filter((price) => !held.has(price));
34
36
  if (orphans.length === 0)
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { REMOVED_WIKI_FOLDERS as REMOVED_FOLDERS } from "../config/schema.js";
4
+ import { wikiPages } from "./wiki-frontmatter.js";
5
+ // Every line here is read by an agent on every task; a cap keeps .context to what it would get wrong without.
6
+ const MAX_LINES = { page: 60, decision: 30, readme: 30, tasks: 100 };
7
+ const DATED_NAME = /^\d{4}-\d{2}-\d{2}-/;
8
+ const lineCount = (repo, file) => readFileSync(join(repo, file), "utf8").trimEnd().split("\n").length;
9
+ export const contextSize = {
10
+ id: "context-size",
11
+ module: "docs",
12
+ description: "pages, decisions, README.md and tasks.md stay under their line caps; decisions are one file per topic; no flow pages",
13
+ run({ repo }) {
14
+ const findings = [];
15
+ const warn = (file, message) => findings.push({ check: "context-size", level: "warn", file, message });
16
+ const overCap = (file, cap) => {
17
+ const lines = lineCount(repo, file);
18
+ if (lines > cap)
19
+ warn(file, `${lines} lines, cap ${cap} — cut what code, designs or git already say`);
20
+ };
21
+ for (const [folder, why] of Object.entries(REMOVED_FOLDERS)) {
22
+ if (existsSync(join(repo, ".context/wiki", folder)))
23
+ warn(`.context/wiki/${folder}`, `${folder}/ is gone: ${why}`);
24
+ }
25
+ for (const file of wikiPages(repo)) {
26
+ const folder = file.split("/")[2];
27
+ if (folder === "synthesis" || folder in REMOVED_FOLDERS)
28
+ continue;
29
+ if (folder === "decisions" && DATED_NAME.test(basename(file))) {
30
+ warn(file, "name a decision by topic and edit it in place — git keeps the history");
31
+ }
32
+ overCap(file, folder === "decisions" ? MAX_LINES.decision : MAX_LINES.page);
33
+ }
34
+ const readme = ".context/README.md";
35
+ const tasks = ".context/tasks.md";
36
+ if (existsSync(join(repo, readme)))
37
+ overCap(readme, MAX_LINES.readme);
38
+ if (existsSync(join(repo, tasks))) {
39
+ overCap(tasks, MAX_LINES.tasks);
40
+ if (/^## (Done|Session Notes)\b/m.test(readFileSync(join(repo, tasks), "utf8"))) {
41
+ warn(tasks, "tasks.md holds only open work — delete done tasks and session notes; the commits record them");
42
+ }
43
+ }
44
+ return findings;
45
+ },
46
+ };
@@ -1,19 +1,23 @@
1
1
  import { architecture } from "./architecture.js";
2
2
  import { canonicalValues } from "./canonical-values.js";
3
3
  import { contextLayout } from "./context-layout.js";
4
+ import { contextSize } from "./context-size.js";
4
5
  import { staleCopies, standardsCopies } from "./copies.js";
5
6
  import { commandChecks } from "./external.js";
6
7
  import { metricsDefinitions } from "./metrics.js";
8
+ import { pipeline } from "./pipeline.js";
7
9
  import { sources } from "./sources.js";
8
10
  import { wikiFrontmatter } from "./wiki-frontmatter.js";
9
11
  export const ALL_CHECKS = [
10
12
  contextLayout,
11
13
  wikiFrontmatter,
14
+ contextSize,
12
15
  canonicalValues,
13
16
  standardsCopies,
14
17
  staleCopies,
15
18
  architecture,
16
19
  metricsDefinitions,
17
20
  sources,
21
+ pipeline,
18
22
  commandChecks,
19
23
  ];
@@ -0,0 +1,34 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readApprovals } from "../pipeline.js";
4
+ /**
5
+ * Stages run in the order zeno.yaml lists them. A stage has started once one
6
+ * of its `done` paths exists; a gated stage must be approved before any later
7
+ * stage starts. Each unapproved gate is reported once, at the first later
8
+ * stage that started.
9
+ */
10
+ export const pipeline = {
11
+ id: "pipeline",
12
+ module: null,
13
+ description: "no stage starts before every earlier gate is approved",
14
+ run({ repo, config }) {
15
+ const approved = new Set(readApprovals(repo).map((approval) => approval.stage));
16
+ const startedAt = config.pipeline.map((stage) => stage.done.find((path) => existsSync(join(repo, path))));
17
+ return config.pipeline.flatMap((gate, index) => {
18
+ if (!gate.gate || approved.has(gate.id))
19
+ return [];
20
+ const later = config.pipeline.findIndex((_, i) => i > index && startedAt[i] !== undefined);
21
+ if (later === -1)
22
+ return [];
23
+ const stage = config.pipeline[later];
24
+ return [
25
+ {
26
+ check: "pipeline",
27
+ level: "error",
28
+ file: startedAt[later],
29
+ message: `stage "${stage.id}" started before gate "${gate.id}" was approved — review ${gate.id}, then a person runs \`zeno approve ${gate.id}\``,
30
+ },
31
+ ];
32
+ });
33
+ },
34
+ };
@@ -2,8 +2,11 @@ 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";
6
- const REQUIRED_FIELDS = ["id", "type", "status", "last-verified"];
5
+ import { REMOVED_WIKI_FOLDERS, WIKI_FOLDER_BY_TYPE } from "../config/schema.js";
6
+ const REQUIRED_FIELDS = ["last-verified"];
7
+ // Filler an agent would read as fact: a section with nothing to say is deleted, not stubbed.
8
+ const PLACEHOLDER = /\bTBD\b|\bnone yet\b|\bpopulate when\b/i;
9
+ const CODE_BLOCK = /```[\s\S]*?```/g;
7
10
  const STALE_AFTER_DAYS = 90;
8
11
  const DAY_MS = 86_400_000;
9
12
  const NOT_PAGES = ["index.md", "CLAUDE.md"];
@@ -18,7 +21,7 @@ function asDate(value) {
18
21
  export const wikiFrontmatter = {
19
22
  id: "wiki-frontmatter",
20
23
  module: "docs",
21
- description: "every wiki page has id, type, status and last-verified; type matches folder; ids are unique; nothing stale",
24
+ description: "every wiki page has last-verified and no placeholder text; a type, if set, matches its folder; ids are unique; nothing stale",
22
25
  run({ repo, config, today }) {
23
26
  const folderByType = { ...WIKI_FOLDER_BY_TYPE, ...Object.fromEntries(config.wiki.types.map((entry) => [entry.type, entry.folder])) };
24
27
  // Built-in types' sections are linted by the wiki-lint skill; added types carry theirs in config.
@@ -28,6 +31,9 @@ export const wikiFrontmatter = {
28
31
  const filesById = new Map();
29
32
  const stale = [];
30
33
  for (const file of wikiPages(repo)) {
34
+ const folder = file.split("/")[2];
35
+ if (folder in REMOVED_WIKI_FOLDERS)
36
+ continue; // context-size reports the folder once
31
37
  const { data, body } = parseFrontmatter(readFileSync(join(repo, file), "utf8"));
32
38
  if (!data) {
33
39
  add("error", file, "no frontmatter");
@@ -36,10 +42,11 @@ export const wikiFrontmatter = {
36
42
  const missing = REQUIRED_FIELDS.filter((field) => data[field] === undefined || data[field] === null);
37
43
  if (missing.length > 0)
38
44
  add("error", file, `missing ${missing.join(", ")}`);
39
- const folder = file.split("/")[2];
40
45
  const expected = folderByType[String(data.type)];
41
46
  if (data.type && expected !== folder)
42
47
  add("error", file, `type ${data.type} does not belong in ${folder}/`);
48
+ if (!data.type && !Object.values(folderByType).includes(folder))
49
+ add("error", file, `${folder}/ is not a wiki folder`);
43
50
  const sections = sectionsByType.get(String(data.type));
44
51
  if (sections) {
45
52
  const headings = new Set(body.split("\n").flatMap((line) => (line.startsWith("## ") ? [line.slice(3).trim()] : [])));
@@ -47,8 +54,9 @@ export const wikiFrontmatter = {
47
54
  if (absent.length > 0)
48
55
  add("error", file, `missing sections: ${absent.join(", ")}`);
49
56
  }
50
- if (data.status === "superseded" && !data["superseded-by"])
51
- add("error", file, "superseded without superseded-by");
57
+ const placeholder = body.replace(CODE_BLOCK, "").match(PLACEHOLDER);
58
+ if (placeholder)
59
+ add("warn", file, `placeholder "${placeholder[0]}" — write the fact or delete the section`);
52
60
  const verified = asDate(data["last-verified"]);
53
61
  const age = verified ? Math.floor((today.getTime() - verified.getTime()) / DAY_MS) : 0;
54
62
  if (verified && age > STALE_AFTER_DAYS)
@@ -0,0 +1,20 @@
1
+ import { resolve } from "node:path";
2
+ import { loadConfig } from "../config/load.js";
3
+ import { recordApproval } from "../pipeline.js";
4
+ export function registerApprove(program) {
5
+ program
6
+ .command("approve")
7
+ .description("record that a person reviewed and approved a pipeline gate, dated now (a person runs this, not an agent)")
8
+ .argument("<stage>", "the gated stage's id in zeno.yaml `pipeline:`")
9
+ .argument("[repo]", "product repo", ".")
10
+ .option("--note <text>", "what was reviewed", "")
11
+ .action((stageId, repo, options) => {
12
+ const root = resolve(repo);
13
+ const gates = loadConfig(root).config.pipeline.filter((stage) => stage.gate).map((stage) => stage.id);
14
+ if (!gates.includes(stageId)) {
15
+ throw new Error(`"${stageId}" is not a gated stage here — gates: ${gates.join(", ") || "none (no pipeline: in zeno.yaml)"}`);
16
+ }
17
+ const approval = recordApproval(root, stageId, options.note);
18
+ console.log(`approved ${stageId} at ${approval.at}`);
19
+ });
20
+ }
@@ -89,6 +89,7 @@ export function loadConfig(repo, options = {}) {
89
89
  weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
90
90
  wiki: { types: mergeBy(pick("wiki").map((wiki) => wiki?.types ?? []), (entry) => entry.type) },
91
91
  context: { folders: mergeBy(pick("context").map((context) => context?.folders ?? []), (entry) => entry.folder) },
92
+ pipeline: lastDefined(pick("pipeline")) ?? [],
92
93
  };
93
94
  if (Object.keys(config.expo ?? {}).length === 0)
94
95
  delete config.expo;
@@ -1,15 +1,21 @@
1
1
  import { z } from "zod";
2
2
  export const KINDS = ["expo-app", "web", "content"];
3
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`. */
4
+ /**
5
+ * The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
6
+ * No flow type: a journey worth keeping is a concept. A surface page is a screen's notes, only where it has real ones.
7
+ */
5
8
  export const WIKI_FOLDER_BY_TYPE = {
6
9
  concept: "concepts",
7
10
  entity: "entities",
8
11
  surface: "surfaces",
9
- flow: "flows",
10
12
  decision: "decisions",
11
13
  synthesis: "synthesis",
12
14
  };
15
+ /** Wiki folders that no longer exist, and what to do with what was in them. */
16
+ export const REMOVED_WIKI_FOLDERS = {
17
+ flows: "fold a journey worth keeping into a concept page",
18
+ };
13
19
  /** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
14
20
  export const CONTEXT_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
15
21
  const checkOverride = z
@@ -43,6 +49,15 @@ const wikiType = z
43
49
  message: "a built-in wiki type or its folder cannot be redefined — add a new one",
44
50
  });
45
51
  const contextFolder = z.object({ folder: kebab, reason }).strict();
52
+ const stage = z
53
+ .object({
54
+ id: kebab,
55
+ /** Paths that exist once the stage has started; any one is enough. */
56
+ done: z.array(z.string().min(1)).min(1),
57
+ /** A person must approve this stage (`zeno approve <id>`) before any later stage starts. */
58
+ gate: z.boolean().default(false),
59
+ })
60
+ .strict();
46
61
  const tool = z.object({ tool: z.string(), why: z.string(), hint: z.string() }).strict();
47
62
  /** One layer of `.claude/zeno.yaml` as written: every field optional. */
48
63
  export const layerSchema = z
@@ -62,6 +77,8 @@ export const layerSchema = z
62
77
  weigh: z.object({ weights: z.record(principleId, z.number().nonnegative()) }).partial(),
63
78
  wiki: z.object({ types: z.array(wikiType) }).partial(),
64
79
  context: z.object({ folders: z.array(contextFolder) }).partial(),
80
+ /** The stages work goes through, in order. A more specific layer replaces the whole list. */
81
+ pipeline: z.array(stage),
65
82
  })
66
83
  .partial()
67
84
  .strict();
package/cli/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { resolve } from "node:path";
3
3
  import { Command } from "commander";
4
4
  import { registerAdd } from "./commands/add.js";
5
+ import { registerApprove } from "./commands/approve.js";
5
6
  import { registerCheck } from "./commands/check.js";
6
7
  import { registerDoctor } from "./commands/doctor.js";
7
8
  import { registerInit } from "./commands/init.js";
@@ -29,6 +30,7 @@ registerStandards(program, env);
29
30
  registerDoctor(program);
30
31
  registerUi(program, env);
31
32
  registerLearn(program, env);
33
+ registerApprove(program);
32
34
  registerWeigh(program);
33
35
  program.parseAsync().catch((error) => {
34
36
  console.error(`zeno: ${error.message}`);
package/cli/dist/init.js CHANGED
@@ -66,12 +66,12 @@ const FIRST_FILE = {
66
66
  wiki: ".context/wiki/index.md",
67
67
  features: ".context/features/index.md",
68
68
  };
69
- function template(file, today) {
69
+ function template(file) {
70
70
  switch (file) {
71
71
  case ".context/README.md":
72
72
  return "# .context\n\nProduct material, in the ten folders of the zeno context layout.\n\n## Absent folders\n\n- listing/ — n/a — no store presence yet\n- marketing/, metrics/, support/, legal/ — n/a — not live yet\n- ops/ — n/a — nothing to run yet\n";
73
73
  case ".context/tasks.md":
74
- return `---\nupdated: ${today}\nhorizon: ${today}\n---\n\n# Tasks\n\n## Now\n\n## Parallel\n\n## Blocked\n\n## Waiting\n\n## Done\n\n## Session Notes\n`;
74
+ return "# Tasks\n\n## Now\n\n## Blocked\n\n## Waiting\n";
75
75
  case ".context/wiki/index.md":
76
76
  return "# Wiki index\n\nPages by type. Schema: the zeno plugin's wiki-schema standard.\n";
77
77
  case ".context/features/index.md":
@@ -98,7 +98,6 @@ export function planInit(repo, answers) {
98
98
  return { config, create };
99
99
  }
100
100
  export function applyInit(repo, plan, options) {
101
- const today = options.today ?? new Date().toISOString().slice(0, 10);
102
101
  const write = (file, content) => {
103
102
  mkdirSync(dirname(join(repo, file)), { recursive: true });
104
103
  writeFileSync(join(repo, file), content);
@@ -107,7 +106,7 @@ export function applyInit(repo, plan, options) {
107
106
  const starters = new Map(architectureStarters(repo).map((s) => [s.file, s.template]));
108
107
  for (const file of plan.create) {
109
108
  const starter = starters.get(file);
110
- write(file, starter ? readFileSync(join(options.zenoRoot, ARCHITECTURE_TEMPLATES, starter), "utf8") : template(file, today));
109
+ write(file, starter ? readFileSync(join(options.zenoRoot, ARCHITECTURE_TEMPLATES, starter), "utf8") : template(file));
111
110
  }
112
111
  if (plan.config.modules.includes("knowledge"))
113
112
  indexKnowledge(repo, options.name, options.run);
@@ -0,0 +1,20 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ // In raw/, which is append-only: the dates are the record of how long each stage waited.
4
+ export const GATES_LOG = join(".context", "raw", "zeno", "gates.jsonl");
5
+ export function readApprovals(repo) {
6
+ const path = join(repo, GATES_LOG);
7
+ if (!existsSync(path))
8
+ return [];
9
+ return readFileSync(path, "utf8")
10
+ .split("\n")
11
+ .filter((line) => line.trim())
12
+ .map((line) => JSON.parse(line));
13
+ }
14
+ export function recordApproval(repo, stage, note) {
15
+ const approval = { stage, at: new Date().toISOString(), note };
16
+ const path = join(repo, GATES_LOG);
17
+ mkdirSync(dirname(path), { recursive: true });
18
+ appendFileSync(path, `${JSON.stringify(approval)}\n`);
19
+ return approval;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freetesh/zeno",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
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",
@@ -15,9 +15,8 @@ Question: `$ARGUMENTS`
15
15
  - `entity` pages for "what is the value / price / name of X"
16
16
  - `synthesis` pages for cross-cutting summaries (use these before chasing individual entities)
17
17
  - `concept` pages for "why" or "what does X mean" questions
18
- - `surface` pages for "what does this screen do"
19
- - `flow` pages for "how does the user get from A to B"
20
- - `decision` pages for "why did we choose X over Y"
18
+ - `decision` pages for "why did we choose X over Y", and its `Rejected:` line for "did we consider Y"
19
+ - for "what does this screen do", the HTML in `.context/designs/` is the spec; its `surfaces/` note, if any, says why
21
20
 
22
21
  3. **Read the candidate pages.** Follow `[[wikilinks]]` one hop if the answer is incomplete. Stop at two hops — if you need three, the wiki is missing a synthesis page; say so.
23
22
 
@@ -35,8 +34,7 @@ Question: `$ARGUMENTS`
35
34
 
36
35
  ## Rules
37
36
 
38
- - **Never read `.context/raw/` or `.context/designs/` from this skill.** If the answer requires raw inspection, refuse and recommend `/ingest`.
37
+ - **Never read `.context/raw/` from this skill.** If the answer requires raw inspection, recommend `/ingest`. Designs may be read for screen questions.
39
38
  - **Never invent facts.** If a canonical value isn't in an `entity` page, say "not in the wiki".
40
39
  - **Every cited page must actually exist.** Don't fabricate a `[[wikilink]]` to make the answer look authoritative.
41
- - A page with `status: superseded` is not canonical — follow `superseded-by` to the current page.
42
40
  - A page with `last-verified` > 90 days is stale — flag it in the answer ("last verified 2025-11-04 — may be out of date").
@@ -1,33 +1,36 @@
1
1
  ---
2
2
  name: decision
3
- description: Record a product or technical decision as a new ADR page in .context/wiki/decisions/. Usage `/decision <title> — <body>`.
3
+ description: Record or change a product or technical decision in .context/wiki/decisions/<topic>.md — one file per topic, overwritten when the choice changes. Usage `/decision <topic> — <choice and why>`.
4
4
  ---
5
5
 
6
- You are logging a decision as a new `.context/wiki/decisions/<YYYY-MM-DD>-<slug>.md` page.
6
+ You are recording a decision in `.context/wiki/decisions/<topic>.md`.
7
7
 
8
- Decision to record: `$ARGUMENTS`
8
+ Decision: `$ARGUMENTS`
9
9
 
10
10
  ## Steps
11
11
 
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
-
14
- 2. Parse `$ARGUMENTS`:
15
- - If it contains ` — ` (em-dash with spaces), split on the first occurrence: left = title, right = body.
16
- - Otherwise treat the whole string as the title; body = "(no detail provided)".
17
-
18
- 3. Derive the filename:
19
- - Date = today's `currentDate` from the session context (`YYYY-MM-DD`).
20
- - Slug = kebab-case of the title, lower-cased, alphanumerics + hyphens only, ≤ 60 chars.
21
- - Path = `.context/wiki/decisions/<date>-<slug>.md`. If a file at that path already exists, append `-2`, `-3`, etc.
22
-
23
- 4. Write the new ADR with the standard frontmatter and the body the user provided. Do not summarise or shorten the body.
24
-
25
- 5. Add the page to `.context/wiki/index.md` under Decisions.
26
-
27
- 6. Confirm: `decision logged: "<title>" → <path>`.
12
+ 1. Split `$ARGUMENTS` on the first ` — `: left = topic, right = the choice and why. With no ` — `, ask for the choice.
13
+ 2. Slug = the topic in kebab-case, no date (`pricing`, `auth-provider`). Look for an existing file on the same topic first, under any name. If there is one, it is the file to change.
14
+ 3. Write the file, at most 30 lines:
15
+
16
+ ```markdown
17
+ ---
18
+ last-verified: <today>
19
+ ---
20
+ # <Topic>
21
+ **Choice:** <one or two lines; link values to their entity page>
22
+ **Why:** <the trigger: data, incident, user feedback, cost>
23
+ **Rejected:** <option (why)> · <option (why)>
24
+ ```
25
+
26
+ When changing an existing decision, replace the choice and why, and move the
27
+ old choice into `Rejected:` with the reason it was dropped. Keep the rest of `Rejected:`.
28
+ 4. Put a price, SKU or limit on its entity page, not here (`canonical-values` checks this).
29
+ 5. Add or update the one line in `.context/wiki/index.md`.
30
+ 6. Confirm: `decision recorded: <topic> → <path>`.
28
31
 
29
32
  ## Rules
30
33
 
31
- - One decision per file. Never append to an existing ADR.
32
- - Do not edit any other wiki page in the same command — if cross-references are needed, surface them in the body as `[[wikilinks]]`, but leave the target pages alone.
33
- - The wiki is the only source of truth for decisions. There is no monolithic decision log.
34
+ - One file per topic, overwritten. Git keeps the old text; never add a dated or `-2` file.
35
+ - No fix lists, audit results or step-by-step history. Those go in the commit message or `CHANGELOG.md`.
36
+ - If the user's text is long, keep the choice, the reason and what was rejected, and drop the rest. Show what you dropped.
@@ -9,28 +9,27 @@ Source to ingest: `$ARGUMENTS`
9
9
 
10
10
  ## Steps
11
11
 
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.
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 the folders, the one test for what belongs, and the caps. 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
 
16
16
  3. **Classify** the source into one or more wiki page types:
17
- - HTML wireframe → `surface` page
18
- - Decision note → `decision` page
17
+ - HTML wireframe → nothing; the HTML is the spec. Only a constraint it can't show becomes that screen's `surfaces/<slug>.md` note.
18
+ - Decision note → the `decisions/<topic>.md` file, via the `decision` skill's format
19
19
  - Research doc / interview → typically `concept` pages (split if it covers multiple ideas), or one `synthesis` page for a brief that draws a conclusion
20
20
  - A new tier / product / canonical value → `entity` page
21
- - A user journey description → `flow` page
21
+ - A user journey description → a concept, only if it holds a rule the designs don't show
22
+ - Write nothing an agent could read from the code, the designs or git.
22
23
  - 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.
24
+ - Folders are the four 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 its `sections` headings). Use an added type only when the source fits it.
24
25
 
25
26
  4. **For each page you will create:**
26
- - Pick the ID per the wiki schema § "ID conventions". IDs are uppercase, dot-separated, never change.
27
- - Pick the filename: the slug portion of the ID, lowercase, kebab-case, `.md` extension.
28
- - Check whether the page already exists. If yes, **update** rather than overwrite: preserve `id`, `supersedes`, `superseded-by`; refresh `last-verified` to today.
29
- - Use the required sections for that page type — verbatim from the wiki schema. Missing a required section is a lint failure.
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.
31
- - List the source path under `Sources`, so a future `/wiki-lint` can tell which raw files nothing has read.
27
+ - Filename: the topic, lowercase kebab-case, no date.
28
+ - Check whether a page on the topic already exists. If yes, edit it in place and refresh `last-verified` to today. Keep any `id`.
29
+ - Write only sections with something to say. No placeholders. Stay under the caps (page 60 lines).
30
+ - Link related pages with `[[slug]]` where it helps a reader. List the source path under `Sources`.
32
31
 
33
- 5. **Update `.context/wiki/index.md`:** add the new page under the right type heading, in alphabetical order by slug. Don't reformat existing entries.
32
+ 5. **Update `.context/wiki/index.md`:** one line for the new page, under its folder heading, alphabetical. Don't reformat existing entries.
34
33
 
35
34
  6. **Leave the source where it is.** `.context/raw/` is immutable: a source is "processed" when a wiki page lists it under `Sources`, not when it moves.
36
35
 
@@ -39,7 +38,7 @@ Source to ingest: `$ARGUMENTS`
39
38
  ## Rules
40
39
 
41
40
  - Never edit anything under `.context/raw/` or `.context/designs/`.
42
- - Never delete or rename an existing wiki page from this skill — use a `decision` page to log a rename, then run again.
41
+ - Never delete or rename an existing wiki page from this skill.
43
42
  - Never invent canonical values. If the source has a price/string/enum, copy it verbatim; if it's ambiguous, ask the user before writing.
44
43
  - Follow the copy rules from the root `CLAUDE.md` for any user-facing strings you transcribe.
45
44
  - Today's date is in the session context — use it for `last-verified`.
@@ -9,10 +9,11 @@ Mode: `$ARGUMENTS` (empty = audit only; `--fix` = audit + auto-fix safe issues).
9
9
 
10
10
  ## Steps
11
11
 
12
- 1. **Read the wiki schema** to refresh schema, ID conventions, required sections, and the lint-rules table.
12
+ 1. **Read the wiki schema** to refresh the folders, the one test and the caps.
13
13
 
14
14
  2. **Run `zeno check` first** if the CLI is installed. It checks frontmatter,
15
- duplicate IDs, staleness and prices held by no entity in under a second,
15
+ placeholders, caps, dated decision names, duplicate IDs, staleness and
16
+ prices held by no entity in under a second,
16
17
  plus any checker the repo declares. Start from its output rather than
17
18
  re-deriving it.
18
19
 
@@ -29,30 +30,27 @@ Mode: `$ARGUMENTS` (empty = audit only; `--fix` = audit + auto-fix safe issues).
29
30
 
30
31
  | Check | Rule |
31
32
  |---|---|
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
- | `status: superseded` with no `superseded-by` | Set or remove the status |
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. A type added under `wiki.types` in `.claude/zeno.yaml` lives in its configured folder and has every heading in its `sections`. |
36
+ | Placeholder text | `TBD`, `none yet`: write the fact or delete the section |
37
+ | `type` does not match folder | If a page sets `type`, it lives in that type's folder |
39
38
 
40
39
  ### Warnings
41
40
 
42
41
  | Check | Rule |
43
42
  |---|---|
44
- | Orphan page | Zero inbound `[[wikilinks]]`. Exception: `decision` and `synthesis` pages are commonly orphan-source. |
43
+ | Over a cap | Page 60 lines, decision 30, README 30, tasks 100 |
44
+ | Restates code, a design or git | Delete the lines; link the source instead |
45
+ | Two pages on one topic | Merge into one, edited in place |
45
46
  | Stale `last-verified` | > 90 days from today. |
46
47
  | Unprocessed raw file | A file under `.context/raw/` that no wiki page references in its `Sources`. |
47
- | Stale synthesis | A `synthesis` page where any `Built from` page was modified after the synthesis's `Last refresh`. |
48
- | Page missing two outbound links | Body has fewer than two `[[wikilinks]]`. |
49
48
  | User-facing string violates copy rules | Check quoted copy against the rules in the root `CLAUDE.md`. Best-effort. |
50
49
 
51
50
  6. **Auto-fix (only if `--fix` is in `$ARGUMENTS`):**
52
51
  - Regenerate `.context/wiki/index.md` from scratch: enumerate pages by type, sort alphabetically by slug, keep the structure the file already has.
53
52
  - Fix `last-verified` to today's date on any page whose body was edited in the working tree.
54
- - Fix filename ↔ id slug mismatches (rename the file; do NOT change the ID).
55
- - **Never** auto-fix: contradictory canonical values, missing required sections, broken `[[wikilinks]]`. These need a human decision.
53
+ - **Never** auto-fix: contradictory canonical values, broken `[[wikilinks]]`, merges or cuts. These need a human decision.
56
54
 
57
55
  7. **Report.** Print:
58
56
  - Counts: pages, blockers, warnings, auto-fixes applied.
@@ -63,7 +61,7 @@ Mode: `$ARGUMENTS` (empty = audit only; `--fix` = audit + auto-fix safe issues).
63
61
  ## Rules
64
62
 
65
63
  - **Never edit pages other than auto-fixes above.** Lint reports; the user (or `/ingest`) fixes.
66
- - **Never delete pages.** Superseded pages stay on disk with `status: superseded` and `superseded-by: <new-id>`.
64
+ - **Never delete pages from this skill.** Propose the cut; the user approves it. Git keeps what is deleted.
67
65
  - **Never modify `.context/raw/`** except to warn about unprocessed files.
68
66
  - If a blocker resists auto-fix, exit non-zero (in CI mode) but always print the full report first.
69
67
  - Today's date is in the session context — use it for staleness math (>90 days).
@@ -8,7 +8,7 @@ file with its own `agent-contract.md` in a configured standards folder.
8
8
  - Read the repo's `CLAUDE.md` / `AGENTS.md`: stack, layout, the rules that are
9
9
  the repo's own.
10
10
  - Read `.context/tasks.md`. It is the ordered queue — the top item of `## Now`
11
- is the next action. After finishing, tick the task and append a Session Note.
11
+ is the next action. After finishing, delete the task.
12
12
  - For a non-trivial request, run the weigh gate first (`/zeno:weigh`).
13
13
 
14
14
  ## Repository rules
@@ -40,7 +40,8 @@ function's own name.
40
40
 
41
41
  ## Before calling work done
42
42
 
43
- 1. Reflect changed behaviour in `CLAUDE.md` and any affected `.context/` page.
43
+ 1. Update any `.context/` page the change made wrong. Add a page only if an
44
+ agent would get something wrong without it.
44
45
  2. Run the repo's own lint, typecheck and tests.
45
46
  3. Run `zeno check`. Every checker the repo declares under `commands:` runs
46
47
  with it.
@@ -52,10 +52,10 @@ folder.
52
52
 
53
53
  ## Naming and frontmatter
54
54
 
55
- - Append-only, time-ordered files → ISO date: `YYYY-MM-DD-slug.md`.
56
- - Files referenced by a stable ID → zero-padded number: `NNNN-slug.md`.
57
- - Frontmatter follows the wiki schema: `id`, `type`, `status`, `last-verified`,
58
- `supersedes`, `superseded-by`. Kebab-case keys.
55
+ - Dated sources (`raw/`) → ISO date: `YYYY-MM-DD-slug.md`.
56
+ - Features → zero-padded number: `NNNN-slug.md`.
57
+ - Everything else is named by topic and edited in place.
58
+ - Frontmatter: `last-verified`, kebab-case keys. See the wiki schema.
59
59
 
60
60
  ## Where each kind of document lives
61
61
 
@@ -64,8 +64,8 @@ separate folder per name.
64
64
 
65
65
  | Document | Home |
66
66
  |---|---|
67
- | ADR | `wiki/decisions/`, `status: canonical` |
68
- | RFC | `wiki/decisions/`, `status: draft` — the same page becomes the ADR when accepted |
67
+ | ADR | `wiki/decisions/<topic>.md`, overwritten when the choice changes |
68
+ | RFC | `features/`; once accepted, the choice goes in `wiki/decisions/` |
69
69
  | PRD | `features/`, `type: feature` |
70
70
  | Experiment | `features/`, `type: experiment`; the result is a decision page |
71
71
  | Initiative | `wiki/synthesis/`, linking its features and decisions |
@@ -99,16 +99,11 @@ An experiment adds `## Hypothesis`, `## Metric` (a name from
99
99
  `metrics/definitions.yml`) and `## Result` once it has run.
100
100
 
101
101
  The wiki describes what *is*; `features/` describes what is *next*. A shipped
102
- feature goes `status: shipped` and is never deleted.
102
+ feature goes `status: shipped`.
103
103
 
104
- ## `tasks.md` — the ordered queue
104
+ ## `tasks.md` — open work only
105
105
 
106
106
  ```markdown
107
- ---
108
- updated: 2026-09-29
109
- horizon: 2026-10-13 # this file is stale after this date
110
- ---
111
-
112
107
  # Tasks
113
108
 
114
109
  ## Now <!-- position is the dependency; do in order -->
@@ -116,7 +111,7 @@ horizon: 2026-10-13 # this file is stale after this date
116
111
  2. [ ] T002 Write the capture flows needs:T001 ~3h
117
112
 
118
113
  ## Parallel <!-- independent, safe to batch -->
119
- - [ ] T004 [P] Move the SKU IDs into the wiki ~1h
114
+ - [ ] T004 Move the SKU IDs into the wiki ~1h
120
115
 
121
116
  ## Blocked
122
117
  - [ ] T005 Submit the iOS build
@@ -125,14 +120,9 @@ horizon: 2026-10-13 # this file is stale after this date
125
120
 
126
121
  ## Waiting <!-- deferred by time, not by a task -->
127
122
  - [ ] T006 Second-language listing wait:2026-11-01
128
-
129
- ## Done
130
- - [x] T000 Set up .context done:2026-09-29
131
-
132
- ## Session Notes
133
- ### 2026-09-29
134
- - Done: T000. Learned: … Next: T001.
135
123
  ```
136
124
 
137
- A blocker with no named unblocking action and no check date is a silent leak.
138
- A task that needs more than a line and a continuation is a `features/` page.
125
+ Delete a task when it is done — the commit records it. No `## Done`, no session
126
+ notes. Cap: 100 lines. A blocker with no unblocking action and no check date is a
127
+ silent leak. A task that needs more than a line and a continuation is a
128
+ `features/` page.
@@ -1,299 +1,71 @@
1
- # Wiki — schema and rules
1
+ # Wiki — rules
2
2
 
3
- This folder is a **Claude-maintained knowledge base** for the product. The model is the LLM-Wiki pattern: `.context/raw/` holds immutable sources, `.context/wiki/` holds synthesised pages with `[[wikilinks]]`, this file defines the schema.
3
+ `.context/wiki/` holds what an agent would get wrong without being told. `.context/raw/` holds
4
+ sources, never edited. Answer from the wiki, not from raw.
4
5
 
5
- **Read this file before creating or editing any page under `.context/wiki/`.**
6
+ ## The test
6
7
 
7
- ---
8
-
9
- ## Three layers
10
-
11
- | Layer | Path | Mutability | Who writes |
12
- |---|---|---|---|
13
- | Raw | `.context/raw/`, `.context/designs/` | **Immutable** — never edit | Humans (or external systems) |
14
- | Wiki | `.context/wiki/` | Mutable, schema-bound | Claude (via `/zeno:ingest`, `/zeno:ask`, `/zeno:wiki-lint`) |
15
- | Index | `.context/wiki/index.md` | Auto-maintained | `/zeno:ingest` and `/zeno:wiki-lint` |
16
-
17
- The wiki is the **only** layer Claude reads for `/zeno:ask`. Never answer from `.context/raw/` directly — the synthesis must be written down or it doesn't exist.
18
-
19
- ---
8
+ Keep a line only if an agent would make a mistake without it. Never write what the
9
+ code, an HTML design, or `git log` already says. Describe what is true **now**:
10
+ history is git's job.
20
11
 
21
- ## Page types
22
-
23
- Every page in `.context/wiki/` belongs to exactly one type. The folder name is the type.
24
-
25
- | Type | Folder | What it owns | Example |
26
- |---|---|---|---|
27
- | `concept` | `concepts/` | An idea, principle, or invariant. Prose-shaped. | `offline-first`, `money-in-minor-units` |
28
- | `entity` | `entities/` | A thing with canonical values (price, name, enum). Table-shaped. | `plan-pro`, `plan-free` |
29
- | `surface` | `surfaces/` | One screen / route / email / callable. Links to a raw wireframe. | `onboarding-welcome` |
30
- | `flow` | `flows/` | A user journey. Ordered list of `[[surface]]` steps. | `new-user-to-first-save` |
31
- | `decision` | `decisions/` | One ADR. Filename = `YYYY-MM-DD-slug.md`. Append-only. | `2026-06-24-annual-plan-only` |
32
- | `synthesis` | `synthesis/` | Cross-cutting page built from many entities/concepts. Refreshable. | `pricing`, `launch-checklist` |
33
-
34
- A page cannot be two types. If it feels like it is, it's two pages — split it.
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
- ```
12
+ ## Folders
44
13
 
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 |
14
+ | Folder | Holds | Example |
61
15
  |---|---|---|
62
- | SOURCE.SENTRY.PROJECT | streaks-mobile | 2026-10-23 |
63
- | SOURCE.SENTRY.ACCESS | mcp: sentry | 2026-10-23 |
16
+ | `concepts/` | a rule or invariant | `money-in-minor-units` |
17
+ | `entities/` | canonical values (prices, SKUs, limits) — the one home for each | `plan-pro` |
18
+ | `decisions/` | one file per topic: the current choice | `pricing` |
19
+ | `surfaces/` | a screen's notes: what its HTML can't show (only screens that have some) | `paywall` |
20
+ | `synthesis/` | research and cross-cutting analysis | `tax-table-verification-ay-2026-27` |
64
21
 
65
- ## How to query
66
- The claims this source can answer, and one example query for each.
67
- ```
22
+ There is no page per screen: the HTML design is the screen spec. A screen gets a
23
+ `surfaces/<slug>.md` only when it has a constraint or decision invisible in its pixels;
24
+ the design set's notes button shows it, and `routes.js` sets `notes: null` for every
25
+ other screen. A user journey worth keeping is a concept.
26
+ A product may add a folder in `.claude/zeno.yaml` under `wiki.types`, with a reason.
68
27
 
69
- zeno has no connector of its own. The repo picks what does the reading, and
70
- `ACCESS` names it: `mcp: <server>`, `skill: <name>`, `cli: <command>`, or
71
- `manual: <where>` for a dashboard with no API, which a person reads and pastes
72
- into `raw/`. A `cli:` must also be in `doctor.tools` in `.claude/zeno.yaml`, so `zeno doctor`
73
- reports when it is missing. `zeno check` fails a source page that breaks either
74
- rule or has no `## How to query`. Never put a key or token on the page.
28
+ ## Pages
75
29
 
76
- The weigh gate reads sources to check a claim (C-14). Every query follows these
77
- rules:
30
+ - Frontmatter: `last-verified: YYYY-MM-DD` is the only required field. `id` is kept
31
+ where code references it (`SOURCE.*`, entity keys).
32
+ - Write only the sections you have something to say in. No `TBD`, no `none yet`.
33
+ - One file per topic, edited in place. No dated names, no `v2`, no supersede chains.
34
+ - Caps: page 60 lines, decision 30. Synthesis is exempt.
35
+ - Link with `[[slug]]` (the file name without `.md`). `index.md` is one line per page.
78
36
 
79
- - **Read only.** Never create, change or delete anything in the source.
80
- - **Small.** One or two queries per claim, with a row limit. Some sources bill
81
- per query.
82
- - **No user data in git.** Save counts, totals and rates, never rows that name
83
- a person, an email or a device. Results that back a decision go to
84
- `raw/YYYY-MM-DD-<source>-<slug>.md`: the query, the date and the numbers.
37
+ ## Decisions
85
38
 
86
- ---
87
-
88
- ## Frontmatter (mandatory)
89
-
90
- Every page begins with YAML frontmatter:
91
-
92
- ```yaml
93
- ---
94
- id: PLAN.PRO # Stable, uppercase, dot-separated. Never changes. Referenced from code.
95
- type: entity # concept | entity | surface | flow | decision | synthesis
96
- status: canonical # canonical | draft | superseded
97
- last-verified: 2026-06-24 # ISO date. /zeno:wiki-lint flags pages > 90 days.
98
- supersedes: [] # IDs this page replaces (decisions only)
99
- superseded-by: null # If non-null, this page is no longer canonical
100
- ---
101
- ```
102
-
103
- ### ID conventions
104
-
105
- | Type | ID shape | Example |
106
- |---|---|---|
107
- | concept | `CONCEPT.<SLUG>` | `CONCEPT.OFFLINE-FIRST` |
108
- | entity | `<DOMAIN>.<NAME>` | `PLAN.PRO`, `PLAN.FREE` |
109
- | surface | `SURFACE.<APP>.<PATH>` | `SURFACE.MOBILE.ONBOARDING.WELCOME` |
110
- | flow | `FLOW.<SLUG>` | `FLOW.NEW-USER-TO-FIRST-SAVE` |
111
- | decision | `DECISION.<YYYY-MM-DD>.<SLUG>` | `DECISION.2026-06-24.ANNUAL-PLAN-ONLY` |
112
- | synthesis | `SYNTHESIS.<SLUG>` | `SYNTHESIS.PRICING` |
113
-
114
- Once assigned, an ID never changes. If a page is renamed, the ID stays. If the thing it describes is killed, mark `status: superseded` and set `superseded-by`; do not delete.
115
-
116
- ---
117
-
118
- ## Page body — required sections
119
-
120
- Each type has a required section list. `/zeno:wiki-lint` fails the page if any are missing.
121
-
122
- ### `concept`
123
39
  ```markdown
124
- ## Summary
125
- One sentence — what the concept is.
126
-
127
- ## Why it matters
128
- Why this is a load-bearing idea. Tie it to a constraint, an incident, or a principle.
129
-
130
- ## Implications
131
- Bulleted: what this forces / forbids in practice.
132
-
133
- ## Related
134
- - [[other-concept]] — relationship
135
- - [[entity-x]] — where this applies
136
-
137
- ## Sources
138
- - .context/raw/<source> or .context/designs/<surface>/<flow>/<screen>.html
139
- ```
140
-
141
- ### `entity`
142
- ```markdown
143
- ## Summary
144
- One sentence — what this entity is.
145
-
146
- ## Canonical values
147
- | Key | Value | Last changed |
148
- |---|---|---|
149
- | PRICE.PRO.YEARLY | $49 | 2026-04-12 |
150
- | STORAGE.PRO | 5 GB | 2026-04-12 |
151
-
152
- ## Related
153
- - [[concept-x]] — concept this entity instantiates
154
- - [[entity-y]] — sibling / upgrade-from
155
-
156
- ## Sources
157
- - .context/raw/<source>
158
- ```
159
-
160
- The `Canonical values` table is the single source of truth. Any value here that disagrees with the codebase is a bug — fix the code or update this page.
161
-
162
- ### `surface`
163
- ```markdown
164
- ## Summary
165
- One sentence — what this screen does.
166
-
167
- ## Raw wireframe
168
- - .context/designs/<surface>/<flow>/<screen>.html
169
-
170
- ## Copy
171
- Verbatim user-visible strings. Each must pass the [[copy-voice]] rules.
172
-
173
- ## Flows that touch this surface
174
- - [[new-user-to-first-save]] — step 4
175
-
176
- ## Entities referenced
177
- - [[plan-free]]
178
- - [[privacy-consent]]
179
-
180
- ## Sources
181
- - .context/raw/<source>
182
- ```
183
-
184
- ### `flow`
185
- ```markdown
186
- ## Summary
187
- One sentence — what journey this is.
188
-
189
- ## Audience
190
- end-user | admin
191
-
192
- ## Entry
193
- Where the user arrives from.
194
-
195
- ## Steps
196
- | # | Step | Surface | Notes |
197
- |---|---|---|---|
198
- | 1 | Welcome | [[welcome]] | First launch |
199
- | 2 | Sign up | [[sign-up]] | Email |
200
- | ... |
201
-
202
- ## Exit
203
- The success state.
204
-
205
- ## Escapes
206
- Back-button, skips, abandonment outcomes.
207
-
208
- ## Sources
209
- - .context/raw/<source>
210
- ```
211
-
212
- ### `decision`
213
- ```markdown
214
- ## Decision
215
- One-line statement.
216
-
217
- ## Why
218
- The motivation. Cite the trigger — incident, user feedback, regulator, scope cut.
219
-
220
- ## Impact
221
- What changes downstream. Which [[entities]] / [[surfaces]] / [[concepts]] update.
222
-
223
- ## Status
224
- Active | Superseded by [[decision-id]]
225
-
226
- ## Sources
227
- - .context/raw/<source>
228
- ```
229
-
230
- Decisions are append-only. To reverse a decision, write a new one with `supersedes: [DECISION.<old>]` and mark the old one's `superseded-by`.
231
-
232
- ### `synthesis`
233
- ```markdown
234
- ## Summary
235
- What this synthesis combines.
236
-
237
- ## Built from
238
- - [[entity-x]]
239
- - [[entity-y]]
240
- - [[concept-z]]
241
-
242
- ## Body
243
- The cross-cutting answer. Every claim links back to a source page.
244
-
245
- ## Last refresh
246
- 2026-06-24 — refresh when any "Built from" page changes.
247
-
248
- ## Sources
249
- - (synthesis pages cite wiki pages, not raw files)
250
- ```
251
-
252
- Synthesis pages are **derived**. If a source page changes, `/zeno:wiki-lint` marks the synthesis stale.
253
-
254
40
  ---
255
-
256
- ## Linking
257
-
258
- - Use `[[page-slug]]` — the filename without `.md`. Match the file exactly (case-sensitive on case-sensitive filesystems).
259
- - Cross-folder links use the slug, not the path: `[[tier-self]]`, not `[[entities/tier-self]]`.
260
- - An `[[unknown-link]]` is a TODO marker — `/zeno:wiki-lint` reports it. Never silently delete a broken link; either create the target page or rename the link.
261
- - The `Related` section is what makes the graph navigable. Every page must have at least two outbound links (or be flagged as orphan-source).
262
-
41
+ last-verified: 2026-10-27
263
42
  ---
43
+ # Pricing
44
+ **Choice:** annual plan only, ₹249 ([[plan-pro]]).
45
+ **Why:** monthly churn was 3× annual in the 2026-06 test.
46
+ **Rejected:** monthly plan (churn) · lifetime deal (no recurring revenue).
47
+ ```
264
48
 
265
- ## Lint rules (what `/zeno:wiki-lint` enforces)
266
-
267
- | Rule | Failure |
268
- |---|---|
269
- | Missing required section | Block — must be fixed |
270
- | `[[wikilink]]` to a non-existent page | Block — create target or rename link |
271
- | Orphan page (zero inbound links) | Warn |
272
- | Two entities claim the same `id` | Block |
273
- | Two entities have contradictory canonical values for the same key | Block |
274
- | `last-verified` > 90 days | Warn |
275
- | `status: superseded` without `superseded-by` | Block |
276
- | Source file in `.context/raw/` not referenced by any wiki page | Warn |
277
- | Synthesis page where a `Built from` page changed after `Last refresh` | Warn |
278
- | Filename does not match the `id`'s slug portion | Block |
49
+ When the choice changes, overwrite the file. `Rejected:` is what stops the same idea
50
+ being proposed again; keep it. Fix lists and audit results go in commit messages or
51
+ `CHANGELOG.md`, not here.
279
52
 
280
- `/zeno:wiki-lint --fix` auto-fixes: filename ↔ id mismatches, missing `last-verified` (sets to today if the page is touched), index regeneration.
53
+ ## Data sources
281
54
 
282
- ---
55
+ Each tool that holds product data is one entity page, `id: SOURCE.<NAME>`, with a
56
+ `## How to query` section and a `SOURCE.<NAME>.ACCESS` row: `mcp: <server>`,
57
+ `skill: <name>`, `cli: <command>` (also listed in `doctor.tools`) or `manual: <where>`.
58
+ Queries are read-only, one or two per claim, with a row limit. Save counts and
59
+ rates to `raw/YYYY-MM-DD-<source>-<slug>.md`, never rows that name a person. Never
60
+ put a key or token on the page.
283
61
 
284
- ## What the wiki is NOT for
62
+ ## Not for
285
63
 
286
- - **Not for code documentation.** Docstrings stay in code. The wiki is product-shaped, not API-shaped.
287
- - **Not for transient task tracking.** Use `TaskCreate` / PR descriptions / commit messages for ephemeral work.
288
- - **Not for chat logs.** If a chat produces a decision, write a `decision` page; throw the chat away.
289
- - **Not for drafts of code.** If it would be a comment, it's a comment.
290
-
291
- ---
64
+ Code docs (they go next to the code) · tasks (`tasks.md`) · plans (`features/`) ·
65
+ chat logs · session notes · changelogs.
292
66
 
293
- ## Source of truth
67
+ ## Checked by `zeno check`
294
68
 
295
- `.context/wiki/` is the canonical knowledge base, and every page in it follows
296
- the schema above — there are no plan-shaped exceptions. What is *next* lives in
297
- `.context/features/`, and the ordered queue lives in `.context/tasks.md`; the
298
- wiki describes what is, not what is planned. The repo root holds no
299
- product-knowledge `.md` files.
69
+ `wiki-frontmatter` (last-verified, placeholders, stale > 90 days) · `context-size`
70
+ (caps, dated decision names, no `flows/`) · `canonical-values` (a
71
+ price in a decision or screen note is held by an entity) · `sources`.