Drop this file at
~/setup-agent-first.md. Pass to an AI coding agent via@~/setup-agent-first.mdalong with a project description. The agent will inspect the target repo, apply missing scaffolding, and leave the repo with a working doc infrastructure + enforcement.
Greenfield project: @~/setup-agent-first.md i want to start a project for <describe>. Set up the agent-first scaffolding first, before any product code.
Existing project: @~/setup-agent-first.md this repo already has some of this. Inspect current state, fill the gaps without overwriting what exists, report what you changed.
The file assumes a TypeScript/Node project (pnpm + turbo monorepo or single-app). Adapt paths for other stacks — the conventions are language-agnostic.
From OpenAI's harness-engineering post: repository knowledge is the system of record; the agent can't use what isn't in the repo. The goal of this setup is to make every future agent session productive on day one by giving it a stable map, progressive disclosure, and mechanical enforcement that keeps the map honest.
Core beliefs (encoded in docs/design-docs/core-beliefs.md, see Phase E):
- Map, not manual.
CLAUDE.md/AGENTS.mdis a table of contents pointing to real docs. Never a 1000-line kitchen sink. - Progressive disclosure. Agents start small, learn where to look next.
- File-size budgets. Skill files ~150L target, docs ~300L target. Targets, not fences.
- Skills encode conventions, docs encode knowledge. Skills auto-load on keyword; docs are pull-on-demand.
- Memory ≠ docs. Memory = user-specific cross-conversation state. Docs = project knowledge derivable from the repo.
- Documentation verification is mechanical. Broken links, stale frontmatter, missing round-trip pointers — all caught by CI, never human vigilance.
- Cross-cutting invariants get bidirectional links. One doc owns the rule; every code site points back via comment.
- The repo self-heals. Fix what you touch. No big refactors.
Before writing anything, run this discovery pass:
# What exists in the target repo?
ls -la # root inventory
cat CLAUDE.md 2>/dev/null | head -30 # existing agent instructions?
ls docs/ 2>/dev/null # existing doc tree?
ls .claude/skills/ 2>/dev/null # existing skills?
ls scripts/ 2>/dev/null # existing scripts?
cat package.json 2>/dev/null | head -30 # tech stack + existing npm scriptsThen decide the mode:
- Greenfield (no
CLAUDE.md, nodocs/, empty or near-empty repo) — apply every phase below in order. - Partial (some pieces exist) — for each phase, inspect first, fill gaps, NEVER overwrite existing convention choices. If an existing file uses
accepted/shippedinstead ofactivefor status, propose normalization but ask before changing.
Always ask before:
- Deleting existing files (even ones that look redundant)
- Changing existing naming conventions
- Committing (this document does not authorize commits — the user triggers those)
Always preserve:
- Existing commit history
- Existing skill authoring if skills already exist
- Existing
ARCHITECTURE.mdorREADME.mdcontent — merge, don't overwrite
Report format: after each phase, print a short summary of what you created, skipped, or deferred.
| Phase | Deliverable | Time |
|---|---|---|
| A | Core scaffold — CLAUDE.md, ARCHITECTURE.md, docs/ tree, gitignore | 5 min |
| B | Doc-format convention — docs/design-docs/doc-format.md + frontmatter schema |
10 min |
| C | Mechanical enforcement — scripts/check-docs-links.mjs + scripts/garden-docs.mjs + pnpm entries |
10 min |
| D | Task MD convention — .template.md + INDEX.md + date-based filenames |
5 min |
| E | Core beliefs + subagent policy — docs/design-docs/core-beliefs.md |
10 min |
| F | Skills directory — .claude/skills/ pattern + one example skill |
10 min |
| G | Customization — replace placeholders, wire to project specifics | 5 min |
| H | Verification — run pnpm docs:check, confirm clean |
2 min |
Total: ~60 min for greenfield. Faster for partial (skip what exists).
Create as a short table-of-contents file. Never put policy content here; link out.
# Claude Code Instructions
Act as a Senior Principal Engineer. High information density, minimalism, technical precision. Assume high context.
This file is a **map, not a manual**. It gates every-turn behavior (skill loading, coding rules) and points to the real content elsewhere. For principles behind this layout, see [docs/design-docs/core-beliefs.md](docs/design-docs/core-beliefs.md).
## Repository Map — Read These First
| When you need… | Read |
|---|---|
| Top-level architecture (domains × packages × data flow) | [ARCHITECTURE.md](ARCHITECTURE.md) |
| Design decisions, core beliefs, subagent policy | [docs/design-docs/index.md](docs/design-docs/index.md) |
| Doc-format + frontmatter + cross-linking conventions | [docs/design-docs/doc-format.md](docs/design-docs/doc-format.md) |
| Active / completed execution plans | [docs/exec-plans/README.md](docs/exec-plans/README.md) → [docs/exec-plans/tasks/INDEX.md](docs/exec-plans/tasks/INDEX.md) |
| Product specs (features, contracts) | [docs/product-specs/index.md](docs/product-specs/index.md) |
| Generated refs (DB schema, route catalog) | [docs/generated/README.md](docs/generated/README.md) |
| Vendor / tool references | [docs/references/README.md](docs/references/README.md) |
| Operational guides (dev, deploy, investigation) | [docs/guides/](docs/guides/) |
## Coding Guidelines
1. **YAGNI**: Never implement features, wrappers, or abstractions not explicitly requested.
2. **No "Junior" Patterns**: Avoid excessive defensive coding, unnecessary logging, or custom helpers when stdlib methods exist.
3. **Conciseness > "Robustness"**: 20 readable lines beat 100 lines of abstract architecture.
4. **Standard Libraries**: Prefer built-in language features over new logic.
5. **Comments**: Only for non-obvious *why*. Never restate *what*.
6. **Search First**: Before creating, search. Reuse and extend before creating.
7. **Consistency**: Match existing naming, patterns, doc style.
8. **Stewardship**: Fix dead imports, stale comments, misleading names, and defensive-but-dead code in files you touch. No big refactors — the repo self-heals one small diff at a time.
## Communication Guidelines
1. **No Fluff**: Start with the answer. No "Here is the code."
2. **BLUF**: Lead with the solution. Details only if necessary.
3. **Information Density**: Bullets, tables, short sentences.
4. **Correction**: If the premise is wrong, correct it immediately and neutrally.
## Skills — MANDATORY Load Gate
**Load the relevant skill BEFORE answering any architecture, convention, or implementation question about a package.** Skills carry authoritative conventions that override patterns you'd infer from grep alone.
| Skill | When to Load |
|---|---|
| {{ placeholder — list skills as you add them under .claude/skills/ }} | {{ when to load }} |
Skill authoring + update policy: [docs/design-docs/skill-system.md](docs/design-docs/skill-system.md).
## Subagent Policy (Brief)
Default to the main session — use Read, Grep, Glob, Edit directly. Spawn subagents only when the user explicitly asks, or the task is fully independent from current work (e.g. background test runs). Full rules: [docs/design-docs/core-beliefs.md §9](docs/design-docs/core-beliefs.md#9-subagent-policy).
## Execution Plans (Brief)
Non-trivial work is planned **before** code is written. Pick the right container:
- Single feature/fix → task MD (`docs/exec-plans/tasks/YYYY-MM-DD-*.md`)
- Multi-slice feature → exec-plan (`docs/exec-plans/active/<name>.md`)
- One-off fix → nothing, commit directly
Full decision tree + lifecycle: [docs/exec-plans/README.md](docs/exec-plans/README.md).
## Self-Healing Repo (Brief)
This is an **agent-first, self-healing repo**. Fix what you touch. No big refactors. Full belief + rules: [docs/design-docs/core-beliefs.md §8](docs/design-docs/core-beliefs.md#8-the-repo-self-heals).Then: ln -s CLAUDE.md AGENTS.md (so both tools find the same file).
High-level map: domains × packages × data flow. One page max. Do NOT duplicate what's in product-specs or design-docs; this is the 30,000-foot view.
Template:
# Architecture
## Summary
{{ one paragraph: what this system does, who uses it, key constraints }}
## Runtime topology
| Component | Stack | Role |
|---|---|---|
| {{ app 1 }} | {{ stack }} | {{ role }} |
| … | … | … |
## Domains
{{ list major business domains; each is a candidate skill later }}
## Data flow
{{ one diagram or prose paragraph showing how data moves through the system }}
## Where to look
- Conventions: `docs/design-docs/`
- Feature specs: `docs/product-specs/`
- Runbooks: `docs/guides/`
- Runtime details: source code under `apps/` (or the primary package dirs)docs/
├── design-docs/
│ ├── index.md
│ ├── core-beliefs.md
│ ├── doc-format.md
│ └── skill-system.md
├── product-specs/
│ └── index.md
├── guides/
├── exec-plans/
│ ├── README.md
│ ├── active/
│ └── tasks/
│ ├── .template.md
│ └── INDEX.md
├── generated/
│ └── README.md
└── references/
└── README.md
Each index.md is a pointer table. Each README.md (for generated/ and references/) explains what belongs there and what doesn't.
docs/design-docs/index.md:
---
title: Design docs — index
status: active
last_verified: {{ YYYY-MM-DD }}
owner: {{ primary-author-handle }}
---
# Design Docs
Design decisions, beliefs, and policies that shape how this codebase is built and how agents work inside it. Prescriptive — says what should be true.
## Contents
| Doc | Scope |
|---|---|
| [core-beliefs.md](core-beliefs.md) | Agent-first operating principles — repo knowledge layout, subagent policy. |
| [doc-format.md](doc-format.md) | Doc format + frontmatter enum + structure rules. Enforced by `pnpm docs:check`. |
| [skill-system.md](skill-system.md) | Skill authoring rules, auto-update policy. |
## How to add a design doc
See [doc-format.md](doc-format.md) for the full format spec. Then:
1. Create `docs/design-docs/<slug>.md` (or a folder with `index.md` if multi-file).
2. Add a row to the table above.
3. Run `pnpm docs:check` before committing.docs/product-specs/index.md: same pattern, scope = "features, contracts, domain glossary."
docs/exec-plans/README.md:
# Execution plans
Work planned before code is written. Two shapes:
- **Task MD** — single feature/fix. `tasks/YYYY-MM-DD-<slug>.md` with Status + Steps checklist.
- **Exec plan** — multi-slice feature. `active/<name>.md` with multiple phases.
DONE tasks move to `tasks/archive/` — still committed, read-only after archive.
See `tasks/.template.md` for the task-MD format. Filename is date-prefixed (collision-free under parallel engineering); legacy `T-NNN-` pattern is discouraged.Whatever the base .gitignore has, ensure these are present:
.claude/projects/
.DS_Store
*.log
node_modules/
dist/
.turbo/
.next/
Create docs/design-docs/doc-format.md verbatim:
---
title: Documentation format and conventions
status: active
last_verified: {{ YYYY-MM-DD }}
owner: {{ primary-author-handle }}
---
# Doc format and conventions
**Part of:** [index.md](index.md)
Single source of truth for how docs are written. Prescriptive. When a new doc is created or an existing one is edited, it should match the rules below.
### In scope
- `docs/design-docs/` — decisions, conventions, invariants
- `docs/product-specs/` — features, contracts, domain glossary
- `docs/guides/` (top level) — operational runbooks
### Out of scope
- `docs/references/` — external vendor docs (mostly imported, not authored)
- `docs/generated/` — auto-built, never hand-edited
- `docs/exec-plans/` — task MDs have their own header schema
Enforcement lives in `scripts/check-docs-links.mjs` (run via `pnpm docs:check`). Rules marked **(lint: error)** fail the check; **(lint: warn)** surface as warnings.
## Frontmatter
Every in-scope doc carries YAML frontmatter **(lint: warn if missing)**.
```yaml
---
title: <short descriptive title>
status: draft | active | deprecated | living
last_verified: YYYY-MM-DD
owner: <github-username or handle>
---| Value | Meaning |
|---|---|
draft |
Being written. Contains open questions. Not yet the source of truth. |
active |
Current source of truth. Reflects shipped code + accepted decisions. |
deprecated |
Superseded or no longer applicable. Kept for history. |
living |
Intentionally evolving (indexes, open-questions, glossaries). |
title— descriptive, not chaptered. Good: "Auth token lifecycle". Bad: "2. Auth".last_verified— ISO date. Update when reviewed against code. Stale > 180 days raises a warning.owner— who knows this best, who to route drift to. Usually one handle.
Exactly one H1 per doc. Should match title. First content element after the frontmatter.
Every sub-file inside a folder that contains an index.md MUST start with:
**Part of:** [index.md](index.md)Place directly under the H1.
For peer cross-refs, use:
**Related:** [path/to/doc.md](path/to/doc.md) — one-line reason, [other.md](other.md) — another reasonPlace under **Part of:** if present, otherwise directly under the H1. One-line reason is required — a bare link rots.
- Relative paths always. Never absolute URLs to your own repo.
- Code identifiers in backticks:
`functionName()`. - Inline file paths in backticks:
`<app>/<feature>/file.ts`.
Prefer tables over prose for 3+ same-shape items.
- Always fenced with language tag.
- JSON examples must parse — no
// commentlines.
- Doc: ~300L target, ~500L push-to-split, ~800L hard ceiling.
- Skill: ~150L target, ~250L push-to-split, 500L hard ceiling.
When a doc exceeds push-to-split, extract into sibling files (same folder + **Part of:**) or a references/ subfolder for skills.
When a doc owns an invariant enforced at multiple code sites, use the Enforcement-table pattern:
- Section heading containing
Enforcement(case-insensitive). - Markdown table listing every code file that enforces/mirrors the rule (backticked paths).
- Each listed code file MUST contain the doc's filename in a comment — forms a round-trip grep.
(lint: error) Missing back-pointers fail the check.
pnpm docs:garden is the maintenance counterpart to pnpm docs:check. Scans in-scope docs for drift against code, prints a punch list grouped by owner. Never blocks CI.
Drift types v1 catches: path-drift (backticked paths must resolve), symbol-drift (Enforcement-table identifiers must grep non-zero in code), staleness (last_verified > 180 days).
- Pick the path —
design-docs/for policy,product-specs/for features,guides/for runbooks. - Add frontmatter with all four fields.
- H1 matches
title. Add**Part of:**if in a folder with index.md. Add**Related:**for cross-refs. - Add a row to the parent folder's
index.md. - Run
pnpm docs:checkbefore committing.
---
## Phase C — Mechanical enforcement
Two scripts. Both Node stdlib-only. Both read-only.
### C.1 `scripts/check-docs-links.mjs`
This gates CI. Broken links, invalid status, missing Part-of, file-size warnings, staleness.
```javascript
#!/usr/bin/env node
// Verifies every `docs/...`, top-level `.md`, skill, and sibling-doc reference
// resolves to a real file + anchor. Also checks frontmatter, status enum,
// Part-of pointers, file size, and staleness.
import { readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
const ROOT = resolve(process.argv[2] ?? ".");
const errors = [];
const warnings = [];
function walk(dir, predicate, out = []) {
const s = safeStat(dir);
if (!s?.isDirectory()) return out;
for (const entry of readdirSync(dir)) {
if (["node_modules", ".next", "dist", ".turbo", ".playwright-cli"].includes(entry)) continue;
const full = join(dir, entry);
const st = safeStat(full);
if (!st) continue;
if (st.isDirectory()) walk(full, predicate, out);
else if (predicate(full)) out.push(full);
}
return out;
}
function safeStat(p) { try { return statSync(p); } catch { return null; } }
function safeRead(p) { try { return readFileSync(p, "utf8"); } catch { return null; } }
function rel(p) { return relative(ROOT, p); }
const mdFiles = [
...walk(join(ROOT, "docs"), (p) => p.endsWith(".md")),
...walk(join(ROOT, ".claude"), (p) => p.endsWith(".md")),
...["CLAUDE.md", "AGENTS.md", "ARCHITECTURE.md", "README.md"]
.map((f) => join(ROOT, f))
.filter((p) => safeStat(p)),
];
const codeFiles = [
...walk(join(ROOT, "apps"), (p) => /\.(ts|tsx)$/.test(p)),
...walk(join(ROOT, "packages"), (p) => /\.(ts|tsx)$/.test(p)),
...walk(join(ROOT, "src"), (p) => /\.(ts|tsx|js|jsx)$/.test(p)), // non-monorepo
];
// ---------- anchor slugifier (GitHub-compatible) ----------
function slugify(h) {
return h.toLowerCase().trim()
.replace(/[`*~]/g, "")
.replace(/[^\w\s-]/g, "")
.replace(/\s/g, "-");
}
const anchorCache = new Map();
function anchorsFor(f) {
if (anchorCache.has(f)) return anchorCache.get(f);
const src = safeRead(f);
const set = new Set();
if (src) for (const m of src.matchAll(/^#{1,6}\s+(.+?)\s*$/gm)) set.add(slugify(m[1]));
anchorCache.set(f, set);
return set;
}
// ---------- link scanner ----------
const MD_LINK = /(?<!\!)\[[^\]]+\]\(([^)]+)\)/g;
const CODE_REF = /docs\/[A-Za-z0-9_\-./]+\.md(?:#[A-Za-z0-9_\-]+)?/g;
function check(source, target) {
const [pathPart, anchor] = target.split("#");
if (!pathPart) {
const set = anchorsFor(source);
if (anchor && !set.has(anchor)) errors.push(`${rel(source)}: missing in-page anchor #${anchor}`);
return;
}
if (/^[a-z]+:/i.test(pathPart)) return;
if (pathPart.startsWith("/")) return;
const resolved = resolve(dirname(source), pathPart);
const stat = safeStat(resolved);
if (!stat) { errors.push(`${rel(source)}: broken link → ${target}`); return; }
if (stat.isDirectory()) return;
if (!resolved.endsWith(".md")) return;
if (anchor && !anchorsFor(resolved).has(anchor)) errors.push(`${rel(source)}: missing anchor #${anchor} in ${rel(resolved)}`);
}
for (const md of mdFiles) {
const src = safeRead(md);
if (!src) continue;
const stripped = src.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]+`/g, "");
for (const m of stripped.matchAll(MD_LINK)) check(md, m[1]);
}
for (const code of codeFiles) {
const src = safeRead(code);
if (!src) continue;
const comments = [...(src.match(/\/\/[^\n]*/g) ?? []), ...(src.match(/\/\*[\s\S]*?\*\//g) ?? [])].join("\n");
for (const m of comments.matchAll(CODE_REF)) {
const target = m[0];
const [pathPart, anchor] = target.split("#");
const resolved = resolve(ROOT, pathPart);
const stat = safeStat(resolved);
if (!stat) { errors.push(`${rel(code)}: broken link → ${target}`); continue; }
if (anchor && !anchorsFor(resolved).has(anchor)) errors.push(`${rel(code)}: missing anchor #${anchor} in ${rel(resolved)}`);
}
}
// ---------- doc↔code round-trip ----------
const designDocs = mdFiles.filter((f) => f.includes("/docs/design-docs/"));
for (const doc of designDocs) {
const src = safeRead(doc);
if (!src) continue;
const sections = src.split(/^#{2,3}\s+/m);
for (const section of sections) {
const firstLine = section.split("\n", 1)[0];
if (!/enforcement/i.test(firstLine)) continue;
const rows = section.split("\n").filter((l) => l.startsWith("|"));
if (rows.length < 2) continue;
for (const row of rows.slice(2)) {
const m = row.match(/`((?:apps|packages|src)\/[^`]+)`/);
if (!m) continue;
const codePath = m[1].replace(/[:\s].*$/, "");
const full = resolve(ROOT, codePath);
if (!safeStat(full)) { errors.push(`${rel(doc)}: Enforcement row points at missing file: ${codePath}`); continue; }
const body = safeRead(full);
const docBasename = doc.split("/").pop();
if (body && !body.includes(docBasename)) errors.push(`${rel(doc)}: ${codePath} has no back-pointer comment (grep \`${docBasename}\`)`);
}
}
}
// ---------- file size + frontmatter + staleness ----------
const STATUS_ENUM = new Set(["draft", "active", "deprecated", "living"]);
const STALE_DAYS = 180;
const seenInode = new Set();
function firstSeen(p) {
const s = safeStat(p); if (!s) return false;
const key = `${s.dev}:${s.ino}`;
if (seenInode.has(key)) return false;
seenInode.add(key); return true;
}
function lineCount(p) { const s = safeRead(p); return s ? s.split("\n").length : 0; }
const skillFiles = mdFiles.filter((f) => /\/skills\/[^/]+\/SKILL\.md$/.test(f) && firstSeen(f));
for (const f of skillFiles) {
const n = lineCount(f);
if (n > 500) warnings.push(`${rel(f)}: ${n} lines over skill hard ceiling (500) — split via references/`);
else if (n > 250) warnings.push(`${rel(f)}: ${n} lines over skill target (~150, push-to-split at 250)`);
}
const specDocs = mdFiles.filter((f) => /\/docs\/(design-docs|product-specs|guides)\/(?!workflows\/)/.test(f) && firstSeen(f));
for (const f of specDocs) {
const n = lineCount(f);
if (n > 800) warnings.push(`${rel(f)}: ${n} lines over doc hard ceiling (800) — mandatory split`);
else if (n > 500) warnings.push(`${rel(f)}: ${n} lines over doc target (~300, push-to-split at 500)`);
const src = safeRead(f); if (!src) continue;
const fmMatch = src.match(/^---\n([\s\S]*?)\n---/);
if (!fmMatch) { warnings.push(`${rel(f)}: missing YAML frontmatter`); continue; }
const statusMatch = fmMatch[1].match(/^status:\s*(\S+)/m);
if (!statusMatch) warnings.push(`${rel(f)}: frontmatter missing "status" field`);
else if (!STATUS_ENUM.has(statusMatch[1])) errors.push(`${rel(f)}: invalid status "${statusMatch[1]}"`);
const lvMatch = fmMatch[1].match(/^last_verified:\s*(\d{4}-\d{2}-\d{2})/m);
if (lvMatch) {
const ageDays = Math.floor((Date.now() - Date.parse(lvMatch[1])) / 86400000);
if (ageDays > STALE_DAYS) warnings.push(`${rel(f)}: last_verified ${lvMatch[1]} is ${ageDays} days old`);
}
const basename = f.split("/").pop();
if (basename !== "index.md") {
const dir = f.slice(0, -(basename.length + 1));
if (safeStat(join(dir, "index.md"))) {
const head = src.slice(fmMatch[0].length).split("\n").slice(0, 12).join("\n");
if (!/\*\*Part of:\*\*/.test(head)) warnings.push(`${rel(f)}: sub-file of ${rel(dir)}/ missing "**Part of:**" pointer`);
}
}
}
if (warnings.length) {
console.warn(`docs/link check: ${warnings.length} warning${warnings.length === 1 ? "" : "s"}`);
for (const w of warnings) console.warn(` WARN ${w}`);
}
if (errors.length) {
console.error(`docs/link check: ${errors.length} error${errors.length === 1 ? "" : "s"}`);
for (const e of errors) console.error(` ${e}`);
process.exit(1);
}
console.log(`docs/link check: OK (${mdFiles.length} md files, ${codeFiles.length} code files scanned)`);
Maintenance punch list. Separate from docs:check.
#!/usr/bin/env node
// Doc-gardening — scans in-scope docs for drift against code. Output is a
// maintenance punch list grouped by owner. Read-only; exits 0 regardless.
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { execFileSync } from "node:child_process";
const ROOT = resolve(process.argv[2] ?? ".");
const STALE_DAYS = 180;
function safeStat(p) { try { return statSync(p); } catch { return null; } }
function safeRead(p) { try { return readFileSync(p, "utf8"); } catch { return null; } }
function walk(dir, predicate, out = []) {
const s = safeStat(dir); if (!s?.isDirectory()) return out;
for (const entry of readdirSync(dir)) {
if (["node_modules", ".next", "dist", ".turbo", ".playwright-cli"].includes(entry)) continue;
const full = join(dir, entry);
const st = safeStat(full); if (!st) continue;
if (st.isDirectory()) walk(full, predicate, out);
else if (predicate(full)) out.push(full);
}
return out;
}
const inScopeDocs = [
...walk(join(ROOT, "docs/design-docs"), (p) => p.endsWith(".md")),
...walk(join(ROOT, "docs/product-specs"), (p) => p.endsWith(".md")),
...walk(join(ROOT, "docs/guides"), (p) => p.endsWith(".md") && !p.includes("/workflows/")),
];
const rel = (p) => relative(ROOT, p);
function parseFrontmatter(src) {
const m = src?.match(/^---\n([\s\S]*?)\n---/);
if (!m) return null;
const fm = {};
for (const line of m[1].split("\n")) {
const kv = line.match(/^(\w+):\s*(.+?)\s*$/);
if (kv) fm[kv[1]] = kv[2];
}
return { fm, bodyOffset: m[0].length };
}
const findings = [];
const add = (type, doc, owner, detail) => findings.push({ type, doc: rel(doc), owner: owner || "(unowned)", detail });
// Drift 1: code-path drift
const CODE_PATH_RE = /`((?:apps|packages|scripts|external|src|\.claude|\.agents)\/[\w\-./]+?)`/g;
function isLiteral(p) {
if (/[<>*?{}]/.test(p)) return false;
if (/\/\.\.\.|…/.test(p)) return false;
return true;
}
for (const doc of inScopeDocs) {
const src = safeRead(doc); if (!src) continue;
const owner = parseFrontmatter(src)?.fm.owner;
const prose = src.replace(/```[\s\S]*?```/g, "");
const seen = new Set();
for (const m of prose.matchAll(CODE_PATH_RE)) {
let path = m[1]; if (!isLiteral(path)) continue;
path = path.replace(/:\d+(?:[-:,]\d+)*$/, "").replace(/#.*$/, "");
if (seen.has(path)) continue; seen.add(path);
if (!existsSync(resolve(ROOT, path))) add("path-drift", doc, owner, `missing path: ${path}`);
}
}
// Drift 2: symbol drift in Enforcement tables
function hasSymbol(symbol) {
try {
execFileSync("grep", ["-r", "-l", "-w", "--include=*.ts", "--include=*.tsx", "--include=*.js", symbol, "apps", "packages", "src"], {
cwd: ROOT, stdio: ["ignore", "pipe", "ignore"], timeout: 10_000,
});
return true;
} catch (e) { return e.status === 1 ? false : true; }
}
for (const doc of inScopeDocs) {
const src = safeRead(doc); if (!src) continue;
const owner = parseFrontmatter(src)?.fm.owner;
const sections = src.split(/^#{2,3}\s+/m);
for (const section of sections) {
if (!/enforcement/i.test(section.split("\n", 1)[0])) continue;
const rows = section.split("\n").filter((l) => l.startsWith("|"));
if (rows.length < 2) continue;
for (const row of rows.slice(2)) {
const m = row.match(/`(?:apps|packages|src)\/[^`]+`\s*[—-]\s*`([A-Za-z_][\w$]*)`/);
if (!m) continue;
if (!hasSymbol(m[1])) add("symbol-drift", doc, owner, `zero hits for \`${m[1]}\` in code`);
}
}
}
// Drift 3: staleness elevation
const now = Date.now();
for (const doc of inScopeDocs) {
const parsed = parseFrontmatter(safeRead(doc)); if (!parsed) continue;
const lv = parsed.fm.last_verified;
if (!lv || !/^\d{4}-\d{2}-\d{2}$/.test(lv)) continue;
const ageDays = Math.floor((now - Date.parse(lv)) / 86_400_000);
if (ageDays > STALE_DAYS) add("staleness", doc, parsed.fm.owner, `last_verified ${lv} (${ageDays} days old)`);
}
if (!findings.length) { console.log(`docs:garden — clean. Scanned ${inScopeDocs.length} docs.`); process.exit(0); }
const byOwner = new Map();
for (const f of findings) { if (!byOwner.has(f.owner)) byOwner.set(f.owner, []); byOwner.get(f.owner).push(f); }
console.log(`docs:garden — ${findings.length} findings across ${byOwner.size} owners\n`);
for (const owner of [...byOwner.keys()].sort((a, b) => byOwner.get(b).length - byOwner.get(a).length)) {
const rows = byOwner.get(owner);
console.log(`=== ${owner} — ${rows.length} items ===`);
const byDoc = new Map();
for (const r of rows) { if (!byDoc.has(r.doc)) byDoc.set(r.doc, []); byDoc.get(r.doc).push(r); }
for (const [doc, docRows] of byDoc) {
console.log(` ${doc}`);
for (const r of docRows) console.log(` [${r.type}] ${r.detail}`);
}
console.log();
}
process.exit(0);Add under scripts:
"docs:check": "node scripts/check-docs-links.mjs",
"docs:garden": "node scripts/garden-docs.mjs"If .github/workflows/ exists, add a docs-check.yml:
name: docs
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: node scripts/check-docs-links.mjsIf not, leave it manual. Users run pnpm docs:check before committing, husky pre-commit hook optional.
# {{YYYY-MM-DD}} — {{Title}}
**Created:** {{YYYY-MM-DD}} (matches filename prefix; never update)
**Branch:** {{branch-name}}
**Epic:** {{epic name or standalone}}
**Status:** PLANNED | IN_PROGRESS | DONE | BLOCKED
## Objective
{{One-paragraph description of what this task delivers.}}
## Context
{{Background: why this work exists, links to design, related decisions.}}
## Steps
- [ ] {{Step 1}}
- [ ] {{Step 2}}
## Files Changed
| File | Change |
|------|--------|
| | |
## Errors & Issues
| Issue | Description | Resolution |
|-------|-------------|------------|
| | | |
## Outcome
{{Filled when status = DONE. What shipped, what was skipped, follow-ups.}}# Task Index
Active tasks sit flat in this folder. DONE tasks move to [archive/](archive/).
## Active
| ID | Title | Status | Branch | Date |
|----|-------|--------|--------|------|
## Archive
| ID | Title | Status | Branch | Date |
|----|-------|--------|--------|------|ID = date prefix, not a counter. Use YYYY-MM-DD-<slug>.md. Legacy T-NNN- from older projects is discouraged — it requires centralized counter coordination; the date prefix is collision-free.
When instructing an AI to create a task MD, explicitly say: "Follow .template.md. Filename prefix must be today's date in YYYY-MM-DD format."
Create docs/design-docs/core-beliefs.md — encodes the philosophy from §0. Shorter version that still does the job:
---
title: Core Beliefs — agent-first operating principles
status: active
last_verified: {{ YYYY-MM-DD }}
owner: {{ primary-author-handle }}
---
# Core Beliefs
**Part of:** [index.md](index.md)
The repository is the system of record. Humans and agents share it. Everything here exists to make both productive without one starving the other of context.
## 1. Map, not manual
`CLAUDE.md` is a table of contents. Every-turn gates (coding rules, skill loading) stay here; the rest lives in docs/ and skills/.
## 2. Progressive disclosure
Agents start with a small, stable entry point and learn where to look next. Don't preload everything.
### 2a. File-size targets
| Surface | Target | Push to split | Hard ceiling |
|---|---|---|---|
| Skill `SKILL.md` | ~150L | ~250 → move detail into `references/<topic>.md` | 500 |
| Design / spec doc | ~300L | ~500 → split into sibling files + index | ~800 (mandatory split) |
Targets, not fences. The trigger is: when editing feels scary, or the agent can no longer load it alongside the task context, split.
## 3. Execution plans are first-class
Non-trivial work starts with a task MD (`docs/exec-plans/tasks/YYYY-MM-DD-*.md`) before code is written. Versioned, checkpointable, survives context resets.
## 4. Skills encode conventions, docs encode knowledge
| Content | Home | Why |
|---|---|---|
| Package conventions ("how we name things here") | Skill | Auto-loads on keyword. |
| Feature contracts, domain glossary | `docs/product-specs/` | Stable reference. |
| Decisions, invariants, policy | `docs/design-docs/` | Prescriptive, read when needed. |
| DB schema, route catalog | `docs/generated/` | Auto-built. Never hand-edited. |
Duplicating across homes is worse than a broken link — broken links fail loudly, duplicates rot silently.
## 5. Memory is not docs
Agent memory is user-specific, cross-conversation state: preferences, feedback, ongoing context. Not project knowledge. Rule: if a new team member could reconstruct the fact from the repo, it doesn't belong in memory.
## 6. Documentation verification is mechanical
`pnpm docs:check` gates CI. `pnpm docs:garden` produces the maintenance punch list. If a rule can't be mechanically verified, assume it will rot.
## 7. Prefer code over prose
Prose describes a moment. Code is authoritative. Every doc either links to the code it describes, or explains a *decision* that code alone can't show.
### 7a. Cross-cutting invariants get bidirectional links
When a rule lives in multiple code sites, one doc owns it with an Enforcement table listing every site; each site carries a one-line comment pointing back. `docs:check` verifies the round-trip.
## 8. The repo self-heals
- **Fix what you touch.** Dead imports, stale comments, misleading names.
- **Fix what's adjacent.** But don't widen scope much.
- **No big refactors.** 100 small diffs, not one architectural overhaul.
- **Log slop you can't fix now** as a task MD. Don't pretend it isn't there.
## 9. Subagent policy
Default to main session — Read, Grep, Glob, Edit. Spawn subagents only when user explicitly asks, or task is fully independent (e.g. background test runs). Token efficiency is the scarce resource.
## 10. External systems with agent docs
When you pull in a subtree or sibling repo that has its own `CLAUDE.md`/`.claude/skills/`, read its entry point first for anything in that domain. Note staleness if the subtree is manually synced.And docs/design-docs/skill-system.md — short:
---
title: Skill system
status: active
last_verified: {{ YYYY-MM-DD }}
owner: {{ primary-author-handle }}
---
# Skill system
**Part of:** [index.md](index.md)
## Layout
.claude/skills// ├── SKILL.md — entry point; description + trigger keywords in frontmatter └── references/ — detail extracted when SKILL.md exceeds ~250L └── .md
## SKILL.md frontmatter
```yaml
---
name: <skill-name>
description: <one-liner explaining what the skill does>. Trigger keywords - <comma-separated list>.
---
Claude auto-loads a skill when the user's message contains a trigger keyword.
- One skill per package or domain (
backend,frontend,infra, …). - SKILL.md is the "Quick Reference" — tables, paths, the 80% case. Push detail into
references/<topic>.md. - Max ~150L target, ~250L push-to-split, 500L hard ceiling.
- References are link-target-only: SKILL.md points at them; they never link back up (keeps the skill cheap to load).
- Frontmatter
descriptionis load-bearing — it's the selection signal. Start with "when to use this skill," list trigger keywords at the end.
- When you learn something new about a package, add it to the skill.
- When you see a skill contradicting reality, fix the skill.
- Large changes → split to
references/.
---
## Phase F — Skills directory
### F.1 Directory bootstrap
.claude/skills/ ├── {{ skill-1 }}/ │ ├── SKILL.md │ └── references/ └── {{ skill-2 }}/ ├── SKILL.md └── references/
**Which skills to create?** One per major domain. For the project described by the user, identify 2–5 natural seams. Typical examples:
- `backend` — API conventions, routing, ORM patterns
- `frontend` — UI, styling, state management
- `infra` — AWS, DB, logs, deploy
- `domain` — business entities, workflows, glossary
- `testing` — test runners, coverage, fixtures
Ask the user before creating skills for domains they haven't mentioned. Start minimal — 2 skills is fine; skills are cheap to add later but expensive to get wrong upfront.
### F.2 SKILL.md template
```markdown
---
name: {{ skill-name }}
description: {{ one-line purpose }}. Trigger keywords - {{ comma-separated }}.
---
# {{ Skill Title }}
## Quick Reference
| Question | Answer |
|---|---|
| {{ Q1 }} | {{ A1 }} |
| {{ Q2 }} | {{ A2 }} |
## Core Patterns
### {{ Pattern 1 }}
{{ example code + explanation }}
### {{ Pattern 2 }}
{{ example code + explanation }}
## References
| Topic | File |
|---|---|
| {{ deeper topic }} | [references/{{ topic }}.md](references/{{ topic }}.md) |
If the repo will be used by tools other than Claude Code (e.g. Codex, Cursor) that look for AGENTS.md, add a symlink:
ln -s CLAUDE.md AGENTS.mdBefore declaring done, replace these placeholders:
| Placeholder | What to replace |
|---|---|
{{ project-name }} |
Project / repo name |
{{ primary-author-handle }} |
GitHub username of the person who knows the project best |
{{ YYYY-MM-DD }} |
Today's date, ISO format |
{{ branch-name }} (in task templates) |
leave — user fills when creating a task |
Skills table in CLAUDE.md |
Replace with real skills after Phase F |
ARCHITECTURE.md body |
Replace with actual domains + runtime topology |
docs/product-specs/index.md "Contents" |
Add first real product specs as they come |
Run in order:
pnpm install # if you added node deps or new package.json
pnpm docs:check # must exit 0 (warnings allowed)
pnpm docs:garden # should find very little on day 1Expected docs:check output:
docs/link check: OK (<N> md files, <M> code files scanned)
If warnings appear, they're probably:
- Missing frontmatter on existing docs → add
- Status uses
shipped/accepted→ normalize toactive - Sub-files missing
**Part of:**→ add
If errors appear, they're real — fix before commit.
| Symptom | Root cause | Fix |
|---|---|---|
| "Agent ignored our convention" | Convention lived in CLAUDE.md buried under 500 lines; agent didn't see it. | Move to a skill; CLAUDE.md just points. |
| "Docs drifted silently for weeks" | No mechanical verification; humans forgot to update. | pnpm docs:check in pre-commit + CI; weekly pnpm docs:garden review. |
| "Two docs say different things" | Duplicated content across design-docs + product-specs. | One owns, the other links. §7a Enforcement-table for multi-site rules. |
| "Task MD filenames collide on merge" | Using T-NNN- counter; two engineers bumped to same number. |
Switch to YYYY-MM-DD-slug.md. Collision-free. |
| "CLAUDE.md won't stay short" | Tendency to paste policy there. | Policy goes in design-docs/. CLAUDE.md is a map. |
| "Skill files grow past 250L" | Detail accretes over time. | Extract to references/<topic>.md. Pattern is intentional; use it. |
- Test infrastructure. Add
vitest/playwright/ whatever your stack uses; this setup is agnostic to testing. - CI platform.
docs:checkruns as a pnpm script. Wire to GitHub Actions / Vercel / whatever when ready. - Automated PR opening. v2 of doc-gardening deferred; needs a scheduler (GHA cron, etc.).
- Style/lint for code. Separate concern. Add ESLint/Prettier/Biome per stack; they don't interact with this doc infrastructure.
- Monorepo setup. Assumes it already exists if multi-package. Paths in scripts accommodate both
apps/+packages/(turborepo) andsrc/(single-app).
Based on patterns from OpenAI's Harness engineering: leveraging Codex in an agent-first world blog and iterated on in production repos. Not all of it is in that blog; parts (doc-format convention, date-filename rule, doc-gardening drift types) came from practice.