Imported from JCheungX/agentic-to-do (
AGENTS.md). Install upstream withnpx skills add JCheungX/agentic-to-do. Copyright stays with the author.
todo-list
A todo list app. Express API + React client + Postgres.
The requirements are in docs/brief.pdf. The brief is deliberately over-scoped. What matters is requirement interpretation, prioritization, technical reasoning, code quality, verification, and communication — not feature count.
Build the simplest thing that satisfies the brief
A todo list is a CRUD app. Write the row. If the change is worth showing a user, append a history row in the same transaction.
Complexity has to be paid for by a requirement in the brief, and the payment has to be written
down in DECISION-LOG.md. Cycle detection is complex and earns it — the brief asks for
dependencies.
Layout
server/ Express API. routes -> services -> db.
client/ React app
docs/ The brief, and reference docs
Commands
| Task | Command |
|---|---|
| Install | npm install |
| Dev (all) | npm run dev |
| Type-check | npm run typecheck |
| Lint + format check | npx biome check . |
| Autofix | npx biome check --write . |
| Test | npm test |
| E2E | npx playwright test |
| Decision log HTML | node scripts/generate-decision-log-html.mjs |
Use npm install --legacy-peer-deps if peer dependency conflicts appear.
Conventions
-
TypeScript throughout,
strict: true. Noany— useunknownand narrow. -
Every status, type, or other closed set is a TypeScript
enum— never a bare string union, never a loose string. Members and values areSCREAMING_SNAKE_CASE, and the value is what the database and the API carry:export enum TodoStatus { NOT_STARTED = "NOT_STARTED", IN_PROGRESS = "IN_PROGRESS", COMPLETED = "COMPLETED", } export enum TodoPriority { LOW = "LOW", MEDIUM = "MEDIUM", HIGH = "HIGH", }The schema is the contract; the frontend decides how to display it. The API speaks enum values and nothing else — it never accepts or returns a human-readable label, and it never carries display text, ordering, or colour for one.
NOT_STARTEDis what goes over the wire; whether the user sees "Not Started", "Todo", or an icon is the client's decision alone. A server that formats for a screen has taken a decision that was not its to take. -
Biome is the only formatter and linter. Do not add ESLint or Prettier.
-
Run
npx biome check --write .before committing. Never hand-format to satisfy the linter. -
Kebab-case filenames. React components in
PascalCase.tsx, everything elsekebab-case.ts. -
Server: routes validate input, call a service, serialize the response. Services hold the logic.
-
Client: TanStack Query owns server state. No
useEffectfetching. -
Errors: the API returns
{ error: { code, message } }with a real HTTP status. Never a 200 with an error body. -
No emojis in code, comments, or commit messages.
Comments
Write a comment only to state a constraint the code cannot show — an invariant, a non-obvious reason, a gotcha. Never restate what the next line does.
Testing
Write the test before the implementation. A test must fail for the right reason before you make it pass. Test behavior through public interfaces, not internals.
Do not mock the database. Server tests run against a real Postgres via Testcontainers. The interesting behavior here is recursive SQL, transactions, and races; a mocked database lets those tests pass while asserting none of it. MSW is the one exception, on the client, at the network boundary.
Spend the tests where the risk is: cycle detection, the blocked rule, recurrence arithmetic, and the races. A handful of Playwright journeys, not fifty.
Verification
Never claim work is done without running something and reading its output. Type-check, lint, test, and drive the real endpoint or UI. Tests prove what you thought to assert; driving the thing finds what you did not.
Decisions
DECISION-LOG.md at the repo root: what was chosen, what was rejected, and why. Dated, appended
as we go. It is where a deviation from the brief gets justified — and where a piece of complexity
gets its receipt. Keep it short enough that someone reads it.
DECISION-LOG.html is the readable standalone version. The Markdown file is the source of truth,
but both files must be updated together: after every decision-log edit, run
node scripts/generate-decision-log-html.mjs and include the regenerated HTML in the same change.
Never edit the generated HTML by hand.
Git
- Never commit to the default branch. Branch first.
- One commit per logical change. Imperative subject line, no emojis.
- Commit only when asked.