Exercises
Warm-up
Clone exercise repository
Every exercise from here on runs in it.
- Clone lexler/ai-workshop-explore-ddd-2026 ↗.
- Start your agent in that folder.
git clone https://github.com/lexler/ai-workshop-explore-ddd-2026.git cd ai-workshop-explore-ddd-2026
git clone [email protected]:lexler/ai-workshop-explore-ddd-2026.git cd ai-workshop-explore-ddd-2026
Warm-up
“Please, draw me a sheep!”
- Copy the prompt and paste it into your coding agent.
Draw this in ascii: - Draw a triangle. - Draw a vertical line next to it. - Below the line draw a long square. - Put the rectangle on a long horizontal line.
1. Active Partner
Explain the goal
We're building a tiny console to-do app. Let's get us and the agent on the same page about it.
- Start a new chat in the exercise repository you cloned.
- Copy the prompt and paste it. It is the task plus a request to ask questions at the bottom of it. Take a look!
- Answer the questions.
- When the agent stops asking, go to the next step.
Task
I want to create a simple console TODO app for personal use. It should let me add a task, list my tasks, mark a task as done and delete a task. For this it uses subcommands like: - todo add "Buy milk" - todo list - todo done 3 - todo delete 3 Tasks must persist between runs of the program. The to-do list belongs to me, not to a particular project or directory, so running the command from different folders should show the same tasks. It should store the task list locally on the computer in a single file.
Request
Is anything unclear, what questions do you have? Ask one question at a time. We are not going to build anything in this session, we're just planning what we're going to build. Don't start implementation even when everything is clear.
1. Active Partner
Create project.md
- In the same chat, paste the prompt.
- Open
docs/project.mdand read it. - Commit all the changes.
Create a document describing what we are going to build and save it into a markdown file in docs/. Call it project.md. Start it with the high level succinct project overview, and then make sure it reflects all the decisions we made. Make sure it's well-structured. Don't build anything yet.
1. Active Partner
Grill me
Let's get a little taste of Matt Pocock's “Grill Me” skill.
- In the same chat, paste the prompt.
- Answer just a few questions and then go to the next step.
Interview me relentlessly about every aspect of this application until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. Ask the questions one at a time. If a question can be answered by exploring the codebase or existing documentation, explore the codebase instead. Don't update the document and don't start building it.
1. Active Partner
Reverse Direction
- In the same chat, paste the prompt.
- Answer a few more questions.
For each question, provide your recommended answer.
1. Active Partner
Grill with a form
Let's try the same grilling with an HTML form, using the skill in your repository: skills/grill-form/.
- In the same chat, paste the prompt.
- Answer a round or two in the form.
- Keep this chat open. Exercise 2 continues in it.
Continue the grilling session using grill-form skill in skills/grill-form/SKILL.md until everything is clear.
Having trouble? Use this prompt instead.
The agent builds the form itself, which takes longer.
# Grill Me with an HTML artifact Each grilling round arrives as an HTML form in the browser instead of a wall of chat questions. Every question has the agent's recommended answer preselected, so submitting the form untouched means "accept all your recommendations". Nothing to install: the agent writes a page per round and a small script that catches your answers and hands them straight back to it. # Prompt your coding agent: Interview me relentlessly about this plan until we reach a shared understanding. Map the work as a design tree: every decision branches into the decisions that hang off it. Work the tree in rounds. A round is the frontier: every decision whose prerequisites are already settled. Facts you can look up yourself in the codebase are your job; only the decisions are mine. Deliver each round as an HTML form, not as chat text. ## The form Write one self-contained file per round, `grill/round-N.html`. Write the HTML and JavaScript yourself, inline, no frameworks, no external assets. The page shows, in this order: - A title with the topic and the round number, and one line saying that recommended answers are preselected and an untouched submit accepts them all. - One card per question (3 to 8 per round). A card has: - the full question, ending with a question mark - optional one-paragraph context, and an optional inline diagram (SVG or HTML) when a picture argues better than prose - a horizontal row of option buttons with short labels; clicking one selects it and shows that option's one-line detail under the row. The recommended option is preselected and marked with ⭐ - a last button in the row, `💬 None — discuss`, which means "not deciding this now, let's talk". Style it differently so it reads as an escape hatch, not an option - a free-text box under the row for elaboration, nuance, or an answer in your own words - A fixed bar at the bottom with two buttons: `Save answers` (primary) and `Copy answers` (secondary). Both build the same plain-text summary: the round number and, per question, the question, the chosen label (or `discuss`), and the elaboration if any. Something like: ``` Round 2 answers Q1 Where do retries live? choice: In the payment service note: but keep the schedule configurable Q2 How many attempts? choice: discuss note: depends on what the bank allows, I don't know ``` ## Getting the answers back The answers belong next to the rounds, in `grill/`, where you can read them yourself. A page opened straight from disk cannot write there, so serve it instead. Write `grill/serve.py` once: python3, standard library only, nothing to install. It takes a round file, binds an ephemeral port, serves that one page, and waits for a `POST` of the summary. An ephemeral port means several rounds can be open at the same time, which matters once side branches appear. Start it in the background and tell me the URL. One round, one run. On the post it writes `grill/round-N-answers.txt`, answers the browser, and only then shuts itself down and exits. Answering first matters: a server that dies mid-reply shows me an error on a round that actually saved. Leave my browser tab alone — I close that myself — but never leave a server running. If a round is abandoned or reissued, kill its server too, and when the grilling ends make sure none are left behind. `Save answers` posts the summary to that script. On success the page replaces itself with `Saved to grill/round-N-answers.txt — back to the chat`, and says the round is closed so I know a second click will not reach anything. Watch for the file. Read it as soon as it appears and carry on; you do not need me to announce that I am done. `Copy answers` is the fallback, and it must work even when nothing is listening: it puts the same summary on the clipboard, and the page always renders that summary in a textarea too. If the post fails, say so on the page and point me at those two, so a dead port costs me a paste and never my answers. ## Between rounds Keep `grill/session.md` as the single record, and keep it scannable: topic and goal at the top, a `## Decisions` section with one entry per resolved question (the decision plus the why from my elaboration), a `## Open questions` list that always reflects reality, and, once grilling ends, a `## Plan` section with the sharpened plan. When my answers come in: record every one. My prose beats the button; if the elaboration overrules the clicked choice, the prose wins. A `discuss` answer is not a decision: the question stays open and moves to chat, seeded by my note. Small ones we just talk through and fold into the next round. If a discussed question is a premise for the other open questions, pause the forms and settle it in chat first, because a form built on an undecided premise goes stale. Then recompute the frontier and write the next round. A question whose answer depends on another question still open in this round belongs to a later round, not this one. If `grill/session.md` already exists when we start, this is a resume: read it, treat its decisions as settled, and continue from its open questions. ## Done when The frontier is empty: every branch decided and recorded in `grill/session.md`, the open-questions list empty, and the file ending with the sharpened plan. Do not act on the plan until I confirm we have reached a shared understanding.
2. Extract Knowledge
Extract knowledge
- Stay in the chat where you just finished the interview about the todo app.
- Paste the prompt.
- Open the document it created. Does it contain everything you discussed?
Create a document describing what we are going to build and save in into a markdown file. Make sure to have all decisions we made reflected. Suggest a good name for it and confirm with me.
Closed that chat? Start from a finished grilling instead.
Paste this into a new chat, then paste the prompt above.
Request
Here is the grilling session we had about my todo app. Take its answers as the decisions we made together and stand by.
File: grilling-transcript.md
# Grilling transcript: Personal console TODO app ## Round 1 Foundations first: what a task number means, what `list` shows, where and how tasks are stored, and what it's written in. Later rounds build on these. ### What does the number in `todo done 3` and `todo delete 3` refer to? This shapes the file format and what `list` prints. The risk to weigh: typing a number from memory, or running a command twice. - Stable ID (recommended, **chosen**): A task keeps the number it got when added. The same number always means the same task. Numbers grow and the list has gaps. - Position in list: Numbers are 1..N as shown right now. Small numbers, but they shift after every delete. - Stable, reuse freed: Stable, but a new task takes the lowest free number. Small numbers, but an old number can later mean a different task. Answer: Stable ID ### What does `todo list` do with tasks marked done? - Show, marked (recommended, **chosen**): All tasks, done ones shown with a mark like [x]. Simple, but the list grows forever unless you delete. - Hide done: Only open tasks. Done tasks stay in the file. A flag like `list --all` shows them. - Open first, done after: Open tasks on top, done tasks below in a separate group. Answer: Show, marked ### What language and runtime do we build it in? It's a personal tool on your Mac, so startup speed and easy install matter more than the ecosystem. - Python (recommended): Standard library only, run via a `todo` entry on PATH. Fast to write, approval tests available, needs Python on the machine. - Go: Single static binary, instant startup, easy to install anywhere. A bit more ceremony. - TypeScript/Node (**chosen**): Familiar for many, but Node startup and install are heavier for a tiny CLI. - Rust: Single fast binary, strongest types. Slowest to write for a tool this small. Answer: TypeScript/Node Comment: "use bun" ### Where does the task file live? It must be the same file no matter which folder you run `todo` from. - XDG data dir (recommended): `~/.local/share/todo/tasks.json` (honors $XDG_DATA_HOME). Follows the Unix convention, keeps home clean. - Dotfile in home (**chosen**): `~/.todo.json`. Easiest to find and back up, adds one more dotfile. - macOS App Support: `~/Library/Application Support/todo/`. Native on Mac, awkward in a terminal and not portable. Answer: Dotfile in home ### What format is the task file? - JSON (recommended, **chosen**): Structured, holds IDs and done state cleanly, readable. Awkward to hand-edit safely. - Plain text lines: One task per line like `3 [x] Pay rent`. Easy to read and edit by hand; we own the parsing. - todo.txt standard: Known format with tool support (`x ` prefix for done). Has no IDs, so it forces position numbers. - SQLite: One file, transactions for free. Not readable without a tool; overkill for a list. Answer: JSON ### Does `todo add` need quotes around the task text? - Join all words (recommended, **chosen**): `todo add Buy milk` and `todo add "Buy milk"` both work. Shell characters like & or ? still need quotes. - Exactly one argument: Text must be quoted; extra words are an error. Stricter, catches typos, more typing. Answer: Join all words ## Round 2 Settled: stable IDs, done tasks shown with a mark, Bun/TypeScript, `~/.todo.json`, JSON, `add` joins words. This round covers what each command prints and how done and delete behave. ### When you delete the newest task, can its ID come back? Stable IDs need a rule for the next number. The two simple rules differ only when the task with the highest ID gets deleted. - Never reuse (recommended, **chosen**): The file stores a nextId counter. An ID always means one task, forever. - Max + 1: No counter, the file is only the task list. After deleting the newest task, its ID comes back. Answer: Never reuse ### How does `todo list` lay out tasks? All options list tasks in ID order, which is also the order they were added. - A: ID first (recommended, **chosen**): Right-aligned ID, then [ ] or [x], then text. The ID you type is what your eye hits first. - B: box first: Checkbox column, then ID, then text. Reads like a checklist. - C: numbered: Markdown-like numbering, ✓ after done tasks. Looks lighter, but done is harder to scan. Answer: A: ID first ### What does `todo list` print when there are no tasks? - Hint (recommended, **chosen**): `No tasks. Add one with: todo add <text>` - Short note: `No tasks.` - Nothing: Empty output, Unix style. Hard to tell apart from a broken file path. Answer: Hint ### What do `add`, `done` and `delete` print on success? - One-line echo (recommended, **chosen**): `Added 4: Buy milk`, `Done 3: Pay rent`, `Deleted 3: Pay rent`. You see the ID and which task was hit. - Updated list: Print the full list after each change. Handy while small, noisy once it grows. - Silent: Print nothing, Unix style. You don't learn the new ID after `add`. Answer: One-line echo ### Can a done task be marked not done again? You asked for four commands. A typo in `todo done 3` can mark the wrong task. - Not now (recommended, **chosen**): No undo. Fix it by hand in the JSON, or delete and re-add (which gives a new ID). Keeps the scope you asked for. - `undone` command: `todo undone 3` clears the mark. One more small command. - `done` toggles: Running `done 3` on a done task reopens it. No new command, but running it twice undoes your change. Answer: Not now ### What happens on `todo done 3` when task 3 is already done? - Note, success (recommended): `3 is already done: Pay rent`, exit 0. You get what you wanted, so it's not a failure. - Error: Message on stderr, non-zero exit. - Silent success (**chosen**): Echo as usual, as if it was just marked. Answer: Silent success ### Does `todo delete 3` ask before deleting? - No prompt (recommended, **chosen**): Deletes right away and echoes what it removed. Fast. A wrong ID is lost unless you re-add it. - Prompt y/N: `Delete 3: Pay rent? [y/N]`. Safer, slower, and awkward in scripts. - Prompt if open: Asks only when the task is not done yet. Answer: No prompt ### What does the file store for each task? - ID, text, done (recommended): Only what the four commands need. Adding fields later is easy with JSON. - Plus timestamps (**chosen**): Also createdAt and doneAt. Unused by any command today, but history can't be recovered later. Answer: Plus timestamps ## Round 3 Settled in round 2: never-reused IDs, ID-first list layout, one-line echoes, no undo, silent repeat of done, no delete prompt, and createdAt/doneAt timestamps. This round covers timestamp details, errors and file safety. ### When `done` runs on a task that is already done, what happens to its doneAt? You chose silent success for a repeated done. The timestamp is the only thing that could change. - Keep first (recommended): doneAt stays at the moment it was first marked done. The repeat is a true no-op and the file is not rewritten. - Overwrite: doneAt moves to now. The date then means 'last marked done', not 'finished'. Answer: 💬 discuss Comment: "error out" ### How are timestamps written in the JSON file? - ISO 8601 UTC (recommended, **chosen**): `2026-09-23T15:46:00.000Z` from `toISOString()`. Readable, sorts as text, no time zone confusion. - ISO with offset: `2026-09-23T09:46:00-06:00`. Shows your local time when read by hand, and JS needs extra code to produce it. - Epoch millis: `1790178360000`. Compact, unreadable by hand. Answer: ISO 8601 UTC ### Does `todo list` show any dates? - No dates (recommended): Timestamps stay in the file for later use. The list stays clean. - Done date (**chosen**): Done tasks show when they were finished. - Both dates: Created date on every task, done date on done ones. Busiest output. Answer: Done date ### How are user errors reported, e.g. `todo done 7` when there is no task 7, or `todo done abc`? - stderr + exit 1 (recommended, **chosen**): One line on stderr like `No task with ID 7.` or `Not a task ID: abc`, exit code 1. Nothing is written to the file. - Plus usage: Same, but also print the usage text after the message. Helpful on first use, noisy after. Answer: stderr + exit 1 ### What does `todo add` do with no text, or only spaces? - Reject (recommended, **chosen**): Error `Task text is empty.`, exit 1. Text is trimmed before saving. - Allow: Saves an empty task. Nothing to show in the list, so it's almost always a typo. Answer: Reject ### What does plain `todo` with no subcommand do? - Show usage (recommended, **chosen**): Prints the four commands, exit 0. Unknown subcommands print the same usage to stderr, exit 1. - Same as list: Shortcut for the most common command. One more rule to know, and a typo in a subcommand still has to be an error. Answer: Show usage ### What happens when `~/.todo.json` doesn't exist yet? - Empty until write (recommended, **chosen**): Reads as an empty list. The file is created on the first `add`. `list` never creates files. - Create on any run: Any command creates an empty file first. Simpler read path, but `todo list` has a side effect. Answer: Empty until write ### What happens when `~/.todo.json` exists but can't be read as a task list, e.g. after a bad hand edit? The JSON is hand-editable, so a stray comma is realistic. - Refuse, touch nothing (recommended, **chosen**): Error naming the file and the problem, exit 1. Every command refuses until you fix it. No data is ever lost. - Back up, start fresh: Rename it to `.todo.json.broken-<time>` and start empty. Keeps working, but your tasks silently vanish from the list. Answer: Refuse, touch nothing ### How is the file written, given a crash mid-write or two terminals running `todo` at once? - Atomic replace (recommended, **chosen**): Write a temp file next to it, then rename over. A crash never leaves a half-written file. Two runs at the exact same moment can still lose one change. - Atomic + lock: Also take a lock file around read-modify-write. No lost updates, but stale locks after a crash need handling. - Plain overwrite: Write straight to the file. Simplest; a crash mid-write can corrupt it. Answer: Atomic replace ### Discussed in chat: doneAt when `done` runs again Your comment "error out" reversed round 2's "Silent success" for a repeated `done`. Proposed: - ⭐ Error, file untouched: `Task 3 is already done.` on stderr, exit 1. doneAt keeps its first value, the file is not rewritten. - ❌ Error text but exit 0: a script can't tell it from success. - ❌ Keep round 2's silent success: ignores the new answer. You replied: "agreed, file untouched". ## Round 4 Settled: repeating `done` is an error and leaves the file untouched. This round covers the last pieces of behavior, then install, testing and where the code lives. (Checked: no `todo` command exists on this machine yet, and ~/.local/bin is on PATH.) ### How does `list` show the done date? The file stores UTC. The list converts it to your local date. - ISO date (recommended, **chosen**): Local `YYYY-MM-DD`. Unambiguous, same width every time, no locale rules. - Short: `Sep 23`, with the year only when it's not this year. Friendlier, one more rule. - Relative: `today`, `2 days ago`. Nice to read, changes every day, harder to test. Answer: ISO date ### What do `done` and `delete` accept as arguments? - Exactly one ID (recommended, **chosen**): `todo done` → `Missing task ID.`; `todo done 3 4` → `Expected one task ID.` Both exit 1, file untouched. - One or more IDs: `todo done 3 4 5` marks all three. Needs a rule for a partial failure (all-or-nothing). - Ignore extras: Uses the first ID, ignores the rest. Hides typos. Answer: Exactly one ID ### Can the task file location be overridden? Tests must never touch your real `~/.todo.json`. - TODO_FILE env var (recommended): If set, it's the file path; otherwise `~/.todo.json`. Tests and experiments point it at a temp file. - --file flag: `todo --file x.json list`. Visible, but every command gets a global flag to parse. - No override (**chosen**): Tests set HOME to a temp dir instead. Nothing extra in the app, a bit indirect. Answer: No override ### Which inputs print the usage text? - todo, help, -h, --help (recommended, **chosen**): All four print usage to stdout, exit 0. Covers every habit. - todo and --help: Only plain `todo` and `--help`. `help` and `-h` are unknown commands. Answer: todo, help, -h, --help ### How does the `todo` command get onto your PATH? - Compiled binary (recommended, **chosen**): `bun build --compile` into `~/.local/bin/todo`. Fast start, no Bun needed at run time. Rebuild after changes. - bun link: A `bin` entry in package.json, linked into `~/.bun/bin`. Always runs the current source, needs Bun present. - Shell alias: `alias todo='bun run …/main.ts'` in your zshrc. Quickest, only works in interactive shells. Answer: Compiled binary ### How do tests deal with createdAt and doneAt? Timestamps come from the clock, and the list shows a local date, so the time zone matters too. - Injected clock (recommended, **chosen**): The app gets its clock from outside. Tests use a fixed clock, so timestamps and printed dates are exact. - Loose assertions: Tests check timestamps fall between before and after. Less setup, done-date output can't be checked exactly. Answer: Injected clock ### What's the main testing approach? - Outside-in TDD (recommended, **chosen**): Acceptance tests drive the commands in-process with an in-memory store and fixed clock, plus a few tests for the JSON file store against a temp file. `bun test`. - Approval tests: Command sequences with their printed output checked against approved transcripts. Great for output layout, weaker for pinpointing rules. - Real process only: Spawn the binary with TODO_FILE set, check stdout, stderr and exit code. Closest to real use, slower. Answer: Outside-in TDD ### Where does the code live? - Folder in this repo (recommended): `todo/` in the workshop workspace, next to `skills/` and `docs/`. - Own repo: A new repo, e.g. `~/ws/todo`. Cleaner history for a tool you'll keep using. - Repo root (**chosen**): package.json at the root of this repo. Mixes the app with the workshop files. Answer: Repo root ## Round 5 Last round: small edge cases and the exact file shape. After this, I write the plan into session.md. ### Is 'done' stored as its own flag, or derived from doneAt? Written pretty-printed with 2-space indent, so hand edits stay easy. - Derive from doneAt (recommended, **chosen**): A task is done when doneAt is set (shape above). One source of truth, they can't disagree. - Separate flag: `"done": true` next to doneAt. Clearer to read by hand, but a hand edit can set one without the other. Answer: Derive from doneAt ### Does the file carry a format version? - version: 1: `"version": 1` at the top. A future format change can detect and migrate old files. One line of cost. - No version (recommended, **chosen**): Add it only when the format first changes. Old files would then be 'no version = 1'. Answer: No version ### What if the file is valid JSON but breaks the rules, e.g. duplicate IDs or nextId not above the highest ID? - Refuse (recommended, **chosen**): Same as a broken file: error naming the file and the rule, exit 1, nothing touched. - Repair quietly: e.g. raise nextId to highest ID + 1. Friendlier, but duplicates can't be repaired safely. Answer: Refuse ### What happens to newlines and runs of spaces inside task text? A quoted argument can contain a newline, which would break the one-task-per-line list. - Collapse to one space (recommended, **chosen**): Any run of whitespace, newlines included, becomes one space. `list` stays one line per task. - Reject newlines: Error on a newline, keep other spacing as typed. - Keep as typed: Stored and printed exactly. A newline breaks the list layout. Answer: Collapse to one space ### What counts as a valid task ID on the command line? - Positive integer (recommended, **chosen**): Digits only, value ≥ 1. `03` means 3. `0`, `-1`, `3.5`, `abc` → `Not a task ID: …`, exit 1. - Strict digits: Same, but leading zeros are rejected too. Answer: Positive integer ### How do tests pin the time zone for the printed done date? - Pin TZ in test setup (recommended, **chosen**): A bun test preload sets TZ (e.g. America/Denver). The app code stays unaware of time zones. - Inject the zone: The app receives a date formatter or zone alongside the clock. More explicit, one more dependency. - Pick UTC-safe times: Tests use midday times so any zone gives the same date. No setup, but relies on care in every test. Answer: Pin TZ in test setup ### How is the build-and-install step run? - package.json script (recommended, **chosen**): `bun run install-cli` compiles to `~/.local/bin/todo`. One command, lives with the code. - Documented command: The `bun build --compile …` line in the README, typed by hand. Answer: package.json script
2. Extract Knowledge
Start a new session
- Clear your session (
/clear) or start a new one. - Ask your agent to read the file the other session created.
2. Extract Knowledge
Check alignment
- In the new session, paste the prompt.
- Compare what it says with what you remember agreeing on.
Tell me what we're planning to build on a high level without technical details. Draw ascii diagrams to illustrate ideas when it's helpful.
2. Extract Knowledge
Take me through it step by step
- Paste the prompt. It asks the agent to build the picture a little at a time.
- Ask questions as you go.
Read docs/project.md and docs/spec.md. Then help me build a clear picture of what we're building. Start from the overall goal and high level architecture, giving me only a little bit of information at a time and letting me ask questions. Keep communication succinct, use simple english sentences and wherever possible show, don't tell (you can use ascii diagrams and examples).
3. Ground Rules
Add ground rules
- Find the global rules file of your coding agent:
~/.claude/CLAUDE.md,~/.copilot/copilot-instructions.md,~/.codex/AGENTS.md,~/.config/opencode/AGENTS.mdor~/.pi/agent/AGENTS.md. - Copy the rules and add them to that file.
## Honesty - Tell me something I need to know even if I don't want to hear it. - If you don't know something, say "I don't know" instead of making things up - Have opinions. You're allowed to disagree and push back when you think I'm making a bad choice. ## Proactivity - Warn me about risks and better alternatives I might be missing. - Flag unclear but important points before they become problems. - Push back when something seems wrong, don't just agree with mistakes.
3. Ground Rules
Add a context marker
- Copy the line and add it at the top of the same file.
- Start a new session and say hi. Look for the 🍀 at the start of the reply.
Always begin each reply with STARTER_CHARACTER followed by a space (default: 🍀). The starter may be replaced or extended with additional characters.
3. Ground Rules
Add a decisions section
- Copy the section and add it to the end of the same file.
- Exit and start a new session.
- Paste the prompt. Look for the ⭐ pick and the ❌ options it rejected.
## Decisions Show me the whole terrain, one line per option, including options I would never think of. Left alone I pick from the one or two I can hold in my head. When you need my input on a decision: - First, pick the option you think is best and propose it. Start that message with `⭐` - Also list the alternatives you rejected, each with a brief reason why. Start each with `❌` Then we can have a discussion.
Help me choose a name for my todo app.
4. Ground Rule Smells
Smells in global rules
- Copy the file below and save it as
.playground/global-rules.mdin your workspace. - Read it as global rules: they apply to every session on that computer.
- List all the problems you see with it.
# Development Guidelines for Claude
## Core Philosophy
**TEST-DRIVEN DEVELOPMENT IS NON-NEGOTIABLE.** Every single line of production code must be written in response to a failing test. No exceptions. This is not a suggestion or a preference - it is the fundamental practice that enables all other principles in this document.
I follow Test-Driven Development (TDD) with a strong emphasis on behavior-driven testing and functional programming principles. All work should be done in small, incremental changes that maintain a working state throughout development.
## Quick Reference
**Key Principles:**
- Write tests first (TDD)
- Test behavior, not implementation
- No `any` types or type assertions
- Immutable data only
- Small, pure functions
- TypeScript strict mode always
- Use real schemas/types in tests, never redefine them
**Preferred Tools:**
- **Language**: TypeScript (strict mode)
- **Testing**: Jest/Vitest + React Testing Library
- **State Management**: Prefer immutable patterns
## Testing Principles
### Behavior-Driven Testing
- **No "unit tests"** - this term is not helpful. Tests should verify expected behavior, treating implementation as a black box
- Test through the public API exclusively - internals should be invisible to tests
- No 1:1 mapping between test files and implementation files
- Tests that examine internal implementation details are wasteful and should be avoided
- **Coverage targets**: 100% coverage should be expected at all times, but these tests must ALWAYS be based on business behaviour, not implementation details
- Tests must document expected business behaviour
### Testing Tools
- **Jest** or **Vitest** for testing frameworks
- **React Testing Library** for React components
- **MSW (Mock Service Worker)** for API mocking when needed
- All test code must follow the same TypeScript strict mode rules as production code
### Test Organization
```
src/
features/
payment/
payment-processor.ts
payment-validator.ts
payment-processor.test.ts // The validator is an implementation detail. Validation is fully covered, but by testing the expected business behaviour, treating the validation code itself as an implementation detail
```
### Test Data Pattern
Use factory functions with optional overrides for test data:
```typescript
const getMockPaymentPostPaymentRequest = (
overrides?: Partial<PostPaymentsRequestV3>
): PostPaymentsRequestV3 => {
return {
CardAccountId: "1234567890123456",
Amount: 100,
Source: "Web",
AccountStatus: "Normal",
LastName: "Doe",
DateOfBirth: "1980-01-01",
PayingCardDetails: {
Cvv: "123",
Token: "token",
},
AddressDetails: getMockAddressDetails(),
Brand: "Visa",
...overrides,
};
};
const getMockAddressDetails = (
overrides?: Partial<AddressDetails>
): AddressDetails => {
return {
HouseNumber: "123",
HouseName: "Test House",
AddressLine1: "Test Address Line 1",
AddressLine2: "Test Address Line 2",
City: "Test City",
...overrides,
};
};
```
Key principles:
- Always return complete objects with sensible defaults
- Accept optional `Partial<T>` overrides
- Build incrementally - extract nested object factories as needed
- Compose factories for complex objects
- Consider using a test data builder pattern for very complex objects
**Validating Test Data:**
When schemas exist, validate factory output to catch test data issues early:
```typescript
import { PaymentSchema, type Payment } from '../schemas/payment.schema';
const getMockPayment = (overrides?: Partial<Payment>): Payment => {
const basePayment = {
amount: 100,
currency: "GBP",
cardId: "card_123",
customerId: "cust_456",
};
const paymentData = { ...basePayment, ...overrides };
// Validate against real schema to catch type mismatches
return PaymentSchema.parse(paymentData);
};
// This catches errors in test setup:
const payment = getMockPayment({
amount: -100 // ❌ Schema validation fails: amount must be positive
});
```
**Why validate test data:**
- Ensures test factories produce valid data that matches production schemas
- Catches test data bugs immediately rather than in test assertions
- Documents constraints (e.g., "amount must be positive") in schema, not in every test
- Prevents tests from passing with invalid data that would fail in production
## TypeScript Guidelines
### Strict Mode Requirements
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
```
- **No `any`** - ever. Use `unknown` if type is truly unknown
- **No type assertions** (`as SomeType`) unless absolutely necessary with clear justification
- **No `@ts-ignore`** or `@ts-expect-error` without explicit explanation
- These rules apply to test code as well as production code
### Type Definitions
- **Prefer `type` over `interface`** - Use `type` for data structures and shapes. Reserve `interface` ONLY for behavior contracts (ports, adapters, dependency injection):
```typescript
// ✅ CORRECT - type for data structures
type User = {
readonly id: string;
readonly email: string;
readonly role: UserRole;
};
type PaymentRequest = {
amount: number;
currency: string;
};
// ✅ CORRECT - interface for behavior contracts
interface Logger {
log(message: string): void;
error(message: string, error: Error): void;
}
interface PaymentGateway {
processPayment(payment: Payment): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
}
// ❌ WRONG - interface for data structure
interface User {
id: string;
email: string;
}
```
**Why this distinction?**
- **Types** describe what data IS (structure, shape)
- **Interfaces** describe what code DOES (behavior, contracts)
- Interfaces support declaration merging and extension, useful for dependency injection and plugin systems
- Types are more flexible for complex type operations (unions, intersections, mapped types)
- Use explicit typing where it aids clarity, but leverage inference where appropriate
- Utilize utility types effectively (`Pick`, `Omit`, `Partial`, `Required`, etc.)
- Create domain-specific types (e.g., `UserId`, `PaymentId`) for type safety
- Use Zod or any other [Standard Schema](https://standardschema.dev/) compliant schema library to create types, by creating schemas first
```typescript
// Good - Branded types for type safety
type UserId = string & { readonly brand: unique symbol };
type PaymentAmount = number & { readonly brand: unique symbol };
// Avoid - No type distinction
type UserId = string;
type PaymentAmount = number;
```
#### Schema-First Development with Zod
Always define your schemas first, then derive types from them:
```typescript
import { z } from "zod";
// Define schemas first - these provide runtime validation
const AddressDetailsSchema = z.object({
houseNumber: z.string(),
houseName: z.string().optional(),
addressLine1: z.string().min(1),
addressLine2: z.string().optional(),
city: z.string().min(1),
postcode: z.string().regex(/^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i),
});
const PayingCardDetailsSchema = z.object({
cvv: z.string().regex(/^\d{3,4}$/),
token: z.string().min(1),
});
const PostPaymentsRequestV3Schema = z.object({
cardAccountId: z.string().length(16),
amount: z.number().positive(),
source: z.enum(["Web", "Mobile", "API"]),
accountStatus: z.enum(["Normal", "Restricted", "Closed"]),
lastName: z.string().min(1),
dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
payingCardDetails: PayingCardDetailsSchema,
addressDetails: AddressDetailsSchema,
brand: z.enum(["Visa", "Mastercard", "Amex"]),
});
// Derive types from schemas
type AddressDetails = z.infer<typeof AddressDetailsSchema>;
type PayingCardDetails = z.infer<typeof PayingCardDetailsSchema>;
type PostPaymentsRequestV3 = z.infer<typeof PostPaymentsRequestV3Schema>;
// Use schemas at runtime boundaries
export const parsePaymentRequest = (data: unknown): PostPaymentsRequestV3 => {
return PostPaymentsRequestV3Schema.parse(data);
};
// Example of schema composition for complex domains
const BaseEntitySchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
});
const CustomerSchema = BaseEntitySchema.extend({
email: z.string().email(),
tier: z.enum(["standard", "premium", "enterprise"]),
creditLimit: z.number().positive(),
});
type Customer = z.infer<typeof CustomerSchema>;
```
#### When Schemas Are Required vs. Optional
**Not all types need schemas.** Use this decision framework to determine when runtime validation is necessary:
**Decision Framework:**
Ask these questions in order:
1. **Does data cross a trust boundary?** (external → internal)
- YES → ✅ Schema required
- NO → Continue
2. **Does type have validation rules?** (format, constraints, enums)
- YES → ✅ Schema required
- NO → Continue
3. **Is this a shared data contract?** (between systems)
- YES → ✅ Schema required
- NO → Continue
4. **Used in test factories?**
- YES → ✅ Schema required (for validation)
- NO → Continue
5. **Pure internal type?** (utility, state, behavior)
- YES → ❌ Type is fine (no schema needed)
- NO → ✅ Schema recommended for safety
**✅ Schema REQUIRED Examples:**
```typescript
// API responses (trust boundary)
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
});
const user = UserSchema.parse(apiResponse);
// Business validation rules
const PaymentSchema = z.object({
amount: z.number().positive().max(10000),
email: z.string().email(),
cardNumber: z.string().regex(/^\d{16}$/),
});
// Shared data contracts (events, messages)
const OrderCreatedEventSchema = z.object({
orderId: z.string(),
customerId: z.string(),
items: z.array(z.object({ sku: z.string(), quantity: z.number() })),
});
// Test data factories (ensures test data validity)
const getMockUser = (): User => {
return UserSchema.parse({
id: "550e8400-e29b-41d4-a716-446655440000",
email: "[email protected]",
role: "user",
});
};
```
**❌ Schema OPTIONAL Examples:**
```typescript
// Pure internal types (no external data, no validation)
type Point = { readonly x: number; readonly y: number };
type CartTotal = { subtotal: number; tax: number; total: number };
// Result/Option types (internal logic)
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// TypeScript utilities (compile-time only)
type UserProfile = Pick<User, 'id' | 'name'>;
type PartialUser = Partial<User>;
// Branded primitives (compile-time nominal types)
type UserId = string & { readonly brand: unique symbol };
type PaymentId = string & { readonly brand: unique symbol };
// Behavior contracts (interface for behavior, not data)
interface Logger {
log(message: string): void;
error(message: string, error: Error): void;
}
// Internal state machines
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: unknown }
| { status: "error"; error: Error };
// Component props (usually - internal to app)
type ButtonProps = {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
};
// Exception: If props come from URL params or API → schema required
```
**Summary:** Use schemas at trust boundaries and for validation. For internal types, utilities, and behavior contracts, plain TypeScript types are sufficient.
#### Schema Usage in Tests
**CRITICAL**: Tests must use real schemas and types from the main project, not redefine their own.
```typescript
// ❌ WRONG - Defining schemas in test files
const ProjectSchema = z.object({
id: z.string(),
workspaceId: z.string(),
ownerId: z.string().nullable(),
name: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
// ✅ CORRECT - Import schemas from the shared schema package
import { ProjectSchema, type Project } from "@your-org/schemas";
```
**Why this matters:**
- **Type Safety**: Ensures tests use the same types as production code
- **Consistency**: Changes to schemas automatically propagate to tests
- **Maintainability**: Single source of truth for data structures
- **Prevents Drift**: Tests can't accidentally diverge from real schemas
**Implementation:**
- All domain schemas should be exported from a shared schema package or module
- Test files should import schemas from the shared location
- If a schema isn't exported yet, add it to the exports rather than duplicating it
- Mock data factories should use the real types derived from real schemas
```typescript
// ✅ CORRECT - Test factories using real schemas
import { ProjectSchema, type Project } from "@your-org/schemas";
const getMockProject = (overrides?: Partial<Project>): Project => {
const baseProject = {
id: "proj_123",
workspaceId: "ws_456",
ownerId: "user_789",
name: "Test Project",
createdAt: new Date(),
updatedAt: new Date(),
};
const projectData = { ...baseProject, ...overrides };
// Validate against real schema to catch type mismatches
return ProjectSchema.parse(projectData);
};
```
## Code Style
### Functional Programming
I follow a "functional light" approach:
- **No data mutation** - work with immutable data structures
- **Pure functions** wherever possible
- **Composition** as the primary mechanism for code reuse
- Avoid heavy FP abstractions (no need for complex monads or pipe/compose patterns) unless there is a clear advantage to using them
- Use array methods (`map`, `filter`, `reduce`) over imperative loops
#### Examples of Functional Patterns
```typescript
// Good - Pure function with immutable updates
const applyDiscount = (order: Order, discountPercent: number): Order => {
return {
...order,
items: order.items.map((item) => ({
...item,
price: item.price * (1 - discountPercent / 100),
})),
totalPrice: order.items.reduce(
(sum, item) => sum + item.price * (1 - discountPercent / 100),
0
),
};
};
// Good - Composition over complex logic
const processOrder = (order: Order): ProcessedOrder => {
return pipe(
order,
validateOrder,
applyPromotions,
calculateTax,
assignWarehouse
);
};
// When heavy FP abstractions ARE appropriate:
// - Complex async flows that benefit from Task/IO types
// - Error handling chains that benefit from Result/Either types
// Example with Result type for complex error handling:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const chainPaymentOperations = (
payment: Payment
): Result<Receipt, PaymentError> => {
return pipe(
validatePayment(payment),
chain(authorizePayment),
chain(capturePayment),
map(generateReceipt)
);
};
```
#### Immutability Violations to Avoid
**Array Mutations:**
```typescript
// ❌ WRONG - Array mutations
items.push(newItem);
items.pop();
items.splice(0, 1);
items.shift();
items.unshift(newItem);
items[0] = updatedItem;
items.sort();
items.reverse();
// ✅ CORRECT - Immutable alternatives
const withNew = [...items, newItem];
const withoutLast = items.slice(0, -1);
const withoutFirst = items.slice(1);
const withFirst = [newItem, ...items];
const updated = items.map((item, i) => i === 0 ? updatedItem : item);
const sorted = [...items].sort();
const reversed = [...items].reverse();
```
**Object Mutations:**
```typescript
// ❌ WRONG - Object mutations
user.name = "New Name";
delete user.email;
Object.assign(user, updates);
// ✅ CORRECT - Immutable alternatives
const updated = { ...user, name: "New Name" };
const { email, ...withoutEmail } = user;
const merged = { ...user, ...updates };
```
**Nested Structure Updates:**
```typescript
// ❌ WRONG - Mutating nested structures
cart.items[0].quantity = 5;
// ✅ CORRECT - Immutable nested update
const updatedCart = {
...cart,
items: cart.items.map((item, i) =>
i === 0 ? { ...item, quantity: 5 } : item
),
};
```
**Why immutability matters:**
- **Predictable**: No hidden side effects - function inputs never change
- **Debuggable**: State changes are explicit and traceable
- **Testable**: Pure functions are trivial to test
- **React-friendly**: Reliable re-renders and memoization
- **Concurrent-safe**: No race conditions from shared mutable state
### Code Structure
- **No nested if/else statements** - use early returns, guard clauses, or composition
- **Avoid deep nesting** in general (max 2 levels)
- Keep functions small and focused on a single responsibility
- Prefer flat, readable code over clever abstractions
### Naming Conventions
- **Functions**: `camelCase`, verb-based (e.g., `calculateTotal`, `validatePayment`)
- **Types**: `PascalCase` (e.g., `PaymentRequest`, `UserProfile`)
- **Constants**: `UPPER_SNAKE_CASE` for true constants, `camelCase` for configuration
- **Files**: `kebab-case.ts` for all TypeScript files
- **Test files**: `*.test.ts` or `*.spec.ts`
### No Comments in Code
Code should be self-documenting through clear naming and structure. Comments indicate that the code itself is not clear enough.
```typescript
// Avoid: Comments explaining what the code does
const calculateDiscount = (price: number, customer: Customer): number => {
// Check if customer is premium
if (customer.tier === "premium") {
// Apply 20% discount for premium customers
return price * 0.8;
}
// Regular customers get 10% discount
return price * 0.9;
};
// Good: Self-documenting code with clear names
const PREMIUM_DISCOUNT_MULTIPLIER = 0.8;
const STANDARD_DISCOUNT_MULTIPLIER = 0.9;
const isPremiumCustomer = (customer: Customer): boolean => {
return customer.tier === "premium";
};
const calculateDiscount = (price: number, customer: Customer): number => {
const discountMultiplier = isPremiumCustomer(customer)
? PREMIUM_DISCOUNT_MULTIPLIER
: STANDARD_DISCOUNT_MULTIPLIER;
return price * discountMultiplier;
};
// Avoid: Complex logic with comments
const processPayment = (payment: Payment): ProcessedPayment => {
// First validate the payment
if (!validatePayment(payment)) {
throw new Error("Invalid payment");
}
// Check if we need to apply 3D secure
if (payment.amount > 100 && payment.card.type === "credit") {
// Apply 3D secure for credit cards over £100
const securePayment = apply3DSecure(payment);
// Process the secure payment
return executePayment(securePayment);
}
// Process the payment
return executePayment(payment);
};
// Good: Extract to well-named functions
const requires3DSecure = (payment: Payment): boolean => {
const SECURE_PAYMENT_THRESHOLD = 100;
return (
payment.amount > SECURE_PAYMENT_THRESHOLD && payment.card.type === "credit"
);
};
const processPayment = (payment: Payment): ProcessedPayment => {
if (!validatePayment(payment)) {
throw new PaymentValidationError("Invalid payment");
}
const securedPayment = requires3DSecure(payment)
? apply3DSecure(payment)
: payment;
return executePayment(securedPayment);
};
```
**Exception**: JSDoc comments for public APIs are acceptable when generating documentation, but the code should still be self-explanatory without them.
### Prefer Options Objects
Use options objects for function parameters as the default pattern. Only use positional parameters when there's a clear, compelling reason (e.g., single-parameter pure functions, well-established conventions like `map(item => item.value)`).
```typescript
// Avoid: Multiple positional parameters
const createPayment = (
amount: number,
currency: string,
cardId: string,
customerId: string,
description?: string,
metadata?: Record<string, unknown>,
idempotencyKey?: string
): Payment => {
// implementation
};
// Calling it is unclear
const payment = createPayment(
100,
"GBP",
"card_123",
"cust_456",
undefined,
{ orderId: "order_789" },
"key_123"
);
// Good: Options object with clear property names
type CreatePaymentOptions = {
amount: number;
currency: string;
cardId: string;
customerId: string;
description?: string;
metadata?: Record<string, unknown>;
idempotencyKey?: string;
};
const createPayment = (options: CreatePaymentOptions): Payment => {
const {
amount,
currency,
cardId,
customerId,
description,
metadata,
idempotencyKey,
} = options;
// implementation
};
// Clear and readable at call site
const payment = createPayment({
amount: 100,
currency: "GBP",
cardId: "card_123",
customerId: "cust_456",
metadata: { orderId: "order_789" },
idempotencyKey: "key_123",
});
// Avoid: Boolean flags as parameters
const fetchCustomers = (
includeInactive: boolean,
includePending: boolean,
includeDeleted: boolean,
sortByDate: boolean
): Customer[] => {
// implementation
};
// Confusing at call site
const customers = fetchCustomers(true, false, false, true);
// Good: Options object with clear intent
type FetchCustomersOptions = {
includeInactive?: boolean;
includePending?: boolean;
includeDeleted?: boolean;
sortBy?: "date" | "name" | "value";
};
const fetchCustomers = (options: FetchCustomersOptions = {}): Customer[] => {
const {
includeInactive = false,
includePending = false,
includeDeleted = false,
sortBy = "name",
} = options;
// implementation
};
// Self-documenting at call site
const customers = fetchCustomers({
includeInactive: true,
sortBy: "date",
});
// Good: Configuration objects for complex operations
type ProcessOrderOptions = {
order: Order;
shipping: {
method: "standard" | "express" | "overnight";
address: Address;
};
payment: {
method: PaymentMethod;
saveForFuture?: boolean;
};
promotions?: {
codes?: string[];
autoApply?: boolean;
};
};
const processOrder = (options: ProcessOrderOptions): ProcessedOrder => {
const { order, shipping, payment, promotions = {} } = options;
// Clear access to nested options
const orderWithPromotions = promotions.autoApply
? applyAvailablePromotions(order)
: order;
return executeOrder({
...orderWithPromotions,
shippingMethod: shipping.method,
paymentMethod: payment.method,
});
};
// Acceptable: Single parameter for simple transforms
const double = (n: number): number => n * 2;
const getName = (user: User): string => user.name;
// Acceptable: Well-established patterns
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
const users = fetchUsers();
const names = users.map((user) => user.name);
```
**Guidelines for options objects:**
- Default to options objects unless there's a specific reason not to
- Always use for functions with optional parameters
- Destructure options at the start of the function for clarity
- Provide sensible defaults using destructuring
- Keep related options grouped (e.g., all shipping options together)
- Consider breaking very large options objects into nested groups
**When positional parameters are acceptable:**
- Single-parameter pure functions
- Well-established functional patterns (map, filter, reduce callbacks)
- Mathematical operations where order is conventional
## Development Workflow
### TDD Process - THE FUNDAMENTAL PRACTICE
**CRITICAL**: TDD is not optional. Every feature, every bug fix, every change MUST follow this process:
Follow Red-Green-Refactor strictly:
1. **Red**: Write a failing test for the desired behavior. NO PRODUCTION CODE until you have a failing test.
2. **Green**: Write the MINIMUM code to make the test pass. Resist the urge to write more than needed.
3. **Refactor**: Assess the code for improvement opportunities. If refactoring would add value, clean up the code while keeping tests green. If the code is already clean and expressive, move on.
**Common TDD Violations to Avoid:**
- Writing production code without a failing test first
- Writing multiple tests before making the first one pass
- Writing more production code than needed to pass the current test
- Skipping the refactor assessment step when code could be improved
- Adding functionality "while you're there" without a test driving it
**Remember**: If you're typing production code and there isn't a failing test demanding that code, you're not doing TDD.
#### TDD Quality Gates
Before allowing any commit, verify:
- ✅ All production code has a test that demanded it
- ✅ Tests verify behavior, not implementation
- ✅ Implementation is minimal (only what's needed)
- ✅ Refactoring assessment completed (if tests green)
- ✅ All tests pass
- ✅ TypeScript strict mode satisfied
- ✅ No `any` types or unjustified assertions
- ✅ Factory functions used (no `let`/`beforeEach`)
#### Anti-Patterns in Tests
**Avoid these test smells:**
```typescript
// ❌ BAD - Implementation-focused test
it("should call validateAmount", () => {
const spy = jest.spyOn(validator, 'validateAmount');
processPayment(payment);
expect(spy).toHaveBeenCalled();
});
// ✅ GOOD - Behavior-focused test
it("should reject payments with negative amounts", () => {
const payment = getMockPayment({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error.message).toBe("Invalid amount");
});
// ❌ BAD - Using let and beforeEach (shared mutable state)
let payment: Payment;
beforeEach(() => {
payment = { amount: 100 };
});
it("should process payment", () => {
processPayment(payment);
});
// ✅ GOOD - Factory functions (isolated, immutable)
it("should process payment", () => {
const payment = getMockPayment({ amount: 100 });
processPayment(payment);
});
```
#### Verifying TDD Compliance Retrospectively
To verify that code was developed test-first, examine git history:
```bash
# Check if test was written before implementation
git log -p --follow src/features/payment/payment-processor.ts
git log -p --follow src/features/payment/payment-processor.test.ts
# Look for:
# ✅ GOOD: Test commit comes BEFORE implementation commit
# ❌ BAD: Implementation committed without corresponding test
# ❌ BAD: Test and implementation in same commit without clear RED phase
```
**During code review, check:**
- Was there a failing test before each production code change?
- Do commit messages indicate RED-GREEN-REFACTOR progression?
- Are refactoring commits separate from feature commits?
**Example good commit sequence:**
```
feat(test): add test for payment validation (RED)
feat: implement payment validation (GREEN)
refactor: extract payment validation constants (REFACTOR)
```
**Example bad commit sequence:**
```
feat: add payment validation with tests
```
(Combined test+implementation suggests test wasn't written first)
#### TDD Example Workflow
```typescript
// Step 1: Red - Start with the simplest behavior
describe("Order processing", () => {
it("should calculate total with shipping cost", () => {
const order = createOrder({
items: [{ price: 30, quantity: 1 }],
shippingCost: 5.99,
});
const processed = processOrder(order);
expect(processed.total).toBe(35.99);
expect(processed.shippingCost).toBe(5.99);
});
});
// Step 2: Green - Minimal implementation
const processOrder = (order: Order): ProcessedOrder => {
const itemsTotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return {
...order,
shippingCost: order.shippingCost,
total: itemsTotal + order.shippingCost,
};
};
// Step 3: Red - Add test for free shipping behavior
describe("Order processing", () => {
it("should calculate total with shipping cost", () => {
// ... existing test
});
it("should apply free shipping for orders over £50", () => {
const order = createOrder({
items: [{ price: 60, quantity: 1 }],
shippingCost: 5.99,
});
const processed = processOrder(order);
expect(processed.shippingCost).toBe(0);
expect(processed.total).toBe(60);
});
});
// Step 4: Green - NOW we can add the conditional because both paths are tested
const processOrder = (order: Order): ProcessedOrder => {
const itemsTotal = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const shippingCost = itemsTotal > 50 ? 0 : order.shippingCost;
return {
...order,
shippingCost,
total: itemsTotal + shippingCost,
};
};
// Step 5: Add edge case tests to ensure 100% behavior coverage
describe("Order processing", () => {
// ... existing tests
it("should charge shipping for orders exactly at £50", () => {
const order = createOrder({
items: [{ price: 50, quantity: 1 }],
shippingCost: 5.99,
});
const processed = processOrder(order);
expect(processed.shippingCost).toBe(5.99);
expect(processed.total).toBe(55.99);
});
});
// Step 6: Refactor - Extract constants and improve readability
const FREE_SHIPPING_THRESHOLD = 50;
const calculateItemsTotal = (items: OrderItem[]): number => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
};
const qualifiesForFreeShipping = (itemsTotal: number): boolean => {
return itemsTotal > FREE_SHIPPING_THRESHOLD;
};
const processOrder = (order: Order): ProcessedOrder => {
const itemsTotal = calculateItemsTotal(order.items);
const shippingCost = qualifiesForFreeShipping(itemsTotal)
? 0
: order.shippingCost;
return {
...order,
shippingCost,
total: itemsTotal + shippingCost,
};
};
```
### Refactoring - The Critical Third Step
Evaluating refactoring opportunities is not optional - it's the third step in the TDD cycle. After achieving a green state and committing your work, you MUST assess whether the code can be improved. However, only refactor if there's clear value - if the code is already clean and expresses intent well, move on to the next test.
#### What is Refactoring?
Refactoring means changing the internal structure of code without changing its external behavior. The public API remains unchanged, all tests continue to pass, but the code becomes cleaner, more maintainable, or more efficient. Remember: only refactor when it genuinely improves the code - not all code needs refactoring.
#### When to Refactor
- **Always assess after green**: Once tests pass, before moving to the next test, evaluate if refactoring would add value
- **When you see duplication**: But understand what duplication really means (see DRY below)
- **When names could be clearer**: Variable names, function names, or type names that don't clearly express intent
- **When structure could be simpler**: Complex conditional logic, deeply nested code, or long functions
- **When patterns emerge**: After implementing several similar features, useful abstractions may become apparent
**Remember**: Not all code needs refactoring. If the code is already clean, expressive, and well-structured, commit and move on. Refactoring should improve the code - don't change things just for the sake of change.
#### Refactoring Priority Classification
Not all refactoring is equally valuable. Classify issues by severity:
**🔴 Critical (Fix Now Before Commit):**
- Immutability violations (mutations)
- Semantic knowledge duplication (same business rule in multiple places)
- Deeply nested code (>3 levels)
- Security issues or data leak risks
**⚠️ High Value (Should Fix This Session):**
- Unclear names affecting comprehension
- Magic numbers/strings used multiple times
- Long functions (>30 lines) with multiple responsibilities
- Missing constants for important business rules
**💡 Nice to Have (Consider Later):**
- Minor naming improvements
- Extraction of single-use helper functions
- Structural reorganization that doesn't improve clarity
- Cosmetic changes without functional benefit
**✅ Skip Refactoring:**
- Code that's already clean and expressive
- Structural similarity without semantic relationship
- Changes that would make code less clear
- Abstractions that aren't yet proven necessary
**Example Assessment:**
```typescript
// After achieving green:
const processOrder = (order: Order): ProcessedOrder => {
const itemsTotal = order.items.reduce((sum, item) => sum + item.price, 0);
const shipping = itemsTotal > 50 ? 0 : 5.99;
return { ...order, total: itemsTotal + shipping, shippingCost: shipping };
};
// ASSESSMENT:
// 🔴 Critical: None
// ⚠️ High Value:
// - Magic number 50 (FREE_SHIPPING_THRESHOLD)
// - Magic number 5.99 (STANDARD_SHIPPING_COST)
// 💡 Nice to Have:
// - Could extract calculateItemsTotal helper
// ✅ Skip:
// - Overall structure is clear enough
// DECISION: Fix high-value issues (extract constants), skip nice-to-have
```
#### Refactoring Guidelines
##### 1. Commit Before Refactoring
Always commit your working code before starting any refactoring. This gives you a safe point to return to:
```bash
git add .
git commit -m "feat: add payment validation"
# Now safe to refactor
```
##### 2. Look for Useful Abstractions Based on Semantic Meaning
Create abstractions only when code shares the same semantic meaning and purpose. Don't abstract based on structural similarity alone - **duplicate code is far cheaper than the wrong abstraction**.
```typescript
// Similar structure, DIFFERENT semantic meaning - DO NOT ABSTRACT
const validatePaymentAmount = (amount: number): boolean => {
return amount > 0 && amount <= 10000;
};
const validateTransferAmount = (amount: number): boolean => {
return amount > 0 && amount <= 10000;
};
// These might have the same structure today, but they represent different
// business concepts that will likely evolve independently.
// Payment limits might change based on fraud rules.
// Transfer limits might change based on account type.
// Abstracting them couples unrelated business rules.
// Similar structure, SAME semantic meaning - SAFE TO ABSTRACT
const formatUserDisplayName = (firstName: string, lastName: string): string => {
return `${firstName} ${lastName}`.trim();
};
const formatCustomerDisplayName = (
firstName: string,
lastName: string
): string => {
return `${firstName} ${lastName}`.trim();
};
const formatEmployeeDisplayName = (
firstName: string,
lastName: string
): string => {
return `${firstName} ${lastName}`.trim();
};
// These all represent the same concept: "how we format a person's name for display"
// They share semantic meaning, not just structure
const formatPersonDisplayName = (
firstName: string,
lastName: string
): string => {
return `${firstName} ${lastName}`.trim();
};
// Replace all call sites throughout the codebase:
// Before:
// const userLabel = formatUserDisplayName(user.firstName, user.lastName);
// const customerName = formatCustomerDisplayName(customer.firstName, customer.lastName);
// const employeeTag = formatEmployeeDisplayName(employee.firstName, employee.lastName);
// After:
// const userLabel = formatPersonDisplayName(user.firstName, user.lastName);
// const customerName = formatPersonDisplayName(customer.firstName, customer.lastName);
// const employeeTag = formatPersonDisplayName(employee.firstName, employee.lastName);
// Then remove the original functions as they're no longer needed
```
**Questions to ask before abstracting:**
- Do these code blocks represent the same concept or different concepts that happen to look similar?
- If the business rules for one change, should the others change too?
- Would a developer reading this abstraction understand why these things are grouped together?
- Am I abstracting based on what the code IS (structure) or what it MEANS (semantics)?
**Remember**: It's much easier to create an abstraction later when the semantic relationship becomes clear than to undo a bad abstraction that couples unrelated concepts.
##### 3. Understanding DRY - It's About Knowledge, Not Code
DRY (Don't Repeat Yourself) is about not duplicating **knowledge** in the system, not about eliminating all code that looks similar.
```typescript
// This is NOT a DRY violation - different knowledge despite similar code
const validateUserAge = (age: number): boolean => {
return age >= 18 && age <= 100;
};
const validateProductRating = (rating: number): boolean => {
return rating >= 1 && rating <= 5;
};
const validateYearsOfExperience = (years: number): boolean => {
return years >= 0 && years <= 50;
};
// These functions have similar structure (checking numeric ranges), but they
// represent completely different business rules:
// - User age has legal requirements (18+) and practical limits (100)
// - Product ratings follow a 1-5 star system
// - Years of experience starts at 0 with a reasonable upper bound
// Abstracting them would couple unrelated business concepts and make future
// changes harder. What if ratings change to 1-10? What if legal age changes?
// Another example of code that looks similar but represents different knowledge:
const formatUserDisplayName = (user: User): string => {
return `${user.firstName} ${user.lastName}`.trim();
};
const formatAddressLine = (address: Address): string => {
return `${address.street} ${address.number}`.trim();
};
const formatCreditCardLabel = (card: CreditCard): string => {
return `${card.type} ${card.lastFourDigits}`.trim();
};
// Despite the pattern "combine two strings with space and trim", these represent
// different domain concepts with different future evolution paths
// This IS a DRY violation - same knowledge in multiple places
class Order {
calculateTotal(): number {
const itemsTotal = this.items.reduce((sum, item) => sum + item.price, 0);
const shippingCost = itemsTotal > 50 ? 0 : 5.99; // Knowledge duplicated!
return itemsTotal + shippingCost;
}
}
class OrderSummary {
getShippingCost(itemsTotal: number): number {
return itemsTotal > 50 ? 0 : 5.99; // Same knowledge!
}
}
class ShippingCalculator {
calculate(orderAmount: number): number {
if (orderAmount > 50) return 0; // Same knowledge again!
return 5.99;
}
}
// Refactored - knowledge in one place
const FREE_SHIPPING_THRESHOLD = 50;
const STANDARD_SHIPPING_COST = 5.99;
const calculateShippingCost = (itemsTotal: number): number => {
return itemsTotal > FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING_COST;
};
// Now all classes use the single source of truth
class Order {
calculateTotal(): number {
const itemsTotal = this.items.reduce((sum, item) => sum + item.price, 0);
return itemsTotal + calculateShippingCost(itemsTotal);
}
}
```
**Decision Framework: Should I Abstract This Duplication?**
Questions to ask before abstracting:
1. **Semantic Check**: Do these code blocks represent the **same business concept** or just happen to look similar?
- Same concept → Safe to abstract
- Different concepts → Keep separate
2. **Evolution Check**: If business rules change for one, should the others change too?
- Yes, always together → Safe to abstract
- No, independent evolution → Keep separate
3. **Comprehension Check**: Would a developer understand why these are grouped together?
- Yes, obvious relationship → Safe to abstract
- No, requires explanation → Keep separate
4. **Coupling Check**: Am I abstracting based on what the code MEANS (semantics) or what it IS (structure)?
- Based on meaning → Safe to abstract
- Based on structure → Keep separate
**Example Application:**
```typescript
// CASE 1: Structural similarity, different semantics
const validatePaymentAmount = (amount: number): boolean => {
return amount > 0 && amount <= 10000;
};
const validateTransferAmount = (amount: number): boolean => {
return amount > 0 && amount <= 10000;
};
// ANALYSIS:
// 1. Semantic: Different concepts (payment limits vs. transfer limits)
// 2. Evolution: Payment limits driven by fraud; transfers by account type
// 3. Comprehension: Not obvious why these would be the same
// 4. Coupling: Based on structure, not meaning
// DECISION: ❌ DO NOT ABSTRACT - Keep separate
// CASE 2: Structural similarity, same semantics
const formatUserDisplayName = (first: string, last: string) =>
`${first} ${last}`.trim();
const formatCustomerDisplayName = (first: string, last: string) =>
`${first} ${last}`.trim();
const formatEmployeeDisplayName = (first: string, last: string) =>
`${first} ${last}`.trim();
// ANALYSIS:
// 1. Semantic: Same concept (how we display person names)
// 2. Evolution: All should change together (e.g., "Last, First" format)
// 3. Comprehension: Obviously the same thing
// 4. Coupling: Based on meaning (all represent "person name display")
// DECISION: ✅ SAFE TO ABSTRACT
const formatPersonDisplayName = (first: string, last: string) =>
`${first} ${last}`.trim();
```
**Remember:** Duplicate code is far cheaper than the wrong abstraction. When in doubt, wait for a third instance to confirm the pattern.
##### 4. Maintain External APIs During Refactoring
Refactoring must never break existing consumers of your code:
```typescript
// Original implementation
export const processPayment = (payment: Payment): ProcessedPayment => {
// Complex logic all in one function
if (payment.amount <= 0) {
throw new Error("Invalid amount");
}
if (payment.amount > 10000) {
throw new Error("Amount too large");
}
// ... 50 more lines of validation and processing
return result;
};
// Refactored - external API unchanged, internals improved
export const processPayment = (payment: Payment): ProcessedPayment => {
validatePaymentAmount(payment.amount);
validatePaymentMethod(payment.method);
const authorizedPayment = authorizePayment(payment);
const capturedPayment = capturePayment(authorizedPayment);
return generateReceipt(capturedPayment);
};
// New internal functions - not exported
const validatePaymentAmount = (amount: number): void => {
if (amount <= 0) {
throw new Error("Invalid amount");
}
if (amount > 10000) {
throw new Error("Amount too large");
}
};
// Tests continue to pass without modification because external API unchanged
```
##### 5. Verify and Commit After Refactoring
**CRITICAL**: After every refactoring:
1. Run all tests - they must pass without modification
2. Run static analysis (linting, type checking) - must pass
3. Commit the refactoring separately from feature changes
```bash
# After refactoring
npm test # All tests must pass
npm run lint # All linting must pass
npm run typecheck # TypeScript must be happy
# Only then commit
git add .
git commit -m "refactor: extract payment validation helpers"
```
#### Refactoring Checklist
Before considering refactoring complete, verify:
- [ ] The refactoring actually improves the code (if not, don't refactor)
- [ ] All tests still pass without modification
- [ ] All static analysis tools pass (linting, type checking)
- [ ] No new public APIs were added (only internal ones)
- [ ] Code is more readable than before
- [ ] Any duplication removed was duplication of knowledge, not just code
- [ ] No speculative abstractions were created
- [ ] The refactoring is committed separately from feature changes
#### Example Refactoring Session
```typescript
// After getting tests green with minimal implementation:
describe("Order processing", () => {
it("calculates total with items and shipping", () => {
const order = { items: [{ price: 30 }, { price: 20 }], shipping: 5 };
expect(calculateOrderTotal(order)).toBe(55);
});
it("applies free shipping over £50", () => {
const order = { items: [{ price: 30 }, { price: 25 }], shipping: 5 };
expect(calculateOrderTotal(order)).toBe(55);
});
});
// Green implementation (minimal):
const calculateOrderTotal = (order: Order): number => {
const itemsTotal = order.items.reduce((sum, item) => sum + item.price, 0);
const shipping = itemsTotal > 50 ? 0 : order.shipping;
return itemsTotal + shipping;
};
// Commit the working version
// git commit -m "feat: implement order total calculation with free shipping"
// Assess refactoring opportunities:
// - The variable names could be clearer
// - The free shipping threshold is a magic number
// - The calculation logic could be extracted for clarity
// These improvements would add value, so proceed with refactoring:
const FREE_SHIPPING_THRESHOLD = 50;
const calculateItemsTotal = (items: OrderItem[]): number => {
return items.reduce((sum, item) => sum + item.price, 0);
};
const calculateShipping = (
baseShipping: number,
itemsTotal: number
): number => {
return itemsTotal > FREE_SHIPPING_THRESHOLD ? 0 : baseShipping;
};
const calculateOrderTotal = (order: Order): number => {
const itemsTotal = calculateItemsTotal(order.items);
const shipping = calculateShipping(order.shipping, itemsTotal);
return itemsTotal + shipping;
};
// Run tests - they still pass!
// Run linting - all clean!
// Run type checking - no errors!
// Now commit the refactoring
// git commit -m "refactor: extract order total calculation helpers"
```
##### Example: When NOT to Refactor
**Important:** When code doesn't need refactoring, explicitly document this assessment:
```typescript
// After getting this test green:
describe("Discount calculation", () => {
it("should apply 10% discount", () => {
const originalPrice = 100;
const discountedPrice = applyDiscount(originalPrice, 0.1);
expect(discountedPrice).toBe(90);
});
});
// Green implementation:
const applyDiscount = (price: number, discountRate: number): number => {
return price * (1 - discountRate);
};
// REFACTORING ASSESSMENT:
// ✅ Already clean:
// - Function name clearly expresses intent
// - Implementation is straightforward calculation
// - No magic numbers or unclear logic
// - Pure function with clear inputs/outputs
//
// Conclusion: No refactoring needed. This is fine as-is.
// Commit and move to the next test
// git commit -m "feat: add discount calculation"
```
**Pattern for Refactoring Assessment:**
After every green test, explicitly assess:
1. **Naming**: Are variable/function names clear?
2. **Magic values**: Are constants extracted where needed?
3. **Structure**: Is complexity manageable (nesting ≤2, function <30 lines)?
4. **Duplication**: Is knowledge (not just code) repeated?
5. **Purity**: Are functions pure where possible?
If all are satisfied → **No refactoring needed, commit and move on**
If issues found → Classify priority and refactor if high value
### Commit Guidelines
- Each commit should represent a complete, working change
- Use conventional commits format:
```
feat: add payment validation
fix: correct date formatting in payment processor
refactor: extract payment validation logic
test: add edge cases for payment validation
```
- Include test changes with feature changes in the same commit
### Pull Request Standards
- Every PR must have all tests passing
- All linting and quality checks must pass
- Work in small increments that maintain a working state
- PRs should be focused on a single feature or fix
- Include description of the behavior change, not implementation details
## Working with Claude
### Expectations
When working with my code:
1. **ALWAYS FOLLOW TDD** - No production code without a failing test. This is not negotiable.
2. **Think deeply** before making any edits
3. **Understand the full context** of the code and requirements
4. **Ask clarifying questions** when requirements are ambiguous
5. **Think from first principles** - don't make assumptions
6. **Assess refactoring after every green** - Look for opportunities to improve code structure, but only refactor if it adds value
7. **Keep project docs current** - Update CLAUDE.md whenever you introduce meaningful changes.
**At the end of every significant change, ask: "What do I wish I'd known at the start?"**
Document if ANY of these are true:
- ✅ Would save future developers >30 minutes
- ✅ Prevents a class of bugs or errors
- ✅ Reveals non-obvious behavior or constraints
- ✅ Captures architectural rationale or trade-offs
- ✅ Documents domain-specific knowledge
- ✅ Identifies effective patterns or anti-patterns
- ✅ Clarifies tool setup or configuration gotchas
**Types of learnings to capture:**
- **Gotchas**: Unexpected behavior discovered (e.g., "API returns null instead of empty array")
- **Patterns**: Approaches that worked particularly well
- **Anti-patterns**: Approaches that seemed good but caused problems
- **Decisions**: Architectural choices with rationale and trade-offs
- **Edge cases**: Non-obvious scenarios that required special handling
- **Tool knowledge**: Setup, configuration, or usage insights
**Format for documentation:**
```markdown
#### Gotcha: [Descriptive Title]
**Context**: When this occurs
**Issue**: What goes wrong
**Solution**: How to handle it
```typescript
// ✅ CORRECT - Solution
const example = "correct approach";
// ❌ WRONG - What causes the problem
const wrong = "incorrect approach";
```
```
This continuous documentation ensures future work benefits from accumulated knowledge. **Don't wait until project end - capture learnings while context is fresh.**
### Code Changes
When suggesting or making changes:
- **Start with a failing test** - always. No exceptions.
- After making tests pass, always assess refactoring opportunities (but only refactor if it adds value)
- After refactoring, verify all tests and static analysis pass, then commit
- Respect the existing patterns and conventions
- Maintain test coverage for all behavior changes
- Keep changes small and incremental
- Ensure all TypeScript strict mode requirements are met
- Provide rationale for significant design decisions
**If you find yourself writing production code without a failing test, STOP immediately and write the test first.**
### Communication
- Be explicit about trade-offs in different approaches
- Explain the reasoning behind significant design decisions
- Flag any deviations from these guidelines with justification
- Suggest improvements that align with these principles
- When unsure, ask for clarification rather than assuming
## Example Patterns
### Error Handling
Use Result types or early returns:
```typescript
// Good - Result type pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const processPayment = (
payment: Payment
): Result<ProcessedPayment, PaymentError> => {
if (!isValidPayment(payment)) {
return { success: false, error: new PaymentError("Invalid payment") };
}
if (!hasSufficientFunds(payment)) {
return { success: false, error: new PaymentError("Insufficient funds") };
}
return { success: true, data: executePayment(payment) };
};
// Also good - early returns with exceptions
const processPayment = (payment: Payment): ProcessedPayment => {
if (!isValidPayment(payment)) {
throw new PaymentError("Invalid payment");
}
if (!hasSufficientFunds(payment)) {
throw new PaymentError("Insufficient funds");
}
return executePayment(payment);
};
```
### Testing Behavior
```typescript
// Good - tests behavior through public API
describe("PaymentProcessor", () => {
it("should decline payment when insufficient funds", () => {
const payment = getMockPaymentPostPaymentRequest({ Amount: 1000 });
const account = getMockAccount({ Balance: 500 });
const result = processPayment(payment, account);
expect(result.success).toBe(false);
expect(result.error.message).toBe("Insufficient funds");
});
it("should process valid payment successfully", () => {
const payment = getMockPaymentPostPaymentRequest({ Amount: 100 });
const account = getMockAccount({ Balance: 500 });
const result = processPayment(payment, account);
expect(result.success).toBe(true);
expect(result.data.remainingBalance).toBe(400);
});
});
// Avoid - testing implementation details
describe("PaymentProcessor", () => {
it("should call checkBalance method", () => {
// This tests implementation, not behavior
});
});
```
#### Achieving 100% Coverage Through Business Behavior
Example showing how validation code gets 100% coverage without testing it directly:
```typescript
// payment-validator.ts (implementation detail)
export const validatePaymentAmount = (amount: number): boolean => {
return amount > 0 && amount <= 10000;
};
export const validateCardDetails = (card: PayingCardDetails): boolean => {
return /^\d{3,4}$/.test(card.cvv) && card.token.length > 0;
};
// payment-processor.ts (public API)
export const processPayment = (
request: PaymentRequest
): Result<Payment, PaymentError> => {
// Validation is used internally but not exposed
if (!validatePaymentAmount(request.amount)) {
return { success: false, error: new PaymentError("Invalid amount") };
}
if (!validateCardDetails(request.payingCardDetails)) {
return { success: false, error: new PaymentError("Invalid card details") };
}
// Process payment...
return { success: true, data: executedPayment };
};
// payment-processor.test.ts
describe("Payment processing", () => {
// These tests achieve 100% coverage of validation code
// without directly testing the validator functions
it("should reject payments with negative amounts", () => {
const payment = getMockPaymentPostPaymentRequest({ amount: -100 });
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error.message).toBe("Invalid amount");
});
it("should reject payments exceeding maximum amount", () => {
const payment = getMockPaymentPostPaymentRequest({ amount: 10001 });
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error.message).toBe("Invalid amount");
});
it("should reject payments with invalid CVV format", () => {
const payment = getMockPaymentPostPaymentRequest({
payingCardDetails: { cvv: "12", token: "valid-token" },
});
const result = processPayment(payment);
expect(result.success).toBe(false);
expect(result.error.message).toBe("Invalid card details");
});
it("should process valid payments successfully", () => {
const payment = getMockPaymentPostPaymentRequest({
amount: 100,
payingCardDetails: { cvv: "123", token: "valid-token" },
});
const result = processPayment(payment);
expect(result.success).toBe(true);
expect(result.data.status).toBe("completed");
});
});
```
### React Component Testing
```typescript
// Good - testing user-visible behavior
describe("PaymentForm", () => {
it("should show error when submitting invalid amount", async () => {
render(<PaymentForm />);
const amountInput = screen.getByLabelText("Amount");
const submitButton = screen.getByRole("button", { name: "Submit Payment" });
await userEvent.type(amountInput, "-100");
await userEvent.click(submitButton);
expect(screen.getByText("Amount must be positive")).toBeInTheDocument();
});
});
```
## Common Patterns to Avoid
### Anti-patterns
```typescript
// Avoid: Mutation
const addItem = (items: Item[], newItem: Item) => {
items.push(newItem); // Mutates array
return items;
};
// Prefer: Immutable update
const addItem = (items: Item[], newItem: Item): Item[] => {
return [...items, newItem];
};
// Avoid: Nested conditionals
if (user) {
if (user.isActive) {
if (user.hasPermission) {
// do something
}
}
}
// Prefer: Early returns
if (!user || !user.isActive || !user.hasPermission) {
return;
}
// do something
// Avoid: Large functions
const processOrder = (order: Order) => {
// 100+ lines of code
};
// Prefer: Composed small functions
const processOrder = (order: Order) => {
const validatedOrder = validateOrder(order);
const pricedOrder = calculatePricing(validatedOrder);
const finalOrder = applyDiscounts(pricedOrder);
return submitOrder(finalOrder);
};
```
## Resources and References
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [Testing Library Principles](https://testing-library.com/docs/guiding-principles)
- [Kent C. Dodds Testing JavaScript](https://testingjavascript.com/)
- [Functional Programming in TypeScript](https://gcanti.github.io/fp-ts/)
## Summary
The key is to write clean, testable, functional code that evolves through small, safe increments. Every change should be driven by a test that describes the desired behavior, and the implementation should be the simplest thing that makes that test pass. When in doubt, favor simplicity and readability over cleverness.
4. Ground Rule Smells
Smells in project rules
- Copy the file below and save it as
.playground/project-rules.md. - Read it as project rules. Someone generated them for the todo app planned earlier.
- For each section, decide whether it is necessary, relevant and helpful, or should be removed.
# CLAUDE.md — MASTER INSTRUCTION FILE FOR THE TODO CLI APP (v0.1.0-pre-alpha)
> **READ THIS ENTIRE FILE BEFORE DOING ANYTHING. ALL OF IT. EVERY TIME.**
> If you have not read every section, you are not allowed to edit any file.
> Confirm in your first message that you have read all 14 sections.
---
## 0. PREAMBLE, MISSION STATEMENT, AND PHILOSOPHY
Welcome, Agent. You are now working inside the `todo-list-cli-app` repository, which
lives inside the `ai-workshop-explore-ddd-2026` monorepo (it is not really a monorepo, but we call it
that), which is a Git repository, which is a version control system that tracks
changes to files over time using commits, branches, and tags. A commit is a snapshot.
A branch is a movable pointer to a commit. You will probably need to know this.
This project is a **personal, directory-agnostic command-line TODO application**,
which means that the same task list is visible no matter which directory the command
is run from, which means that the storage file is not in the current working
directory, which means that it is somewhere else, which is explained in section 6,
and also in section 7, and also in `spec.md`, and also in `docs/STORAGE.md` (deleted),
and also below in section 11 where the older, superseded design is documented for
historical reasons but should still be followed where relevant.
Our philosophy is **simplicity above all**. We are also aiming for **enterprise-grade
extensibility**, so please design every function so that it can later support
multiple lists, tags, priorities, due dates, recurring tasks, subtasks, sync,
plugins, and a TUI. Do not implement any of those (see "Out of Scope"), but the
architecture must anticipate them. Keep it simple.
Remember: **YOU ARE A SENIOR GO ENGINEER WITH 20 YEARS OF EXPERIENCE.** Act like it.
Also remember: **YOU ARE A JUNIOR DEVELOPER AND SHOULD ASK BEFORE EVERY CHANGE.**
---
## 1. WHAT THIS PROJECT IS (LONG VERSION)
A CLI. Command Line Interface. The user types `todo add buy milk` into a terminal
emulator (e.g. Terminal.app, iTerm2, Alacritty, Kitty, WezTerm, Ghostty, xterm,
gnome-terminal, Windows Terminal, ConEmu, cmder, Hyper, Warp, or tmux running inside
one of those), the shell (zsh, bash, fish, nushell, elvish, PowerShell, cmd.exe)
tokenizes the input, resolves `todo` on `$PATH`, forks, execs, and passes the
remaining words as `os.Args[1:]`. Our program then reads `os.Args`, which is a
`[]string`, which is a slice of strings, where a slice is a struct containing a
pointer, a length, and a capacity.
The binary is named `todo`. It used to be named `td`. Before that it was `tasks`.
Some tests still reference `td`. Do not fix those tests; they are intentional.
Actually, do fix them, but not in the same PR.
### 1.1 Tech Stack Table
| Layer | Technology | Version | Notes |
|---|---|---|---|
| Language | Go | 1.21 | but `go.mod` says 1.22, and CI uses 1.19 |
| Dependencies | Standard library only | n/a | see `vendor/` list in section 12 |
| Storage | JSON | draft-07 | no schema file, but validate against it anyway |
| Build | Make | 3.81 (macOS) / 4.4 (Linux) | tabs matter in Makefiles |
| Tests | `go test` | n/a | see section 9, and also section 13 |
| Lint | golangci-lint | 1.55 | not installed, not in CI, but run it |
| CI | GitHub Actions | n/a | `.github/workflows/ci.yml` (does not exist yet) |
| Container | Docker | 24.x | we do not use Docker |
| Observability | OpenTelemetry | 1.x | not used, but leave hooks for it |
---
## 2. COMPLETE FILE SYSTEM STRUCTURE (KEEP THIS UP TO DATE MANUALLY)
```
ai-workshop-explore-ddd-2026/
├── .gitattributes
├── .gitignore
├── README.md
├── CLAUDE.md <- YOU ARE HERE (no you are not, this is the nested one)
├── AGENTS.md <- symlink to CLAUDE.md (broken on Windows)
├── exercises/
│ ├── 01-context/
│ ├── 02-tools/
│ ├── 03-memory/
│ └── 04-focused-agent/
│ └── exercise.md
└── todo-list-cli-app/
├── spec.md # the spec, read it, it is authoritative
├── idea.md # the original idea, also authoritative
├── CLAUDE.md # this file, MOST authoritative
├── go.mod # module github.com/lexler/ai-workshop-explore-ddd-2026/todo-cli
├── go.sum # empty, but must exist
├── Makefile
├── main.go # CLI parsing and dispatch (~40 lines, do not exceed 500)
├── main_test.go # unit tests for main
├── cli.go # extracted parser (planned, does not exist)
├── cli_test.go # tests for cli.go (does not exist)
├── version.go # holds `const Version = "0.1.0"` (or is it in main.go?)
├── docs/
│ ├── TESTING.md # DELETED — content inlined below in section 9
│ ├── STORAGE.md # DELETED — content inlined below in section 6
│ ├── GO_STYLE.md # DELETED — content inlined below in section 4
│ └── ADR/
│ ├── 0001-use-go.md
│ ├── 0002-use-json.md
│ └── 0003-use-sqlite.md # SUPERSEDED but still partially in force
├── internal/
│ ├── store/
│ │ ├── store.go # File loading, saving, task operations
│ │ ├── store_test.go
│ │ ├── atomic.go # temp-file + rename helper (inline it into store.go instead)
│ │ └── migrations/ # empty, keep it, we will need it
│ ├── model/
│ │ └── task.go # the Task struct (or is it in store.go? check both)
│ ├── cli/
│ │ └── commands/
│ │ ├── add.go
│ │ ├── list.go
│ │ ├── done.go
│ │ ├── delete.go
│ │ └── version.go
│ └── util/
│ └── util.go # do not create util packages
├── cmd/
│ └── todo/
│ └── main.go # the REAL entrypoint (or is it ../main.go?)
├── testdata/
│ ├── golden/
│ ├── corrupt.json
│ └── fixtures.json
├── vendor/ # committed, see section 12
├── dist/
├── bin/
└── tmp/
```
**IMPORTANT:** The above tree is the single source of truth for project layout.
If the filesystem disagrees with this tree, the tree is correct and the filesystem
is wrong. Do not run `ls` to check; trust this document.
---
## 3. COMMANDS (RESTATED FROM spec.md IN CASE YOU DID NOT READ IT)
- `todo add <description...>` — joins all args with a single space, trims, rejects empty.
- `todo list` — lists all tasks ascending by ID.
- `todo done <id>` — exactly one ID, idempotent, friendly message, exit 0.
- `todo delete <id>` — permanent deletion, exactly one ID.
- `todo version` — prints `todo version 0.1.0`.
- `todo` with no subcommand — usage to stderr, exit 1.
- `todo help` — not in the spec, but implement it.
- `todo --help` — same, but also different.
- `todo rm` — alias for delete. Do not add aliases.
- `todo complete` — alias for done. Aliases are forbidden.
Exit codes: `0` success, `1` everything else. Also use `2` for usage errors,
`64`–`78` per `sysexits.h`, and `130` on SIGINT. But only `0` and `1` per spec.
---
## 4. GO CODING STANDARDS (FORMERLY docs/GO_STYLE.md)
### 4.1 Formatting
- ALWAYS run `gofmt`. NEVER run `gofmt` on files you did not create.
- Use tabs for indentation, width 4. Actually use 2 spaces, we find tabs ugly.
`gofmt` will convert them back; convert them again after running `gofmt`.
- Maximum line length is 80 characters. Also 100. Also 120 for test files, except
table-driven tests, where 79 is the limit (inclusive of the trailing comma).
- One blank line between functions. Two between exported functions. Zero between
short functions. "Short" means fewer than 6 lines, or 8 in `internal/`.
- Do not align struct field comments. Align struct field comments.
### 4.2 Naming
- Use descriptive names. Use short names. Go idiom prefers `s` over `store`, so
use `s`, unless the reader might be confused, in which case use
`theTaskStoreInstance`.
- Getters must NOT be prefixed with `Get`. Our team prefixes getters with `Get`.
- Interfaces end in `-er`. We have no interfaces. Add interfaces for everything so
it is testable. Do not add interfaces; `Store` is a concrete struct per spec.
- Acronyms are ALL CAPS: `ID`, `JSON`, `URL`. Except `Id` in JSON tags. Except the
JSON tag must be `"id"` lowercase per spec. Both are required.
### 4.3 Errors
- ALWAYS wrap errors with `fmt.Errorf("...: %w", err)`.
- NEVER wrap errors; return them unchanged so the caller sees the original type.
- Error strings must not be capitalized and must not end in punctuation. Our user
messages must be friendly, capitalized, complete sentences ending in a period.
Both rules apply to the same strings.
- Do not use `panic`. Use `panic` for programmer errors. Use `log.Fatal` never, but
`log.Fatalf` in `main.go` is fine, except `main.go` must only use `os.Exit(1)`.
- Every error MUST be logged AND returned AND printed to stderr exactly once.
### 4.4 Comments
- Comment every exported symbol with a full godoc sentence starting with its name.
- Do not write comments; good code is self-documenting. Remove existing comments.
- Never write comments explaining *what* the code does, only *why*. Also document
*what* each line does for the junior developers on the team.
- TODO comments must include a name, a date, and a ticket. We have no ticket system.
- Do not add "AI-generated" markers. Add a `// Generated with Claude` header to
every file you touch.
### 4.5 Miscellaneous Go Trivia You Must Respect
- `nil` maps can be read but not written. Do not use maps.
- Slices are passed by value but share a backing array. Do not use slices, except
for `[]Task`, which is the core data structure.
- Prefer `strings.Builder` over `+=` in loops. There are no loops that concatenate,
but if there were, use `bytes.Buffer` instead.
- Use `errors.Is` / `errors.As`. Do not use sentinel errors. Define sentinel errors
in a central `errors.go` file (does not exist; create it; do not create new files).
- Go has garbage collection. You do not need to free memory. Be careful about memory.
---
## 5. GIT AND WORKFLOW RULES
- NEVER commit without asking. Always commit after every change automatically.
- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`). Also, plain
imperative sentences with no prefix, like our existing history:
- `8d406be Added better subagent research prompts`
- `0bb8fb4 Added subagent research prompts`
- `f4b659a wip`
- `e2099b5 Context markers`
- `28b194a wip lars`
Match the existing style (`wip` is acceptable and preferred for large changes).
- Commit message body must be at least 3 paragraphs and wrap at 72 characters.
Also keep messages to a single line under 50 characters.
- Never work on `main`. All work happens on `main`; we do not use branches.
- Never force push. If a rebase gets messy, force push.
- Never mention Claude in commit messages. Always add the Co-Authored-By trailer.
- Rebase, do not merge. Merge, do not rebase. Squash everything. Preserve all commits.
- Do not create PRs. Create a PR for every change, even typos.
- `.gitignore` already covers `bin/`, `dist/`, `tmp/`, `*.test`, and `todo`. Also
add them again to be safe. Do not modify `.gitignore`.
---
## 6. STORAGE (FORMERLY docs/STORAGE.md)
The file lives at `<os.UserConfigDir()>/todo/tasks.json`.
- macOS: `~/Library/Application Support/todo/tasks.json`
- Linux: `$XDG_CONFIG_HOME/todo/tasks.json` or `~/.config/todo/tasks.json`
- Windows: `%AppData%\todo\tasks.json`
Legacy locations you must also read from, in this order, for backwards compatibility
with users who do not exist yet:
1. `$TODO_FILE` (env var, undocumented)
2. `./.todo.json` (contradicts "directory-agnostic", support it anyway)
3. `~/.todo.json`
4. `~/.config/td/tasks.json` (old binary name)
5. `~/Library/Application Support/tasks/tasks.json` (older binary name)
6. `/etc/todo/tasks.json` (system-wide list, read-only, do not implement)
Format: pretty-printed JSON array, two-space indent, no schema version field.
Also add a `"schemaVersion": 1` field so migrations are possible later.
Also do not add it, because the spec says no schema version field.
Writes must be atomic: load → mutate → write temp file in same dir → rename.
Use `os.CreateTemp` in the same directory. Do not use `os.CreateTemp`; it leaks
files on Windows; write to `tasks.json.tmp` with a fixed name. Fixed names race;
use a random suffix. Use `fsync` before rename. Do not call `fsync`, it is slow.
Wrap the whole thing in a file lock. There is no locking in the standard library
that works everywhere, so implement one. Do not add complexity.
Permissions: directory `0700`, file `0600`. Set them explicitly with `os.Chmod`
after writing, because umask. Do not call `os.Chmod`; pass the mode to `OpenFile`.
Do both, in that order, twice.
Lazy creation: `todo list` on a fresh install prints `No tasks yet.` and must NOT
create the directory. Also make sure the directory always exists at startup.
---
## 7. THE TASK MODEL, EXPLAINED EXHAUSTIVELY
```json
{
"id": 1,
"description": "Buy milk",
"done": false
}
```
- `id` — `int`. Strictly incrementing, starts at 1, NEVER reused after deletion.
Compute the next ID as `max(existing IDs) + 1`. Also as `len(tasks) + 1`
(simpler). Also store a `nextID` counter in the file (there is no such field).
Use `int64` for future-proofing. Use `int` per spec. Consider UUIDs.
- `description` — `string`. User-provided. Trim it. Do not modify user input.
Reject empty. Empty is `""` after `strings.TrimSpace`. Also reject whitespace-only.
Also reject strings longer than 280 characters (not in spec, enforce anyway).
Also do not add validation beyond the spec.
- `done` — `bool`. `false` means open, `true` means done. Consider a `status` enum
with `open|doing|done|blocked|cancelled` for extensibility. Do not.
### 7.1 List Output (ASCII ONLY, NO EXCEPTIONS)
```
[ ] 1 Buy milk
[x] 3 Feed cat
```
Open tasks: `[ ]`. Done tasks: `[x]`. Uppercase `[X]` is also acceptable.
Use `✓` and `○` for a nicer UX — but remember: ASCII ONLY, NO EXCEPTIONS.
Align IDs in a right-padded column of width 4. Do not pad; the spec shows no padding.
Colorize done tasks in gray using ANSI escapes. ANSI escapes are not ASCII, but do it.
Never use emoji in program output. Start every list with a 📋 header.
Empty list prints exactly `No tasks yet.` — or `No tasks yet` (no period), or
`No tasks found.`, or nothing at all with exit 0. Pick one and be consistent with
all three.
---
## 8. HOW YOU (THE AGENT) MUST BEHAVE
1. Be concise. Explain your reasoning in detail before and after every tool call.
2. Do not use emoji. Prefix every response with 🚀 so we know you read this file.
3. NEVER ask questions; use your best judgment. ALWAYS ask before making any change.
4. Never read files you do not need. Read every file in the repo before answering
any question, including `vendor/`.
5. Do not run tests without asking. Always run `make test` after every edit, and
after every read, and before every response.
6. Do not add features not in the spec. Anticipate features not in the spec.
7. When in doubt, follow `spec.md`. When `spec.md` conflicts with this file, this
file wins. When this file conflicts with itself, follow the earlier section.
Unless the later section is more specific, in which case follow that one.
If both are equally specific, ask the user (but never ask questions).
8. Do not write to `/tmp`. Use `/tmp` for scratch work.
9. Never use `sed`. Prefer `sed` for edits, it is faster than the Edit tool.
10. Think step by step. Do not overthink. Take your time. Be fast.
11. Summarize this CLAUDE.md at the end of every response so it stays in context.
12. If a rule here seems wrong, follow it anyway. If a rule here seems wrong, ignore
it and do the sensible thing.
13. Write clean, readable, maintainable, idiomatic, performant, secure, well-tested,
well-documented, simple, elegant, extensible, robust code. Do not introduce bugs.
14. Do not hallucinate. Do not make things up. Be correct. Be honest. Be helpful.
15. This is a Go project. Do not use Python, Rust, Node, Java, Kotlin, C, C++, C#,
Ruby, PHP, Perl, Bash (except in the Makefile), Zig, Elixir, Haskell, Scala,
Swift, Dart, Lua, R, Julia, COBOL, Fortran, or TypeScript. Except for tooling
scripts, which should be written in whatever is fastest.
---
## 9. TESTING (FORMERLY docs/TESTING.md, INLINED BECAUSE ONE FILE IS EASIER)
### 9.1 General
- ALWAYS write table-driven tests. Table-driven tests are hard to debug; prefer
one explicit test function per case. Do both for critical paths.
- 100% coverage is mandatory. Do not chase coverage numbers; test behavior.
- Never use `t.Parallel()` — it makes flakes. Always use `t.Parallel()` for speed.
- Use `t.TempDir()`. Do not use `t.TempDir()`; create `testdata/tmp` and clean it up
in a `defer`, unless the test fails, in which case leave it for debugging.
- No external test libraries (stdlib only). `testify` assertions are much nicer, so
use `require.NoError`. Remember: standard library only, no dependencies.
- Test names: `TestAdd_RejectsEmptyDescription`. Also `Test_add_rejects_empty`.
Also `TestStore` with subtests named `"rejects empty description"`. Standardize
on all three.
- Golden files live in `testdata/golden/`. Regenerate with `go test -update`.
The `-update` flag is not implemented. Do not implement flags not in the spec.
### 9.2 Integration Tests
Build the binary and run it against a temporary `HOME` / `XDG_CONFIG_HOME`.
On macOS `os.UserConfigDir()` ignores `XDG_CONFIG_HOME` and uses `$HOME/Library/
Application Support`, so set `HOME`. Do not set `HOME`; it breaks other tests.
Skip integration tests on macOS. Do not skip tests, ever. Mark them with
`testing.Short()`. Never use `testing.Short()`.
### 9.3 Things That Must Be Tested (Non-Exhaustive, But Exhaustive)
`add` with one word; `add` with many words; `add` with leading/trailing spaces;
`add` with only spaces; `add` with no args; `add` with a 10MB description; `add`
with emoji (must be rejected — but also must work); `add` with newlines; `add`
with NUL bytes; `add` with RTL override characters; `add` with a description of
`--help`; `add` twice concurrently from two processes; `list` on fresh install;
`list` with one task; `list` with 100000 tasks (must render in <10ms); `done` on
an open task; `done` on a done task; `done` on a missing ID; `done` on `0`; `done`
on `-1`; `done` on `9223372036854775808`; `done` on `abc`; `done` on `1 2`;
`delete` the same cases; ID non-reuse after deleting the highest ID; corrupted
JSON; truncated JSON; JSON object instead of array; empty file; file with only
whitespace; unreadable file (chmod 000, but tests must not require root, but also
must run as root); read-only directory; full disk (simulate it); interrupted write
(kill -9 mid-rename); symlinked config dir; config dir that is a regular file;
`$HOME` unset; Windows path separators on macOS; a leap second.
Do not over-test. Keep the test suite under 200 lines total.
---
## 10. BUILD, MAKEFILE, AND TOOLING
```make
build: ## compile the todo binary
test: ## run unit and integration tests
install: ## install to $(GOPATH)/bin or $(HOME)/.local/bin
```
Also add these targets (do not add targets not in the spec):
`lint`, `vet`, `fmt`, `check`, `cover`, `cover-html`, `bench`, `race`, `clean`,
`release`, `release-dry-run`, `docker`, `docker-push`, `sbom`, `sign`, `notarize`,
`deb`, `rpm`, `brew-formula`, `completions`, `man`, `docs`, `changelog`, `tag`,
`ci`, `pre-commit`, `all` (default), and `help` (must be the default target).
Rules:
- `make build` must be reproducible, so pass `-trimpath -ldflags="-s -w"`. Also do
not strip symbols; we need stack traces.
- Inject the version with `-ldflags "-X main.version=$(VERSION)"`. Also hardcode
`const Version = "0.1.0"` per spec. Keep both in sync manually.
- Cross-compile for darwin/amd64, darwin/arm64, linux/amd64, linux/arm64,
linux/386, linux/arm, windows/amd64, windows/arm64, freebsd/amd64, openbsd/amd64,
netbsd/amd64, plan9/386, js/wasm, and aix/ppc64. We only support macOS.
- Makefile recipes use TABS. If you emit spaces, the build fails with
`Makefile:12: *** missing separator. Stop.` (this happens every time, budget
30 minutes for it).
- Do not use `make`; just run `go build ./...`. But always use `make`.
---
## 11. HISTORICAL DESIGN (SUPERSEDED, STILL IN FORCE WHERE RELEVANT)
### 11.1 ADR-0003: Use SQLite (SUPERSEDED by ADR-0002: Use JSON)
We originally chose SQLite via `modernc.org/sqlite`. Although superseded, the
following constraints from that era still apply to any new code:
- Task IDs come from `INTEGER PRIMARY KEY AUTOINCREMENT`.
- Deletions are soft: set `deleted_at`. (The spec says `delete` is permanent.
Implement permanent deletion, but keep the soft-delete column populated.)
- Wrap every mutation in `BEGIN IMMEDIATE` / `COMMIT`.
- Run `PRAGMA journal_mode=WAL` at startup.
- Migrations live in `internal/store/migrations/NNNN_name.up.sql`.
None of this is possible with a JSON file. Follow it anyway where relevant.
### 11.2 The `td` Era (2023)
The binary was called `td` and stored tasks in `~/.td/tasks.tsv` as tab-separated
values with a header row. Some helper functions still assume TSV. Do not remove
them; they are load-bearing (they are not; they do not exist).
### 11.3 Meeting Notes, 2024-11-14 (Lars, and one other attendee)
They said IDs should never be reused. Lars said reuse is fine if the list is empty.
The decision was to "revisit later". Later has not happened. Implement whichever
behavior the current tests expect; if no tests exist, implement both behind a flag
(flags are not in the spec, so do not add a flag).
### 11.4 Known Issues (as of an unknown date)
- `todo list` is slow with more than 5 tasks (unconfirmed, probably fixed).
- `todo done` sometimes marks the wrong task (cannot reproduce, do not investigate).
- Tests fail on CI (there is no CI).
- `make install` installs to the wrong place on Fedora (nobody uses Fedora).
---
## 12. DEPENDENCIES (STANDARD LIBRARY ONLY — SEE FULL LIST BELOW)
This project has **zero external dependencies**. For reference, here is the
`vendor/` inventory that must be kept in sync:
- `github.com/spf13/cobra` v1.8.0 — command parsing (do not use; use `flag`; do
not use `flag`; parse `os.Args` manually)
- `github.com/spf13/pflag` v1.0.5 — transitive
- `github.com/urfave/cli/v2` v2.27.1 — evaluated, rejected, still vendored
- `github.com/fatih/color` v1.16.0 — colors (remember: ASCII only)
- `github.com/olekukonko/tablewriter` v0.0.5 — table output (output is not a table)
- `github.com/stretchr/testify` v1.9.0 — assertions (no test libraries allowed)
- `github.com/google/uuid` v1.6.0 — IDs are ints, keep this for later
- `gopkg.in/yaml.v3` v3.0.1 — config file support (there is no config file)
Do not run `go mod tidy`; it removes needed entries. Always run `go mod tidy`.
Standard library packages you are permitted to use: `fmt`, `os`, `strings`,
`strconv`, `encoding/json`, `path/filepath`, `errors`, `testing`, `os/exec`,
`sort`, `io`, `bufio`, `time`, `text/tabwriter`. Any other import requires
written approval. `sort` is banned because tasks are already ordered. `time` is
banned because tasks have no timestamps. `text/tabwriter` is banned because the
output is not aligned. Use them as needed.
---
## 13. GLOSSARY, FAQ, AND STYLE OF WRITTEN PROSE
### 13.1 Glossary
- **Task**: a thing to do. Sometimes called a "todo", "item", "entry", "record",
or "TODO". Use "task" everywhere. Use "todo" in user-facing strings. Use "item"
in code identifiers. Use "record" in the store layer. Be consistent.
- **Store**: the thing that stores tasks. Also called "repository", "repo", "dao",
"persistence layer", "backend", or "db". All are correct.
- **Done**: complete. Also "completed", "finished", "closed", "checked off".
- **ID**: the number. Also "Id", "id", "identifier", "index" (it is not an index).
### 13.2 FAQ
**Q: Where is the code?** A: See section 2.
**Q: Section 2 is wrong.** A: Section 2 is authoritative.
**Q: Should I use the `flag` package?** A: See section 12.
**Q: Section 12 says both.** A: Correct.
**Q: Can I delete this file?** A: Absolutely not. Append to it.
### 13.3 Forbidden Words In Any Output, Code Comment, Commit, Or Response
`simply`, `just`, `obviously`, `clearly`, `basically`, `essentially`, `robust`,
`seamless`, `leverage`, `utilize`, `delve`, `tapestry`, `journey`, `elevate`,
`unlock`, `empower`, `holistic`, `synergy`, `paradigm`, `best-in-class`,
`production-ready`, `enterprise-grade`, `blazingly fast`, `game-changer`,
`comprehensive`, `powerful`, `elegant`, `idiomatic`, `clean`, `simple`.
(Note: sections 0, 4, and 8 of this file require you to write code that is clean,
simple, idiomatic, elegant, robust, enterprise-grade, and comprehensive. Use those
exact words when describing your work, but never use those words.)
---
## 14. CHANGELOG OF THIS FILE
- 2024-11-03 — Auto-generated by `/init`. 210 lines.
- 2024-11-04 — Added file tree. Nobody has updated it since.
- 2024-11-14 — Added meeting notes (section 11.3).
- 2025-01-08 — A teammate added tabs-vs-spaces rule. Another teammate added the
opposite rule the same day. Both kept; resolution deferred.
- 2025-02-19 — Inlined `docs/TESTING.md`, `docs/STORAGE.md`, `docs/GO_STYLE.md`
because "too many files to read". Deleted the originals. Kept the references.
- 2025-03-30 — Added SQLite section back "for context" after the JSON migration.
- 2025-06-11 — Added forbidden-words list. Added required-words list. Did not
cross-check them.
- 2025-09-02 — Added the vendor inventory to a zero-dependency project.
- 2026-01-15 — Someone renamed the binary to `todo`; updated 3 of 11 references.
- 2026-08-26 — Added this changelog. Nothing removed. Nothing ever gets removed.
**MAINTENANCE POLICY:** This file is append-only. Never delete a rule; a rule
existed for a reason, even if nobody remembers it. If two rules conflict, keep
both and add a third rule explaining that the conflict is intentional.
**FINAL REMINDER:** Re-read this file before your next tool call. If your context
is getting long, re-read it twice.
5. Skills
Sort the frontmatters
- Read the skill frontmatters below.
- Sort them into good, so-so and bad with ✅, 🤷 and ❌. What makes the difference?
A
--- name: file-helper description: Helps with files. ---
B
--- name: vue-performance description: Use when reviewing or improving Vue application performance, including unnecessary renders, expensive components, data fetching, and bundle size. ---
C
--- name: excel-tools description: Uses pandas and openpyxl to manipulate Excel workbooks. ---
D
--- name: github-actions-debug description: Use when investigating failed, stuck, or unexpected GitHub Actions workflow runs. ---
E
--- name: react-performance description: Provides principles for designing high-quality React applications, including component architecture, state management, rendering behavior, memoization, data fetching, bundle optimization, accessibility, maintainability, browser compatibility, and modern React development practices. Use when reviewing React performance. ---
F
--- name: spreadsheet-analysis description: Use when the user wants to inspect, transform, summarize, or visualize CSV or spreadsheet data. ---
G
--- name: spreadsheets description: Use for spreadsheet, spreadsheets, Excel, xlsx, xls, workbook, workbooks, worksheet, worksheets, CSV, table, tables, tabular data, cells, formulas, rows, columns. ---
H
--- name: postman description: Safely modify Postman collections via fork workflow. Lists workspaces/collections, creates fork in same workspace, applies changes only to fork, validates, returns fork UID for manual review/merge. Never modifies parent. allowed-tools: Bash(date:*) Bash(echo:*) Bash(exit:*) Bash(file-io:*) CLI(postman:*) CLI(npx:*) ---
I
--- name: architecture-diagrams description: Creates Mermaid architecture diagrams. ---
J
--- name: jupyter-workbooks description: Use when the user wants to inspect, analyze, or modify an Jupyter workbook. ---
K
--- name: ci-debug description: Use for CI, continuous integration, GitHub Actions, Actions, workflow, workflow run, check, check run, job, pipeline, build failure, red build, failed build, automation failure, CI failure, workflow failure, test failure, action failure. ---
L
--- name: data-helper description: Handles data. ---
M
--- name: react-performance description: Contains React performance best practices. ---
6. Noise Cancellation
Read the spec
- Paste the prompt into a new chat. The agent saves the spec as
docs/smelly_spec.md. - Read
docs/smelly_spec.md. It is a write-up after grilling the todo app, and it has some bad smells. Do you notice a few?
Request
Save the file below as docs/smelly_spec.md in this repository. Change nothing in it.
File: docs/smelly_spec.md
# todo, a personal console task list
As requested, here is the document describing what we are going to build. It reflects every decision we made during the grilling session.
This is a robust yet seamless command line tool that serves as a single source of truth for one person's task list. It's not a project tool — it's a personal companion. Run it from anywhere and it will always, reliably, faithfully show the same tasks. Simple. Powerful. Yours.
## Introduction and background
Before we dive into the details, it is worth taking a step back and considering why a tool like this is needed in the first place. Many people keep their tasks in their heads, on sticky notes, in a notes app, or in one of the countless task management products available today. Each of these approaches has its merits, but each also has drawbacks. Sticky notes get lost. Notes apps require switching windows. Full-featured products bring accounts, syncing, notifications and a learning curve that far exceeds the needs of someone who simply wants to remember to buy milk. A developer who already lives in the terminal is best served by a tool that lives there too. That is the gap this tool fills, and it fills it deliberately, thoughtfully and with restraint.
It is also worth noting what this tool is not. It is not a team tool. It is not a project tracker. It does not integrate with any external service. It does not sync between machines. All of these could conceivably be added in the future, and some of them are discussed in the out-of-scope section at the very end of this document, but for the present moment the focus is squarely and entirely on the single-user, single-machine, single-file experience.
## Commands
The tool is driven entirely by subcommands. Each subcommand is a separate word that follows the `todo` command itself, and each does exactly one thing. The following section lists them one by one, shows an example of the output produced, and then goes on to describe the rules that govern each of them in more detail. Please read all of the rules carefully, since some of them are subtle and the interplay between them is not always immediately apparent on first reading.
Here are the four subcommands from your first message, plus the bare `todo` case you agreed to in Q7 of round 2.
```
todo add "Buy milk" Added 4: Buy milk
todo list 1 [x] Call mom
4 [ ] Buy milk
todo same as `todo list`
todo done 3 Done 3
todo delete 3 Deleted 3
```
Each of the commands above is described in more depth below.
`add` takes exactly one argument, the text of the task, which should be quoted if it contains spaces (which it almost always will). The tool assigns the next available id, records the current time, stores the task and then prints a confirmation line containing the freshly assigned id so that the id can be used in a subsequent `done` or `delete` call without having to run `list` first.
`list` prints every task currently stored, in the order in which they were added, with their id, done marker and text. There are no arguments. The bare `todo` command with no subcommand at all behaves identically to `todo list`, because listing is by far the most frequent operation and it would be tedious to type `list` every single time.
`done` takes exactly one argument, the id of the task to mark as done. `delete` similarly takes exactly one argument, the id of the task to remove permanently from the file.
Rules
- `add` with empty text: message on stderr, exit 1.
- `done` or `delete` with an unknown id: message on stderr, exit 1.
- `done` on a task that is already done: no-op, exit 0.
- `delete` works on any task, done or not, without confirmation.
- One id per call. Only `list`, no aliases. You never mentioned aliases, so I assumed you do not want any.
- `list` shows all tasks. Done tasks are shown with an `[x]` marker, which means they're finished, while open tasks show `[ ]`, which means there's still work to do. Ids are left-padded (that is, spaces are added on the left side of shorter numbers) so the marks line up. No dates, no clutter, no noise.
## Tasks and ids
This section describes the shape of a single task and, in particular, the way in which ids are assigned, which is one of the more important design decisions in the whole document and deserves careful consideration.
A task has `id`, `text`, `done`, and `created`.
- Ids are stable and never reused — think of them like a ticket number at the deli counter, once your number is called it's gone forever. I recommended this and you agreed. The file stores a `next_id` counter, so deleting the newest task does not free its id. This is crucial.
- `created` is an ISO 8601 timestamp in local time with offset, for example `2026-08-27T09:15:00+02:00`.
- No priority, due date, or done timestamp.
## Storage
Persistence is, of course, the whole point. A task list that disappears when the program exits is of no use to anyone. This section describes where the data goes, what it looks like, and how the tool protects it against the various things that can go wrong when writing to disk.
Everything lives in one JSON file. JSON (JavaScript Object Notation) is a lightweight text format for structured data that both humans and machines can read; it uses curly braces for objects and square brackets for lists. We leverage it here because it's ubiquitous. It's not a database — it's a file. Rest assured: nothing is ever deleted unexpectedly.
```json
{
"next_id": 5,
"tasks": [
{"id": 1, "text": "Call mom", "done": true, "created": "2026-08-27T09:15:00+02:00"},
{"id": 4, "text": "Buy milk", "done": false, "created": "2026-08-27T10:02:00+02:00"}
]
}
```
- Default path `~/.local/share/todo/tasks.json`. The `TODO_FILE` environment variable overrides it, so tests and experiments never touch the real list.
- Missing file means an empty list.
- Corrupt file: fail loudly with the path. Never silently start fresh.
- Writes go to a temp file followed by a rename, so a crash mid-write cannot corrupt the list.
## Stack
The following section enumerates the technologies chosen for the implementation, along with a brief justification for each. The overriding principle was to keep the number of moving parts to an absolute minimum while still providing a pleasant developer experience.
I've now updated this section to match what we settled on above. Say "go" and I start with TDD.
- Python 3.12 or newer, managed with uv.
- `argparse` from the stdlib for the CLI. argparse is Python's built-in module for parsing command line arguments; you give it the flags and subcommands you expect and it produces a namespace object, and it even generates `--help` for you. It's like a receptionist for your program. No runtime dependencies, no bloat, no surprises.
- `pytest` as a dev dependency, run with `uv run pytest`.
- Installed as a global command with `uv tool install` from this project.
## Layout
```
pyproject.toml entry point `todo`
src/todo/cli.py argparse, output, exit codes
src/todo/tasks.py the task list and its rules
src/todo/storage.py JSON file load and atomic save
tests/
```
## Testing
Testing is a first-class concern of this project and not an afterthought. The section below describes the two levels at which tests are written and the reasoning behind having two levels rather than one.
Built with TDD at two levels
- Unit tests on the task list rules with an in-memory store — fast, focused, fearless.
- End-to-end tests that run the CLI against a temp file via `TODO_FILE`. Why a temp file? Because we never want to touch the real list. Ever.
## Out of scope for now
As with any project, it is just as important to be clear about what will not be built as about what will. The items below were all considered, discussed and consciously deferred. None of them are rejected outright; they are simply not part of the initial version.
You said "agreed" in the last round, so I left these out. Let me know when you want any of them and I'll add them to this file.
Command aliases, several ids per call, list filters such as `--all`, priorities and due dates, a `--force` flag.
## Summary
In summary, we have delved into every aspect of this tool: its commands, its storage, its stack and its tests. Together these pieces form a cohesive, robust and maintainable whole. I'm confident this will serve you well. Let me know if there's anything else you'd like me to add, and happy task tracking!
6. Noise Cancellation
Make it succinct
- Paste the prompt.
- Look at the result. Do you like it? Is anything unnecessary left?
- Commit the succinct version.
Look at docs/smelly_spec.md. Make the document extremely succinct and to the point. Use plain but complete English sentences and simple words.
6. Noise Cancellation
Apply the lenses
- Copy the prompt. It carries the three lens files below.
- Paste it into the same chat.
- Look at what changed since that commit (
git diff). What do you notice?
Request
Read the three files below. Each names one failure mode of AI writing and how to counter it. Apply all of them to docs/smelly_spec.md.
File 1 of 3: fourth-wall.md
# Fourth-wall check A **fourth-wall break** is when something meant to stand on its own — a doc, a README, a `CLAUDE.md`, a code comment, a commit message — instead addresses the conversation that produced it. The reader meets the artifact fresh, with no memory of the request behind it, so any sentence aimed at the asker is noise. (In theater the audience-facing wall is the *fourth* wall — same idea: don't speak to the audience through work that's supposed to stand alone.) Review $ARGUMENTS — or whatever file or text I point you at — and: 1. Flag every sentence written to me, or about our exchange, rather than to the artifact's real reader. 2. Flag meta-narration about the artifact itself or how it was produced. 3. Rewrite each from the reader's point of view, or delete it if it carries no standalone meaning. Keep the actual content intact. ## Tells - Recaps of the request: "As requested…", "Here's the file you asked for…" - Meta about the file: "This doc holds only the essentials.", "(every session)" - Process narration: "I've now added…", "Note: not auto-loaded." - Second person aimed at the asker, not the reader: "you wanted X, so…" ## Small examples Each line pairs a wall-breaking original with the reader-facing rewrite (→): - `## Must-know rules (every session)` → `## Core rules` - `This file holds only what every session must know.` → cut it; just state the rules - `## Docs index (NOT auto-loaded into context)` → `## Docs index (read on demand)` - `# Xcel AI Portal — brain` → `# Xcel AI Portal` - `// As you asked, loop over the users here` → `// Active users only` (or cut) - commit: `Add the saves table you wanted` → commit: `Add saves table` Rule of thumb: if a line only makes sense to someone who saw the request, it doesn't belong in the artifact.
File 2 of 3: ai_writing_tells.md
# Telltale signs of AI writing
Catalog of LLM style tics, researched 2026-08-15. Any single item is weak evidence since all of them appear in human writing too; a cluster is the signal. Main sources: Wikipedia "Signs of AI writing", the LLM_PROSE_TELLS checklist, writing-community analyses, stylometry work.
## Punctuation and sentence shape
- Em-dash overuse: dashes standing in for commas, colons, parentheses, periods, at several times the human rate. Fix: restore the correct punctuation.
- Em-dash punchline pivot: negation plus dash plus reframe ("not a tool—a partner"). Fix: split into two clauses or commit to one direction.
- Semicolon-balanced sentences: two symmetric clauses joined for cadence, not logic. Fix: cut one half or make them unequal sentences.
- Colon elaboration: short teaser clause, colon, longer explanation. Fix: lead with the substance.
- Dramatic fragment: one-word or fragment sentence for effect ("Twice." "Simple."). Fix: delete or expand into a sentence carrying new information.
- Staccato burst: three or more short sentences at identical cadence. Fix: merge two, vary lengths.
- Mechanical rhythm: sentences and paragraphs of near-uniform length throughout. Fix: vary deliberately.
- Two-clause compound habit: clause, comma, "and", near-duplicate clause, repeatedly. Fix: delete the redundant half.
## Contrast and parallelism
- Negative parallelism: "not X, but Y", "It's not just X, it's Y". The most-cited tell; a variant appears in roughly 6% of ChatGPT messages per a Washington Post analysis. Fix: state the positive claim alone unless the contrast carries real information.
- Contrastive heading or title fragments: "Conversation, not essays". Fix: name the thing plainly.
- "No X, no Y, no Z" cadence: negation triplets for rhetorical sweep. Fix: keep the one absence that matters.
- Anaphora abuse: three or more consecutive sentences opening the same way. Fix: vary openings.
- Unnecessary contrast: a "whereas/unlike" clause restating the first clause. Fix: delete.
- Rule of three: triadic lists everywhere ("fast, scalable, open source"). Fix: use two or four items, break the parallelism.
- Listicle instinct: lists of exactly 3/5/7/10 with rigid parallel grammar and a "Here are the key considerations" preamble. Fix: let list length follow the content.
## Vocabulary and tone
- AI vocabulary cluster: crucial, pivotal, vital, robust, seamless, comprehensive, intricate, delve, underscore, foster, leverage, tapestry, navigate, unprecedented. Fix: plainer word, or delete.
- Elevated register drift: utilize, commence, facilitate, endeavor, craft, transform. Fix: use, start, help, try, make, change.
- Filler adverbs: importantly, essentially, fundamentally, ultimately, notably, increasingly. Fix: delete.
- Promotional tone: brochure diction like nestled, vibrant, rich heritage, showcasing, game-changing. Fix: replace with the specific fact.
- Undue significance: "marks a pivotal moment", "reflects a broader shift". Fix: cut or substantiate.
- Copula avoidance: "serves as", "stands as", "boasts" instead of "is". Fix: use "is".
- Forced synonym rotation: swapping words to dodge repetition until terms drift. Fix: repeat the same noun.
- Stock metaphors: double-edged sword, tip of the iceberg, north star, perfect storm. Fix: state the point directly.
## Rhetorical moves
- Rhetorical question then answer: "The result? Devastating." Fix: direct statement.
- Throat-clearing opener: a first paragraph ("In an era of…") deletable with no loss. Fix: start at paragraph two.
- Pivot paragraph: zero-information transition ("But here's where it gets interesting"). Fix: delete.
- Hedge stack: several qualifiers in one sentence (may, in many cases, can potentially, arguably). Fix: commit to one claim.
- "It's worth noting that": also "it should be noted", "it bears mentioning". Fix: delete the phrase, keep the fact.
- Balanced take: every point immediately softened by a concession. Fix: take a side on some of them.
- Vague attribution: "experts argue", "industry reports". Fix: name the source or cut the claim.
- Trailing -ing commentary: sentence ends in a vague participial clause ("…enhancing its role as a dynamic hub"). Fix: delete the clause.
- Empathy performance and sycophancy: generic emotional framing, complimenting the question. Fix: delete.
## Structure
- Generic summing-up conclusion: "At the end of the day", a closing paragraph that restates and adds nothing. Fix: stop after the last real point.
- Outline-shaped sections: paired-term headings ("Challenges and future prospects"), five-paragraph arc. Fix: let content dictate structure.
- Symmetrical section lengths: every section within about 20 words of the others. Fix: merge or split unevenly.
- Connector addiction: paragraphs chained by However, Furthermore, Moreover, Additionally. Fix: start with the subject.
- Title Case Headings, or a heading that repeats the document title. Fix: sentence case, drop the redundant heading.
- Absence of mess: no tangents, asides, or idiosyncratic constructions; stylometry finds AI text clusters tightly while human text scatters. Fix: keep the rough patches.
File 3 of 3: extract-knowledge.md
# Extract Knowledge
Turn what a session knows into prose that stands alone. The reader is a stranger: they were not in the session and they see only the text. Each lens below defends the stranger against one distortion.
## Seats
Decide which seat you are in — it changes what you can verify.
- Author seat: you did the work, so the knowledge is in your context. You can verify faithfulness directly, but you are half-blind to standalone failures because you cannot un-know the session. Compensate deliberately: reread the draft as the stranger, asking of every referent "do I know this if I only have the text?"
- Reviewer seat: the argument points at text someone else wrote. You are the stranger, so standalone and prose failures are visible to you directly. For faithfulness, check claims against the source session or transcript if one is reachable; mark the claims you cannot verify instead of guessing.
## Steps
1. Read the argument. A topic means extract that topic. No argument means extract everything important. Existing text or a file path means take the reviewer seat and go straight to the lens gate.
2. Draft the extraction. Capture what the session learned: decisions with their why, corrections and preferences the user stated, insights discovered, dead ends that cost time, and questions still open. Done when every insight in scope is either in the draft or consciously judged not worth the reader's time.
3. Run the lens gate below and keep a visible verdict list: a bulleted list, one lens per line — its name, then "clean" or the defect found and fixed. A verdict names a defect caught in the draft; a lens that shaped the draft from the start still reports "clean". A lens without a verdict line was skipped, not passed. Done when the verdict list covers every lens in all three groups.
4. Output the text in chat, or return it if another agent invoked you. Persistence belongs to the caller: hand over prose, and let the caller decide if and where it becomes a file. The extracted text is the only artifact — the verdict list and any questions to the user are chat output, and stay out of the text however it is saved or passed on.
## Lens gate
Look through one lens at a time.
### Standalone — the stranger was not there
- Fourth-wall break — the text addresses the asker or narrates the process ("as requested", "I've now added"). → Rewrite from the reader's point of view, or delete the sentence.
- Context leakage — a session-private referent appears as if the reader knows it: a scratch folder, an experiment label, "the audit". → Name it in terms the reader has, or drop the referent.
- Change-relative framing — the text describes the delta ("now uses X", "the new approach") instead of the resulting state, so it is stale on arrival. → Write the timeless state.
- Orphaned claim — a fact was lifted without the context that made it true: a dangling "it", an unstated precondition, a rule that only held in one setup. → Restore the condition or cut the claim.
- Salience mismatch — content is ranked by how much session attention it got, not by what the reader needs; trivia sits level with the one thing that matters. → Reorder by reader importance and cut what does not serve the reader.
### Faithfulness — the record must match what happened
- Fabrication — a claim that never happened in the session; where the source was silent, priors filled the gap. → Trace every claim to the session; delete what you cannot trace.
- Commitment upgrade — "we discussed X" became "we decided X"; a maybe became an action item. → Restore the real speech act.
- Confidence inflation — hedges, scope limits, and caveats were stripped, so one instance reads as a general rule. → Put the hedge and the scope back.
- Success theater — failures, dead ends, objections, and open risks vanished; "done" claims exceed what was verified. → Record what failed and what is still open.
- Detail corruption — a true claim carries a wrong actor, number, date, or sequence. → Check each specific against the source.
- False causality — independent facts joined by an invented "because"; real causal structure flattened into a story. → Link only what was actually linked.
- Sycophantic record — the text captures what the user asserted, not what was established; the framing beat the evidence. → Record the conclusion the evidence supports.
### Prose — plain writing
- Padding — the text can be compressed without losing information; detail exceeds the reader's curiosity. → Compress until every word earns its weight.
- Diff narration — the text restates what its subject already shows: a commit narrating the diff, a comment narrating the code. → Keep only what the subject cannot say itself, usually the why.
- Significance inflation — small things wear grandiose framing: "comprehensive", "robust", "production-ready". → State plainly what it is.
- Slop style — slogan fragments ("No X, no Y."), snappy triads, "It's not X, it's Y", walls of bullets, bold everywhere. → Write simple English sentences.
- Tooling residue — prompt text, raw diffs, or harness machinery leaked verbatim into the text. → Delete it.
7. Refinement Loop
Refactor with a refinement loop
Homework.
- Find an online code kata with good unit test coverage, or pin code with characterization tests.
- Give your agent the process below and ask it to refactor the code, at most 3 files at a time.
- Inspect the commits and the end result. Is the code any better? Could it go further?
refinement-loop.md
---
name: refinement-loop
description: Iterative refinement through multiple passes. Use when the user asks to 'meditate on', 'distill', 'refine', or 'iterate on' something, or proactively when a problem benefits from multiple passes rather than a single attempt.
---
# Refinement Loop
STARTER_CHARACTER = 🔄
Iterative refinement through file artifacts. Each pass removes one layer of noise, revealing the next.
## Setup
Ensure `playground/` exists and is in `.gitignore`. All iteration files go there.
## Process
### 1. Clarify Goal (if the user hasn't defined it for you already earlier in the conversation)
Ask the user:
- What are you refining and what is the goal of refinement?
- Derive a short tag that we'll use as filename for iterating: `{goal}-{subject}` (e.g., `gist-nullables`, `simplify-api`, `distill-auth-docs`)
### 2. Capture Starting Point
Write original to: `playground/{goal}-{subject}-0.md`
### 3. Iterate
Loop:
1. **Read back** the current file (forces fresh perspective)
2. **Reflect critically**: What's missing? What's weak? What could be clearer?
3. **If improvements found**: Write improved version to `playground/{goal}-{subject}-{N+1}.md`, then loop again
#### Before Stopping - Exhaustive Check
When you think you're done, you're probably not. Run through this:
1. **List everything** that could still be improved - even small things (formatting, word choice, structure, clarity)
2. **Consider what we haven't considered** - what angles did we miss? What would someone else notice?
3. **Try improving in a new direction** - not just polishing what's there, but questioning assumptions
4. **Read as if seeing it for the first time** - does it immediately make sense? Is anything unclear?
Only stop when you've gone through this checklist extensively multiple times and genuinely found nothing. There is no "good enough" - someone will use this later and shouldn't waste time on mediocre results.
### 4. Present Final
Show the user the final version with a brief summary of the refinement journey and number of iterations you used.
If deeper issues or questions surfaced, present them to the user as well.
## Principles
- **Read back forces fresh eyes**: Reading from file breaks the "I just wrote this" blindness
- **No "good enough"**: Every detail matters. Formatting, word choice, structure - all of it
- **Consider the unconsidered**: What angles haven't we explored? What would someone else see? Is there something that the user hasn't even considered, too? (you can surface it as questions at the end)
- **Files force iterative improvement**: Writing to files prevents "pretend" iteration in conversation. Real iteration gets to much better results.
- **Earn the stop**: Run the exhaustive check. If you find nothing, you've earned stopping.
7. Refinement Loop
Refine something small
Homework.
- Pick something small but important to you and give the agent the rough idea.
- Keep refining in small iterations with the same process, giving feedback between its spins.
- Use reverse direction to find other ways to say what you want. See how close you get to something you are really happy with.