@freetesh/zeno 0.7.0 → 0.9.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +35 -10
- package/cli/dist/checks/context-layout.js +5 -4
- package/cli/dist/checks/index.js +4 -0
- package/cli/dist/checks/metrics.js +14 -0
- package/cli/dist/checks/sources.js +39 -0
- package/cli/dist/checks/wiki-frontmatter.js +14 -11
- package/cli/dist/commands/init.js +1 -1
- package/cli/dist/config/load.js +4 -1
- package/cli/dist/config/schema.js +32 -3
- package/package.json +3 -3
- package/skills/decision/SKILL.md +1 -1
- package/skills/ingest/SKILL.md +4 -3
- package/skills/weigh/SKILL.md +67 -22
- package/skills/wiki-lint/SKILL.md +4 -4
- package/standards/README.md +2 -1
- package/standards/agent-contract.md +6 -0
- package/standards/constitution.md +12 -1
- package/standards/context-layout.md +26 -0
- package/standards/dev-methods.md +29 -0
- package/standards/wiki-schema.md +49 -0
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
{
|
|
8
8
|
"name": "zeno",
|
|
9
9
|
"source": "./",
|
|
10
|
-
"description": "
|
|
10
|
+
"description": "Keeps your domain knowledge and AI rules local and enforces them, so Claude hallucinates less."
|
|
11
11
|
}
|
|
12
12
|
],
|
|
13
|
-
"description": "zeno:
|
|
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.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.9.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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
|
@@ -18,6 +19,15 @@ It has two halves that read the same config:
|
|
|
18
19
|
cost vs value, and the write-less ladder, then answers **DO IT**, **DO IT
|
|
19
20
|
DIFFERENTLY** or **PUSH BACK** with citations. Push back stops work until you
|
|
20
21
|
give a reason; overrides are logged and feed `zeno learn`.
|
|
22
|
+
- **No assumptions.** A request is not a fact. Each claim in it ("users want
|
|
23
|
+
this", "it is slow") is checked against the repo and its data sources and
|
|
24
|
+
marked backed or not. Work that rests on an assumption stops until you agree
|
|
25
|
+
to written, numbered terms. Replies use plain words.
|
|
26
|
+
- **Your data sources, your tools.** zeno has no connectors. A repo describes
|
|
27
|
+
each tool that holds its data (Sentry, Datadog, Snowflake) on a `SOURCE.*`
|
|
28
|
+
wiki page that names the MCP server, skill or CLI that reads it. The gate
|
|
29
|
+
queries it, read-only, to back or reject a claim, and `zeno check` fails a
|
|
30
|
+
source page that does not say how to read it.
|
|
21
31
|
- **Knowledge that is found, not remembered.** Every repo's `.context/wiki` and
|
|
22
32
|
feature board is indexed with [QMD](https://github.com/tobi/qmd) (local hybrid
|
|
23
33
|
search). Claude is told to search before answering.
|
|
@@ -42,7 +52,7 @@ It has two halves that read the same config:
|
|
|
42
52
|
|
|
43
53
|
```bash
|
|
44
54
|
# plugin (from GitHub)
|
|
45
|
-
claude plugin marketplace add ritesh-firodiya/zeno
|
|
55
|
+
claude plugin marketplace add ritesh-firodiya/zeno
|
|
46
56
|
claude plugin install zeno@zeno
|
|
47
57
|
|
|
48
58
|
# CLI (Node 22+)
|
|
@@ -83,6 +93,11 @@ Run the CLI from source (Node 23.6+ runs the TypeScript directly):
|
|
|
83
93
|
`pnpm install && ln -s "$PWD/cli/src/index.ts" ~/.local/bin/zeno`.
|
|
84
94
|
`pnpm test`, `pnpm typecheck` and `pnpm build` run from the repo root.
|
|
85
95
|
|
|
96
|
+
**Releasing.** Bump `version` in `package.json` and `.claude-plugin/plugin.json`,
|
|
97
|
+
then push or merge to `main`. The `release` workflow runs the tests, publishes
|
|
98
|
+
`@freetesh/zeno` to npm, tags `vX.Y.Z` and creates the GitHub release. A push
|
|
99
|
+
without a bump releases nothing. It needs the `NPM_TOKEN` Actions secret.
|
|
100
|
+
|
|
86
101
|
## CLI
|
|
87
102
|
|
|
88
103
|
```bash
|
|
@@ -117,13 +132,22 @@ commands: # your own checkers, run by `zeno check`
|
|
|
117
132
|
doctor:
|
|
118
133
|
tools:
|
|
119
134
|
- { tool: pnpm, why: product scripts, hint: npm i -g pnpm }
|
|
135
|
+
wiki:
|
|
136
|
+
types: # added to the six built-in page types
|
|
137
|
+
- type: postmortem
|
|
138
|
+
folder: postmortems
|
|
139
|
+
reason: incidents need a timeline and a fix
|
|
140
|
+
sections: [Summary, Timeline, Root cause, Fix, Sources] # zeno check requires each `## ` heading
|
|
141
|
+
context:
|
|
142
|
+
folders: # added to the ten .context folders
|
|
143
|
+
- { folder: research, reason: interview notes, kept apart from raw/ }
|
|
120
144
|
```
|
|
121
145
|
|
|
122
146
|
```yaml
|
|
123
147
|
# <repo>/.claude/zeno.yaml — written by `zeno init`
|
|
124
148
|
zeno: 0.6.0
|
|
125
149
|
kind: expo-app # expo-app | web | content
|
|
126
|
-
modules: [docs, knowledge,
|
|
150
|
+
modules: [docs, knowledge, code, listing] # all optional; design is added when .context/designs exists, metrics only when asked
|
|
127
151
|
packs: [expo]
|
|
128
152
|
checks:
|
|
129
153
|
stale-copies: { enabled: false, reason: "Cursor is still used here" }
|
|
@@ -132,7 +156,8 @@ weigh:
|
|
|
132
156
|
```
|
|
133
157
|
|
|
134
158
|
Scalars: the most specific layer wins. `packs` and `standards` add up across
|
|
135
|
-
layers; `commands
|
|
159
|
+
layers; `commands`, `doctor.tools`, `wiki.types` and `context.folders` merge
|
|
160
|
+
by name. Built-in types and folders cannot be removed or redefined. A check turned off must
|
|
136
161
|
carry a `reason:`, and `zeno check` lists every override.
|
|
137
162
|
|
|
138
163
|
## Standards
|
|
@@ -152,12 +177,12 @@ your own.
|
|
|
152
177
|
|
|
153
178
|
## Evals
|
|
154
179
|
|
|
155
|
-
`evals/` runs
|
|
180
|
+
`evals/` runs nine requests against a fixture product (`evals/fixtures/streaks`)
|
|
156
181
|
with and without the plugin, each in a sandbox seeded with the fixture and a
|
|
157
182
|
local QMD index:
|
|
158
183
|
|
|
159
184
|
```bash
|
|
160
|
-
claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd__*' --runs 2
|
|
185
|
+
claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd__*' Write Edit --runs 2
|
|
161
186
|
```
|
|
162
187
|
|
|
163
188
|
## Layout
|
|
@@ -165,7 +190,7 @@ claude plugin eval . --scaffold --mocks off --allow-tools 'mcp__plugin_zeno_qmd_
|
|
|
165
190
|
```
|
|
166
191
|
.claude-plugin/ plugin manifest and marketplace
|
|
167
192
|
skills/ weigh, standards, ask, ingest, decision, wiki-lint
|
|
168
|
-
agents/ challenger — independent second opinion for large requests
|
|
193
|
+
agents/ challenger — independent second opinion for too-large requests
|
|
169
194
|
hooks/ session start: constitution titles, qmd refresh
|
|
170
195
|
standards/ default standards
|
|
171
196
|
packs/ vendor doc packs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
|
|
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
|
|
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() && !
|
|
30
|
-
findings.push(finding("error", `.context/${entry.name}`, `${entry.name}/ is not
|
|
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"));
|
package/cli/dist/checks/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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";
|
|
7
|
+
import { sources } from "./sources.js";
|
|
6
8
|
import { wikiFrontmatter } from "./wiki-frontmatter.js";
|
|
7
9
|
export const ALL_CHECKS = [
|
|
8
10
|
contextLayout,
|
|
@@ -11,5 +13,7 @@ export const ALL_CHECKS = [
|
|
|
11
13
|
standardsCopies,
|
|
12
14
|
staleCopies,
|
|
13
15
|
architecture,
|
|
16
|
+
metricsDefinitions,
|
|
17
|
+
sources,
|
|
14
18
|
commandChecks,
|
|
15
19
|
];
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseFrontmatter } from "../lib/frontmatter.js";
|
|
4
|
+
import { wikiPages } from "./wiki-frontmatter.js";
|
|
5
|
+
// zeno has no connectors: a source page must say which MCP server, skill or CLI
|
|
6
|
+
// reads it, or the weigh gate cannot turn a claim into a fact.
|
|
7
|
+
const ACCESS = /^(mcp|skill|cli):\s*(\S+)/;
|
|
8
|
+
const HOW_TO_QUERY = /^## How to query\s*$/m;
|
|
9
|
+
function accessValue(body, id) {
|
|
10
|
+
const row = body.split("\n").find((line) => line.replace(/\s/g, "").startsWith(`|${id}.ACCESS|`));
|
|
11
|
+
return row ? (row.split("|")[2]?.trim() ?? "") : null;
|
|
12
|
+
}
|
|
13
|
+
export const sources = {
|
|
14
|
+
id: "sources",
|
|
15
|
+
module: "docs",
|
|
16
|
+
description: "each SOURCE.* page says how to read it (mcp, skill or a cli in doctor.tools) and has a How to query section",
|
|
17
|
+
run({ repo, config }) {
|
|
18
|
+
const tools = new Set(config.doctor.tools.map((tool) => tool.tool));
|
|
19
|
+
return wikiPages(repo).flatMap((file) => {
|
|
20
|
+
const { data, body } = parseFrontmatter(readFileSync(join(repo, file), "utf8"));
|
|
21
|
+
const id = String(data?.id ?? "");
|
|
22
|
+
if (data?.type !== "entity" || !id.startsWith("SOURCE."))
|
|
23
|
+
return [];
|
|
24
|
+
const findings = [];
|
|
25
|
+
const add = (message) => findings.push({ check: "sources", level: "error", file, message });
|
|
26
|
+
if (!HOW_TO_QUERY.test(body))
|
|
27
|
+
add("no `## How to query` section");
|
|
28
|
+
const access = accessValue(body, id);
|
|
29
|
+
const match = access === null ? null : ACCESS.exec(access);
|
|
30
|
+
if (access === null)
|
|
31
|
+
add(`no ${id}.ACCESS row — say how to read it`);
|
|
32
|
+
else if (!match)
|
|
33
|
+
add(`${id}.ACCESS is "${access}" — write mcp: <server>, skill: <name> or cli: <command>`);
|
|
34
|
+
else if (match[1] === "cli" && !tools.has(match[2]))
|
|
35
|
+
add(`cli ${match[2]} is not in doctor.tools — zeno doctor cannot report it missing`);
|
|
36
|
+
return findings;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -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 =
|
|
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,
|
|
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,
|
package/cli/dist/config/load.js
CHANGED
|
@@ -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", "
|
|
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", "
|
|
41
|
-
web: ["docs", "knowledge", "
|
|
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.
|
|
4
|
-
"description": "zeno CLI:
|
|
3
|
+
"version": "0.9.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
|
|
9
|
+
"url": "git+https://github.com/ritesh-firodiya/zeno.git"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"bin": {
|
package/skills/decision/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Decision to record: `$ARGUMENTS`
|
|
|
9
9
|
|
|
10
10
|
## Steps
|
|
11
11
|
|
|
12
|
-
1.
|
|
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.
|
package/skills/ingest/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/skills/weigh/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
@@ -19,7 +19,10 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
|
|
|
19
19
|
|
|
20
20
|
1. **Restate the goal** in one line — what outcome the user wants, not the
|
|
21
21
|
mechanism they named. A named mechanism ("use library X", "move to Firebase")
|
|
22
|
-
is a proposal to weigh, not a given.
|
|
22
|
+
is a proposal to weigh, not a given. Then **list the claims** the request
|
|
23
|
+
makes or rests on: "users want it", "it will help retention", "it is
|
|
24
|
+
missing", "it is slow". The request is not a fact (C-14); each claim is
|
|
25
|
+
checked in step 4.
|
|
23
26
|
|
|
24
27
|
2. **Rules.** Run `zeno standards --constitution` for the principle titles
|
|
25
28
|
(zeno's and any the team added), and read the constitution files that
|
|
@@ -29,8 +32,10 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
|
|
|
29
32
|
in its area.
|
|
30
33
|
|
|
31
34
|
3. **Domain.** Search before you judge. Run `mcp__plugin_zeno_qmd__query` with
|
|
32
|
-
the goal in plain words, then the key nouns
|
|
33
|
-
|
|
35
|
+
the goal in plain words, then the key nouns, scoped with `collections` to
|
|
36
|
+
this product's `<repo>-wiki` and `<repo>-features` (the names `zeno init`
|
|
37
|
+
gives them; add `pack-<name>` when the question is about a vendor's API). Other products' hits are noise here; step 5 is where
|
|
38
|
+
siblings are searched. Read the top hits with `mcp__plugin_zeno_qmd__get`. Look for: an ADR that already
|
|
34
39
|
decided this, a canonical value it would change, a concept it would break.
|
|
35
40
|
If qmd is unavailable, read `.context/wiki/index.md` and `decisions/`.
|
|
36
41
|
**Architecture:** if `.claude/zeno.yaml` lists the `architecture` module, read
|
|
@@ -40,29 +45,47 @@ product's `.claude/zeno.yaml` may tighten this under `weigh.trivial`.
|
|
|
40
45
|
is **too-large** unless a decision page already approves it. Cite the rule
|
|
41
46
|
by its `name`.
|
|
42
47
|
|
|
43
|
-
4. **
|
|
48
|
+
4. **Claims.** Mark each claim from step 1 **backed**, **partly backed** or
|
|
49
|
+
**not backed**, with the fact and its source. Look in what step 3 read, the
|
|
50
|
+
code and `.context/raw/`. When that is not enough, look for a data source:
|
|
51
|
+
query qmd for `SOURCE` pages in this product's wiki. Each says what it holds,
|
|
52
|
+
how to read it (`mcp:`, `skill:` or `cli:`) and which claims it answers. If
|
|
53
|
+
one fits, run its query and follow the query rules in `wiki-schema.md`
|
|
54
|
+
("Data sources"): read only, one or two small queries, counts not people.
|
|
55
|
+
Save a result that decides the verdict to `.context/raw/`. If the tool is not
|
|
56
|
+
available, say so and leave the claim not backed. If no source fits, name
|
|
57
|
+
the kind of source that would answer it ("an analytics tool with weekly
|
|
58
|
+
active users"). Never mark a claim backed because the user said it again.
|
|
59
|
+
|
|
60
|
+
5. **Duplicates and conflicts.** Check `.context/features/` and `.context/tasks.md`
|
|
44
61
|
of this product, and the features collections of sibling products in qmd:
|
|
45
62
|
is this already planned, built, shelved, or in progress elsewhere? Would it
|
|
46
63
|
conflict with something in `## Now`?
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
6. **Cost vs value.**
|
|
49
66
|
- Size: **small** (1 file, < 50 lines) · **medium** (2–5 files, < 200 lines) ·
|
|
50
67
|
**large** (5+ files) · **too-large** (architecture change — split it).
|
|
51
68
|
- Ongoing cost: new dependency, new service bill, more to maintain, more to
|
|
52
69
|
test, store review risk.
|
|
53
70
|
- Value: name the user or the metric it moves (`metrics/definitions.yml` if
|
|
54
|
-
the product has one). If neither can be named, say "value unproven".
|
|
71
|
+
the product has one). If neither can be named, say "value unproven". A
|
|
72
|
+
value claim is checked like any other claim, in step 4.
|
|
73
|
+
- Evidence: say which evidence from `dev-methods.md` the work must leave — a
|
|
74
|
+
feature page, a failing test first, or a metric and snapshot.
|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
7. **Write less.** Climb the ladder and stop at the first rung that works:
|
|
57
77
|
delete it → reuse something in this repo → reuse from a sibling product or
|
|
58
78
|
shared script → platform or standard library → an existing well-used package
|
|
59
79
|
→ fewer lines → new code. If the `ponytail` plugin is installed, follow its
|
|
60
80
|
ladder for this step.
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
8. **Too-large, or the user asked for a second opinion?** Then, and only then,
|
|
83
|
+
ask the `challenger` agent for an independent read. It costs tens of
|
|
84
|
+
thousands of tokens, so hand it what you already have: the request, the repo
|
|
85
|
+
path, your draft verdict, and every file and wiki page you read with one line
|
|
86
|
+
on what each says. It checks your reasoning and looks only where you did
|
|
87
|
+
not. Where you and it disagree, show both. A large request is weighed by you
|
|
88
|
+
alone.
|
|
66
89
|
|
|
67
90
|
## Verdict
|
|
68
91
|
|
|
@@ -76,26 +99,48 @@ Why:
|
|
|
76
99
|
- <C-xx> <one line> — <file or wiki page cited>
|
|
77
100
|
- ...
|
|
78
101
|
Instead: <only for DO IT DIFFERENTLY — the cheaper or safer way>
|
|
102
|
+
Claims:
|
|
103
|
+
- "<claim>" — backed | partly backed | not backed — <fact and source, or what was searched>
|
|
104
|
+
Assumed: <every claim not backed, or "none">
|
|
79
105
|
Open questions: <only if a real ambiguity blocks a good answer>
|
|
80
106
|
```
|
|
81
107
|
|
|
82
108
|
- Every bullet under **Why** cites a principle ID and a source (a wiki page, ADR,
|
|
83
|
-
standard, feature or
|
|
84
|
-
|
|
85
|
-
- **DO IT**
|
|
109
|
+
standard, feature, file or saved query result). A reason with no source is an
|
|
110
|
+
opinion: drop it, or list it under **Assumed**.
|
|
111
|
+
- **DO IT** only when **Assumed** is "none". Proceed with the work in the same
|
|
112
|
+
turn.
|
|
86
113
|
- **DO IT DIFFERENTLY** → proceed with the *Instead* version only if the user
|
|
87
|
-
already accepted it; otherwise stop and ask
|
|
88
|
-
- **PUSH BACK** → stop. Do not start the work, not even a part of it.
|
|
89
|
-
|
|
114
|
+
already accepted it and **Assumed** is "none"; otherwise stop and ask.
|
|
115
|
+
- **PUSH BACK** → stop. Do not start the work, not even a part of it.
|
|
116
|
+
- **Something is assumed** → stop, and give the user three choices: bring the
|
|
117
|
+
facts (and say where they would go), agree to terms, or drop it.
|
|
118
|
+
|
|
119
|
+
Write the whole reply in plain words (C-15): short sentences, common words, no
|
|
120
|
+
jargon the wiki does not define.
|
|
121
|
+
|
|
122
|
+
## Terms (when the user wants to go on anyway)
|
|
123
|
+
|
|
124
|
+
When the user wants to go on despite a PUSH BACK or an assumption, write the
|
|
125
|
+
terms as a numbered list: what is in scope, what is out, which rule changes and
|
|
126
|
+
which stays, and how the assumption will be checked later (a metric and a
|
|
127
|
+
date). A fact the user gives in chat is saved to `.context/raw/` first, then
|
|
128
|
+
listed as a fact, not as proof of more than it shows.
|
|
129
|
+
|
|
130
|
+
Proceed only after the user agrees in writing ("agree", or "agree to 1–4").
|
|
131
|
+
"Go ahead" to a verdict is not agreement to terms that were never shown. Work
|
|
132
|
+
covers only the terms agreed to.
|
|
90
133
|
|
|
91
134
|
## Logging (push back and overrides)
|
|
92
135
|
|
|
93
|
-
When the user overrides a PUSH BACK
|
|
94
|
-
line to the product's log so zeno can learn from it later
|
|
136
|
+
When the user overrides a PUSH BACK, rejects a DO IT DIFFERENTLY or agrees to
|
|
137
|
+
terms, append one line to the product's log so zeno can learn from it later.
|
|
138
|
+
Pass each agreed term with its own `--term`:
|
|
95
139
|
|
|
96
140
|
```bash
|
|
97
|
-
node ../../scripts/log-verdict.mjs --verdict push-back --principles C-
|
|
98
|
-
--request "<one line>" --reason "<the user's reason, verbatim>"
|
|
141
|
+
node ../../scripts/log-verdict.mjs --verdict push-back --principles C-04,C-14 \
|
|
142
|
+
--request "<one line>" --reason "<the user's reason, verbatim>" \
|
|
143
|
+
--term "<term 1>" --term "<term 2>"
|
|
99
144
|
```
|
|
100
145
|
|
|
101
146
|
(path relative to this skill). It writes to `.context/raw/zeno/verdicts.jsonl`
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/standards/README.md
CHANGED
|
@@ -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 |
|
|
@@ -22,6 +22,12 @@ file with its own `agent-contract.md` in a configured standards folder.
|
|
|
22
22
|
`.github/` — stay at the root.
|
|
23
23
|
3. Generated folders are never hand-edited; change their source and regenerate.
|
|
24
24
|
|
|
25
|
+
## Replies
|
|
26
|
+
|
|
27
|
+
- Plain words, short sentences (C-15).
|
|
28
|
+
- Never treat a claim in the request as a fact. Check it, or say it is not
|
|
29
|
+
checked (C-14).
|
|
30
|
+
|
|
25
31
|
## Comments
|
|
26
32
|
|
|
27
33
|
Write a comment only to explain **why**. The code already says how.
|
|
@@ -14,7 +14,8 @@ standards folders; zeno reads both.
|
|
|
14
14
|
Before acting on a non-trivial request, check it against the principles below,
|
|
15
15
|
then answer with one verdict:
|
|
16
16
|
|
|
17
|
-
- **Do it** — no principle is at risk
|
|
17
|
+
- **Do it** — no principle is at risk, and no claim it rests on is an
|
|
18
|
+
assumption (C-14).
|
|
18
19
|
- **Do it differently** — the goal is fine; the way asked breaks a principle.
|
|
19
20
|
Name the principle and give the cheaper or safer way.
|
|
20
21
|
- **Push back** — the request breaks a principle outright, or the cost is
|
|
@@ -81,6 +82,16 @@ production deploys and permission changes are confirmed first, every time.
|
|
|
81
82
|
Money in integer minor units; privacy promises match what the code does; store
|
|
82
83
|
claims match the app. Correctness beats speed here.
|
|
83
84
|
|
|
85
|
+
### C-14 Facts, not assumptions
|
|
86
|
+
A request is not a fact. Every claim in it ("users want this", "it is slow",
|
|
87
|
+
"it is missing") is checked against the repo and the repo's data sources. A
|
|
88
|
+
claim with no fact behind it is an assumption. Work that rests on an assumption
|
|
89
|
+
does not start until the user agrees to it, in writing, as numbered terms.
|
|
90
|
+
|
|
91
|
+
### C-15 Plain words
|
|
92
|
+
Short sentences and common words. No jargon unless the repo's wiki defines it.
|
|
93
|
+
Say what was checked, what was found and what happens next.
|
|
94
|
+
|
|
84
95
|
## Weights
|
|
85
96
|
|
|
86
97
|
A repo's `.claude/zeno.yaml` may raise or lower the weight of a principle under
|
|
@@ -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.
|
package/standards/wiki-schema.md
CHANGED
|
@@ -33,6 +33,55 @@ 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 | mcp: sentry | 2026-10-23 |
|
|
64
|
+
|
|
65
|
+
## How to query
|
|
66
|
+
The claims this source can answer, and one example query for each.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
zeno has no connector of its own. The repo picks what does the reading, and
|
|
70
|
+
`ACCESS` names it: `mcp: <server>`, `skill: <name>` or `cli: <command>`. A
|
|
71
|
+
`cli:` must also be in `doctor.tools` in `.claude/zeno.yaml`, so `zeno doctor`
|
|
72
|
+
reports when it is missing. `zeno check` fails a source page that breaks either
|
|
73
|
+
rule or has no `## How to query`. Never put a key or token on the page.
|
|
74
|
+
|
|
75
|
+
The weigh gate reads sources to check a claim (C-14). Every query follows these
|
|
76
|
+
rules:
|
|
77
|
+
|
|
78
|
+
- **Read only.** Never create, change or delete anything in the source.
|
|
79
|
+
- **Small.** One or two queries per claim, with a row limit. Some sources bill
|
|
80
|
+
per query.
|
|
81
|
+
- **No user data in git.** Save counts, totals and rates, never rows that name
|
|
82
|
+
a person, an email or a device. Results that back a decision go to
|
|
83
|
+
`raw/YYYY-MM-DD-<source>-<slug>.md`: the query, the date and the numbers.
|
|
84
|
+
|
|
36
85
|
---
|
|
37
86
|
|
|
38
87
|
## Frontmatter (mandatory)
|