diff --git a/.claude/skills/commit.md b/.claude/skills/commit/SKILL.md similarity index 100% rename from .claude/skills/commit.md rename to .claude/skills/commit/SKILL.md diff --git a/.claude/skills/improve-skill/SKILL.md b/.claude/skills/improve-skill/SKILL.md new file mode 100644 index 0000000..7404d84 --- /dev/null +++ b/.claude/skills/improve-skill/SKILL.md @@ -0,0 +1,50 @@ +--- +name: improve-skill +description: Use when something went wrong in a session that a skill or rule should have prevented — a correction from the user, a broken build, a convention violation, a repeated mistake. Captures the failure as a permanent fix to the responsible skill, doc, or rule. +--- + +# Improve Skill + +## Overview + +The hardening loop. Context files (skills, docs, `AGENTS.md`) are treated like code: when +they fail, they get a fix, not a workaround. Every correction the user has to make twice is +a bug in the harness, and this skill patches the harness. Over time skills converge on +bulletproof, and the always-loaded context stays small because knowledge accretes in the +right on-demand file instead of in an ever-growing CLAUDE.md. + +## When invoked + +Mid-session, right after the failure — while the exact wording of what went wrong is still +in context. Typical triggers: the user corrects the same thing again, a hook or CI rejects +work, a skill produced output that violated a repo convention, a doc turned out to be wrong +or stale. + +## Workflow + +1. **Name the failure precisely.** One sentence: what was expected, what happened instead. + If it can't be stated in one sentence, it isn't understood yet. +2. **Find the owner.** Which file *should* have prevented this? + - A task went wrong → the skill for that task. + - A convention was violated → the doc that states it (or should state it). + - It must never happen regardless of task → `AGENTS.md` (always-loaded, so the bar is + high) or better, a deterministic guardrail (git hook, lint rule, CI check). + - No owner exists → propose the smallest new home; a new skill needs a recurring task, + not a one-off. +3. **Write the smallest rule that would have prevented it.** One or two lines in the + owner's body. Prefer tightening an existing step over adding a new section. Never grow a + skill's `description` for this — descriptions are always in context and stay one line. +4. **Log it.** Append to the owner skill's `## Lessons` section (create it on first + lesson): `- YYYY-MM-DD — `. This keeps the audit trail of *why* + each rule exists. +5. **Confirm with the user** before writing — show the exact edit. If the same lesson is + being added a second time, the rule is wrong or unclear; rewrite it instead of + restating it. + +## Do not + +- Patch the symptom in the session but skip the harness fix — that's how the same failure + returns next week. +- Add generic advice a model already knows; rules must be project-specific and falsifiable. +- Grow `AGENTS.md` when an on-demand skill or doc can own the rule. +- Turn a one-off mishap into a rule; twice is a pattern, once is noise (log nothing). diff --git a/.claude/skills/pr-description.md b/.claude/skills/pr-description/SKILL.md similarity index 100% rename from .claude/skills/pr-description.md rename to .claude/skills/pr-description/SKILL.md diff --git a/.claude/skills/safe-rollout.md b/.claude/skills/safe-rollout/SKILL.md similarity index 100% rename from .claude/skills/safe-rollout.md rename to .claude/skills/safe-rollout/SKILL.md diff --git a/.claude/skills/scaffold-module.md b/.claude/skills/scaffold-module/SKILL.md similarity index 63% rename from .claude/skills/scaffold-module.md rename to .claude/skills/scaffold-module/SKILL.md index 8e7bc09..606310d 100644 --- a/.claude/skills/scaffold-module.md +++ b/.claude/skills/scaffold-module/SKILL.md @@ -9,8 +9,9 @@ description: Use when adding a new self-contained module under src/ — "scaffol A module here is a folder under `src/` with a single clear purpose, a typed surface, and a colocated test. This skill scaffolds that trio consistently so a new module matches the two -existing worked examples ([`src/feature-flags/`](../../src/feature-flags), -[`src/http/`](../../src/http)) instead of inventing its own shape. +existing worked examples ([`src/feature-flags/`](../../../src/feature-flags), +[`src/http/`](../../../src/http)) instead of inventing its own shape. Starting points for +the three files live in [`templates/`](templates) — adapt them, don't copy blindly. ## Input @@ -35,3 +36,18 @@ existing worked examples ([`src/feature-flags/`](../../src/feature-flags), - Create a module without a test — the test is the guardrail, not an afterthought. - Add a dependency to scaffold a module; the toolchain stays minimal. - Reach for a class when a function and plain data will do (KISS). + +## Lessons + +Failures this skill has absorbed (see the `improve-skill` skill for how entries get here; +full stories in [docs/CASE_STUDY.md](../../../docs/CASE_STUDY.md)): + +- 2026-06-19 — Fake-timer test left a rejection unhandled: `await advanceTimersByTimeAsync()` + ran before a handler was attached to the promise under test → attach the handler first, + then race both with `Promise.all([...])`. +- 2026-06-19 — `toMatchObject()` silently skipped `Error.cause` (non-enumerable), so the + test passed without checking what it claimed → assert `cause` and other non-enumerable + properties directly. +- 2026-06-19 — Node-context tooling scripts (`.husky/`, `.claude/workflows/`) tripped + `no-undef` under the app's ESLint config → harness scripts stay scoped out of the app + lint config; don't "fix" them by weakening `src/` rules. diff --git a/.claude/skills/scaffold-module/templates/module.test.ts.template b/.claude/skills/scaffold-module/templates/module.test.ts.template new file mode 100644 index 0000000..a2fac00 --- /dev/null +++ b/.claude/skills/scaffold-module/templates/module.test.ts.template @@ -0,0 +1,18 @@ +import { describe, expect, it, vi } from "vitest"; + +import { } from "./"; + +// Sync/pure module: assert inputs -> outputs directly. +// Async/I-O module: inject a fake and use vi.useFakeTimers(). +// +// Fake-timer + rejection pitfalls (see Lessons in SKILL.md): +// - Attach the rejection handler BEFORE advancing time: +// await Promise.all([expect(promise).rejects.toThrow(...), vi.advanceTimersByTimeAsync(ms)]); +// - Error.cause is non-enumerable: toMatchObject() silently skips it. +// Assert it directly (try/catch or `.cause` property check). + +describe("", () => { + it("does what the spec says", () => { + expect(({})).toEqual(/* ... */); + }); +}); diff --git a/.claude/skills/scaffold-module/templates/module.ts.template b/.claude/skills/scaffold-module/templates/module.ts.template new file mode 100644 index 0000000..fec4da6 --- /dev/null +++ b/.claude/skills/scaffold-module/templates/module.ts.template @@ -0,0 +1,8 @@ +import type { Options, Result } from "./types"; + +// Pure where possible; inject I/O boundaries (fetch, clock, storage) as +// parameters so the module is testable without touching globals. + +export function (options: Options): Result { + // ... +} diff --git a/.claude/skills/scaffold-module/templates/types.ts.template b/.claude/skills/scaffold-module/templates/types.ts.template new file mode 100644 index 0000000..3a3c8e1 --- /dev/null +++ b/.claude/skills/scaffold-module/templates/types.ts.template @@ -0,0 +1,17 @@ +// Public types for : options, results, typed errors. +// Make illegal states unrepresentable — prefer unions over booleans. + +export type Options = { + // ... +}; + +export type Result = { + // ... +}; + +export class Error extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "Error"; + } +} diff --git a/AGENTS.md b/AGENTS.md index f2405be..3e01759 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,4 +45,4 @@ For agentic work, use the Superpowers sub-skills (`superpowers:executing-plans`, [docs/GIT_HOOKS.md](docs/GIT_HOOKS.md). - Run lint + typecheck before staging. - Never commit automatically — only on explicit user authorization. See - [.claude/skills/commit.md](.claude/skills/commit.md). + [.claude/skills/commit/SKILL.md](.claude/skills/commit/SKILL.md). diff --git a/CLAUDE.md b/CLAUDE.md index c9e5c68..444f6a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,29 +1,8 @@ # CLAUDE.md — Project Context for Claude Code -## Source of truth - -All AI agent rules, conventions and standards live in [AGENTS.md](AGENTS.md). -**Read it first** — it covers the tech stack, principles (DRY/KISS/SOLID with -judgment), the spec-driven workflow, and commit/PR rules. This file intentionally -stays thin to avoid duplicating that source of truth. - -## Auto-loaded context - -A `SessionStart` hook ([.claude/hooks/session-start.sh](.claude/hooks/session-start.sh)) -primes branch and workflow context at the start of every session. - -## Commands, skills, subagents & workflows - -- **Commands** ([.claude/commands/](.claude/commands)): `new-spec` — scaffold a spec + plan pair. -- **Skills** ([.claude/skills/](.claude/skills)): `commit`, `pr-description`, `safe-rollout`, - `scaffold-module` (scaffold a new `src/` module). -- **Subagents** ([.claude/agents/](.claude/agents)): `write-test`, `review-standards` — dispatch - with the `Agent` tool; run independent work in parallel. -- **Workflows** ([.claude/workflows/](.claude/workflows)): `parallel-review` (multi-agent review - + skeptic), `generate-tests` (parallel test backfill). - -## Workflow - -Brainstorm → spec → implementation plan → implement, on the -[Superpowers](https://github.com/obra/superpowers) workflow. Specs and plans live -in [specs/](specs). Layout: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +All rules, conventions and standards live in [AGENTS.md](AGENTS.md) — read it first; it +indexes the docs to load on demand. This file stays minimal on purpose: skills, subagents +and workflows under [.claude/](.claude) announce themselves through their own descriptions, +and a `SessionStart` hook ([.claude/hooks/session-start.sh](.claude/hooks/session-start.sh)) +primes branch + workflow context. When a session goes wrong in a way a skill should have +prevented, run the `improve-skill` skill — context files are code and failures get fixes. diff --git a/README.md b/README.md index c81dd8c..a950bf4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The biggest lever for good AI output is the context you give it, not the prompt. - **[`CLAUDE.md`](CLAUDE.md)** — the Claude Code entry point. Deliberately thin: it redirects to `AGENTS.md` so the rules live in one place instead of being duplicated per tool. A `SessionStart` hook in [`.claude/`](.claude) primes branch and workflow context automatically. - **[`docs/`](docs)** — the standards `AGENTS.md` points to (architecture, git hooks, development setup, React guidelines). - **[`.claude/commands/`](.claude/commands)** — slash commands for repeatable ops; `new-spec` scaffolds a spec + implementation-plan pair from the house templates. -- **[`.claude/skills/`](.claude/skills)** — reusable, plain-English skills for recurring tasks (commit, PR description, safe rollout, scaffold a module) with the safety rules baked in. +- **[`.claude/skills/`](.claude/skills)** — reusable, plain-English skills for recurring tasks (commit, PR description, safe rollout, scaffold a module) with the safety rules baked in. Each skill is a folder (`/SKILL.md` plus colocated resources, e.g. `scaffold-module/templates/`), and skills carry a `## Lessons` changelog of the failures they've absorbed — see below. - **[`.claude/agents/`](.claude/agents)** — focused subagents (a test writer, a standards reviewer) that run independently and in parallel. - **[`.claude/workflows/`](.claude/workflows)** — `parallel-review`: a runnable, multi-agent orchestration script — three reviewers (correctness, security, @@ -46,6 +46,27 @@ The biggest lever for good AI output is the context you give it, not the prompt. message or branch name doesn't conform, `post-checkout` warns on a bad branch name, `pre-push` runs the test suite. +### Small context, self-hardening skills + +Two rules govern all of the above: + +**Keep the always-loaded context minimal.** `CLAUDE.md` is a handful of lines and +`AGENTS.md` stays under a page, because every word in them is paid for in every single +session. Models already know TypeScript and React; the context documents only what they +can't know — this project's conventions, decisions, and workflow. Everything else loads on +demand: docs through the index in `AGENTS.md`, skills through their own trigger +descriptions (a skill costs one description line until it's actually needed). + +**Treat context files like code: failures get fixes.** When a session goes wrong in a way +a skill or doc should have prevented, the +[`improve-skill`](.claude/skills/improve-skill/SKILL.md) skill patches the responsible +file with the smallest rule that would have prevented the failure, and logs it in that +skill's `## Lessons` section — date, failure, rule. Skills converge on bulletproof instead +of the same correction being repeated across sessions, and the audit trail shows *why* +every rule exists (see the Lessons in +[`scaffold-module`](.claude/skills/scaffold-module/SKILL.md), grown out of the bugs in the +[case study](docs/CASE_STUDY.md)). + ## Running the working example Two worked examples live in [`src/`](src), each taking the same spec → @@ -93,7 +114,8 @@ measured by what users feel, backed by tests, not by how clever the code looks. | [`docs/`](docs) | Standards referenced by `AGENTS.md` | | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | How the repo is laid out — worked examples, docs, and agent tooling | | [`.claude/commands/`](.claude/commands) | Slash commands — `new-spec` scaffolds a spec + plan pair | -| [`.claude/skills/`](.claude/skills) | Reusable task skills with safety rails (incl. `scaffold-module`) | +| [`.claude/skills/`](.claude/skills) | Reusable task skills with safety rails, each with a `## Lessons` changelog (incl. `scaffold-module` + templates) | +| [`.claude/skills/improve-skill/`](.claude/skills/improve-skill) | The hardening loop — session failures become permanent skill fixes | | [`.claude/agents/`](.claude/agents) | Independent, parallelizable subagents (`write-test`, `review-standards`) | | [`.claude/`](.claude) | Permissions + session-start hook | | [`.husky/`](.husky) | Real git hooks matching `docs/GIT_HOOKS.md` — not just documentation | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 14c5060..d8d257c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,7 +15,7 @@ of problem, so the workflow is shown on more than one shape of code: | [`src/http/`](../src/http) | async, real I/O boundary | a `fetchWithRetry` wrapper tested with an injected `fetch` + fake timers | A module is a folder with `types.ts`, the implementation, and a colocated `*.test.ts`. New -modules follow the same shape — see the [`scaffold-module`](../.claude/skills/scaffold-module.md) skill. +modules follow the same shape — see the [`scaffold-module`](../.claude/skills/scaffold-module/SKILL.md) skill. ## Context, in version control @@ -30,7 +30,7 @@ The rules an agent needs are files, not tribal knowledge: | Path | Kind | Runs | Example | |------|------|------|---------| | [`commands/`](../.claude/commands) | slash command | on demand, in your session | `new-spec` scaffolds a spec + plan pair | -| [`skills/`](../.claude/skills) | reusable skill | when its trigger matches | `commit`, `pr-description`, `safe-rollout`, `scaffold-module` | +| [`skills/`](../.claude/skills) | reusable skill | when its trigger matches | `commit`, `pr-description`, `safe-rollout`, `scaffold-module`, `improve-skill` | | [`agents/`](../.claude/agents) | subagent | dispatched via the `Agent` tool, in parallel | `write-test`, `review-standards` | | [`workflows/`](../.claude/workflows) | orchestration script | as a multi-agent run | `parallel-review`, `generate-tests` | | `settings.json` | config | always | scoped permission allowlist + session-start hook | @@ -41,6 +41,11 @@ The rules an agent needs are files, not tribal knowledge: its own tool permissions). A *workflow* is a script that orchestrates several subagents across phases — `parallel-review` and `generate-tests` are the two worked examples. +Each skill is a folder — `SKILL.md` plus colocated resources (e.g. +`scaffold-module/templates/`). Skills carry a `## Lessons` changelog; the `improve-skill` +skill is the loop that appends to it: a session failure becomes the smallest permanent rule +that would have prevented it. + ## Guardrails `.husky/` git hooks and `.github/workflows/ci.yml` enforce the same four checks — `pnpm lint`, diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index be69977..f876673 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,27 +1,14 @@ # Development Setup & Tooling -## Quick reference - -- **Principles:** DRY/KISS/SOLID held with judgment; earn your abstractions. -- **Folder structure:** prefer folders over loose modules; keep a component's - children and tests local to it. -- **Code style:** function declarations, no `any`, prefer `type` over `interface`. -- **Git:** `{username}/{TICKET}-{name}` branches; squash-merge PRs. - -## EditorConfig - -`.editorconfig` keeps formatting consistent across editors: - -- UTF-8, LF line endings, final newline inserted, trailing whitespace trimmed. -- 2-space indentation (tabs in Makefiles). -- TypeScript/JavaScript: 2 spaces, double quotes. -- Markdown: no trailing-whitespace trim, no line-length limit. +Project-specific setup only; principles and workflow live in [AGENTS.md](../AGENTS.md). +Formatting rules are enforced by [.editorconfig](../.editorconfig) and Prettier — read +those files rather than a prose copy here. ## Tooling | Tool | Role | |------|------| -| pnpm | package manager (workspaces/monorepo) | +| pnpm | package manager — never npm/yarn (`packageManager` is pinned) | | ESLint + Prettier | linting and formatting | | TypeScript | type checking (`pnpm typecheck`) | | Vitest | unit/component tests | @@ -36,3 +23,8 @@ pnpm lint:fix # lint and auto-fix pnpm typecheck # type check pnpm test # run tests ``` + +## Code style (project choices) + +- Function declarations, no `any`, prefer `type` over `interface`. +- Git: `{username}/{TICKET}-{name}` branches; squash-merge PRs. diff --git a/docs/REACT_GUIDELINES.md b/docs/REACT_GUIDELINES.md index e55015d..151af23 100644 --- a/docs/REACT_GUIDELINES.md +++ b/docs/REACT_GUIDELINES.md @@ -1,14 +1,9 @@ # React Development Guidelines -Standards for React 19 / Next.js (App Router) work in this project. - -## Core philosophy - -**DRY, KISS and SOLID — held with judgment, not dogma.** I avoid premature -abstraction: a wrong abstraction costs more than a little duplication, so I let a -pattern prove itself before extracting it. I apply SOLID's *spirit* — single -responsibility, composition over inheritance — rather than its OOP-era ceremony. -The goal is clarity and changeability, not acronym compliance. +Standards for React 19 / Next.js (App Router) work in this project. This doc holds only +what a model can't infer: our conventions and the choices we've made. General React/Next.js +knowledge is deliberately absent — the model already has it. Principles (DRY/KISS/SOLID +with judgment) live in [AGENTS.md](../AGENTS.md), not here. ## Folder structure @@ -34,36 +29,14 @@ This keeps boundaries clear and makes search predictable. 4. Assets (images, SVGs, fonts) 5. Styles (Tailwind classes inline — no style imports) -## Component template - -```tsx -// 1. Library imports -import { useState } from "react"; - -// 2. Local imports -import { formatPrice } from "@/lib/format"; -import type { Product } from "./types"; - -// 3. Component — function declaration, typed props, no `any` -export default function ProductCard({ product }: { product: Product }) { - // derive during render; don't store computed values in state - const price = formatPrice(product.cents); - return
{price}
; -} -``` - -## Performance rules +## Project conventions +- Function declarations for components; typed props; no `any`. +- Derive state during render instead of syncing it in `useEffect`; don't store what you + can compute. +- No barrel imports — direct paths keep bundles lean. - `Promise.all()` for independent async work — never sequential `await`s. -- No barrel imports — import direct paths to keep bundles lean. - `next/dynamic` for heavy components not needed on first paint. -- With the React Compiler enabled, skip manual `useMemo`/`useCallback` unless - profiling proves a need. -- Derive state during render instead of syncing it in `useEffect`. - -## Server vs client (App Router) - -- Components render on the server by default; add `"use client"` only when you - need client hooks or browser APIs. -- Never touch `window`/`document`/`localStorage` during init — use `useEffect`. +- **React Compiler is enabled:** skip manual `useMemo`/`useCallback` unless profiling + proves a need. - Treat server actions as public endpoints: authenticate them.