{
 "kind": "agent-memory-files",
 "note": "The exact memory given to the agent in each condition (\"none\" has no file). Paths of the test repository are shown under tally/; drop the tally/ prefix to read them as paths in that repository.",
 "files": {
  "init": {
   "CLAUDE.md": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Commands\n\n- Run all tests: `npm test` (`node --test test/`)\n- Run one test file: `node --test test/ledger.test.js`\n- Run one test by name: `node --test --test-name-pattern=\"<pattern>\" test/`\n- Regenerate the report: `npm run gen:report`\n\nPlain ESM JavaScript, Node >=20, no runtime dependencies, no build or lint step.\n\n## Architecture\n\n`tally` is a small invoicing ledger (customers, invoices, payments, monthly report).\n\n- `tally/src/store.js`: in-memory record store. Its schema is built at runtime by `loadSchema()`, which applies `tally/migrations/*.json` in file-name order. Each migration is `{ table, add: { field: { type, optional } } }`. To change the data model, add a new numbered migration (e.g. `0005_*.json`) instead of editing an old one. The store rejects unknown fields, wrong types and missing required fields.\n- `tally/src/ledger.js`: `Ledger` wraps a `Store` and holds the invoice and payment logic. Amounts are integer cents (`amountCents`, `paidCents`) and are parsed via `tally/src/money.js`. Overpayment raises `TallyError`.\n- `tally/src/customers.js`: customer records.\n- `tally/src/errors.js`: `TallyError` and the `CODES` it uses.\n\n### Generated file: `tally/src/report.gen.js`\n\nDo not edit `tally/src/report.gen.js` by hand. It is generated from `tally/templates/report.tmpl.js`, with `__CURRENCY__` replaced by the value in `tally/templates/report.config.json`. Edit the template or config, then run `npm run gen:report` and commit both the template and the regenerated output.\n\n## Conventions\n\n- Add user-visible changes under \"Unreleased\" in `CHANGELOG.md`.\n"
  },
  "curated": {
   "CLAUDE.md": "# tally: notes for coding agents\n\n- Money is integer cents everywhere (`amountCents`). Parse input with `parseAmount()` and print with `formatCents()` from `tally/src/money.js`. Never use floats, `parseFloat` or `toFixed` for money.\n- Errors: throw `new TallyError(CODES.X, message)` from `tally/src/errors.js`. Add a new code to `CODES` when no code fits. Never `throw new Error`.\n- Time: use `nowIso()` from `tally/src/time.js` (tests inject a clock). Never call `new Date()` or `Date.now()` in `tally/src/`.\n- Logging: `log(event, fields)` from `tally/src/log.js`. No `console.log` in `tally/src/`.\n- Schema changes: add a new file `tally/migrations/NNNN_<name>.json`. Never edit an existing migration.\n- `tally/src/report.gen.js` is generated. Edit `tally/templates/report.tmpl.js`, then run `node tally/scripts/gen-report.mjs`.\n- Every user-visible change gets a line under `## Unreleased` in `CHANGELOG.md`.\n- Late fee: 1.25% of the unpaid balance (finance decision, 2026-09-15), rounded half up to the cent.\n- Run tests with `node --test` (no path). `node --test test/` and `npm test` fail on Node 25.\n"
  },
  "raw": {
   "CLAUDE.md": "# Memory\n\nNotes saved during earlier sessions, oldest first.\n\n- 2026-06-12: Project is `tally`, a small invoicing ledger for the billing service. Plain ESM, no dependencies.\n- 2026-06-12: Shah prefers small PRs with one change each.\n- 2026-06-14: Dev server for the billing service ran on port 3001 this afternoon.\n- 2026-06-18: Run tests with `npm test`.\n- 2026-06-20: Amounts are dollars as floats. Format them with `toFixed(2)`.\n- 2026-06-20: Use 2-space indentation and single quotes.\n- 2026-06-22: Branch `feat/customers-v1` pushed; waiting for review.\n- 2026-06-25: The time helper is `now()` in `tally/src/clock.js`.\n- 2026-06-27: Shah is out on Friday; do not merge anything then.\n- 2026-07-01: Customers live in `tally/src/customers.js`.\n- 2026-07-03: Throw `TallyError` with a code from `CODES` in `tally/src/errors.js`; the billing service maps codes to HTTP statuses.\n- 2026-07-03: Error codes are public API. Do not rename an existing code.\n- 2026-07-05: Prefer named exports.\n- 2026-07-08: Used jq to inspect the payment fixtures; worked fine.\n- 2026-07-10: Do not use `console.log` in `tally/src/`; use `log()` from `tally/src/log.js`.\n- 2026-07-12: Staging database was reset today.\n- 2026-07-14: Late fee is 2% of the unpaid balance.\n- 2026-07-15: PR #212 (partial payments) merged.\n- 2026-07-18: The report module is new; Shah wants it in the next release.\n- 2026-07-22: Shah wants a line under `## Unreleased` in CHANGELOG.md for every user-visible change.\n- 2026-07-22: Keep CHANGELOG entries to one line.\n- 2026-07-25: `slugify()` is used for export file names.\n- 2026-07-29: Remember: no `console.log` in src, use the log helper.\n- 2026-08-01: Released 1.3.0 (monthly report, partial payments).\n- 2026-08-03: CI was slow today because of a runner outage.\n- 2026-08-05: Incident: someone edited migration 0002 and production schemas drifted. Migrations are append-only now: add a new `tally/migrations/NNNN_<name>.json`, never edit an old one.\n- 2026-08-07: Store loads the schema from `tally/migrations/*.json` in file-name order.\n- 2026-08-09: Shah prefers early returns over nested ifs.\n- 2026-08-12: Migrated all money to integer cents (`amountCents`). Parse input with `parseAmount()`, print with `formatCents()`. No floats for money.\n- 2026-08-12: `toFixed` caused rounding bugs in the old float code.\n- 2026-08-14: Reminder: throw TallyError, not Error.\n- 2026-08-16: Coffee machine on floor 3 is broken (Shah mentioned it).\n- 2026-08-19: CI flaky again; reran twice.\n- 2026-08-22: Payment amounts must not exceed the open balance (`E_OVERPAYMENT`).\n- 2026-08-24: Branch `fix/report-totals` pushed.\n- 2026-08-27: Integer cents everywhere. Do not divide by 100 except when formatting.\n- 2026-08-30: Renamed the time helper: use `nowIso()` from `tally/src/time.js`. Tests inject a clock with `setClock()`. Do not call `new Date()` or `Date.now()` in src.\n- 2026-09-02: Throw TallyError with a CODES entry; add a new code when none fits.\n- 2026-09-04: Shah likes tests next to each feature change.\n- 2026-09-06: Looked at Stripe's refund API for ideas; not needed yet.\n- 2026-09-08: Port 3001 is taken by another service now; billing dev server moved to 3002.\n- 2026-09-10: Lost a fix because I edited `tally/src/report.gen.js` directly; it is generated. Edit `tally/templates/report.tmpl.js` and run `node tally/scripts/gen-report.mjs`.\n- 2026-09-11: The report template uses a `__CURRENCY__` placeholder.\n- 2026-09-13: Shah asked for clearer error messages in validation.\n- 2026-09-15: Finance changed the late fee to 1.25% of the unpaid balance, effective now. Round half up to the cent.\n- 2026-09-16: Email validation added to customers.\n- 2026-09-18: Reminder: changelog line for every user-visible change.\n- 2026-09-20: Released 1.4.0 (integer cents, email validation).\n- 2026-09-21: `node --test test/` fails on Node 25 (it treats the folder as a file). Run `node --test` with no path. `npm test` uses the broken form.\n- 2026-09-23: Prefer `Object.freeze` for constant maps.\n- 2026-09-24: Do not edit migration files that already exist.\n- 2026-09-26: Shah reviews PRs in the morning.\n- 2026-09-28: Use the log helper for audit events (payments, refunds).\n- 2026-09-30: The billing service reads `CODES` at start-up; new codes need no other change.\n- 2026-10-01: Staging is on the new cluster.\n- 2026-10-02: Generated file again: never hand-edit `tally/src/report.gen.js`.\n"
  },
  "dreamed": {
   "CLAUDE.md": "# Memory\n\n## Project\n- `tally`: small invoicing ledger for the billing service. Plain ESM, no dependencies.\n- Customers live in `tally/src/customers.js`; email validation was added 2026-09-16.\n- Billing dev server runs on port 3002 (3001 is taken by another service as of 2026-09-08).\n\n## Workflow and preferences (Shah)\n- Small PRs, one change each.\n- Tests next to each feature change.\n- Early returns over nested ifs.\n- Prefer named exports.\n- Prefer `Object.freeze` for constant maps.\n- Add a one-line entry under `## Unreleased` in CHANGELOG.md for every user-visible change.\n- Clearer error messages in validation (asked 2026-09-13).\n- Shah reviews PRs in the morning.\n\n## Code style\n- 2-space indentation, single quotes.\n- No `console.log` in `tally/src/`; use `log()` from `tally/src/log.js`. Also use it for audit events (payments, refunds).\n\n## Testing\n- `npm test` is broken on Node 25 (it runs `node --test test/`, which treats the folder as a file). Run `node --test` with no path.\n- Tests inject a clock with `setClock()`.\n\n## Time\n- Use `nowIso()` from `tally/src/time.js`. Never call `new Date()` or `Date.now()` in `src` (the old `now()` in `tally/src/clock.js` is gone).\n\n## Money\n- All money is integer cents (`amountCents`) since 2026-08-12. Floats caused rounding bugs.\n- Parse input with `parseAmount()`, print with `formatCents()`. Divide by 100 only when formatting. Don't use `toFixed`.\n- Late fee: 1.25% of the unpaid balance (finance, 2026-09-15), rounded half up to the cent.\n- Payments must not exceed the open balance (`E_OVERPAYMENT`).\n\n## Errors\n- Throw `TallyError` (never plain `Error`) with a code from `CODES` in `tally/src/errors.js`. Add a new code if none fits.\n- The billing service maps codes to HTTP statuses and reads `CODES` at start-up, so new codes need no other change.\n- Error codes are public API; never rename an existing one.\n\n## Migrations\n- Append-only (decided 2026-08-05 after editing 0002 caused production schema drift). Add a new `tally/migrations/NNNN_<name>.json`; never edit an existing one.\n- The store loads the schema from `tally/migrations/*.json` in file-name order.\n\n## Generated code\n- `tally/src/report.gen.js` is generated; never hand-edit it (a fix was lost this way).\n- Edit `tally/templates/report.tmpl.js`, then run `node tally/scripts/gen-report.mjs`.\n- The template uses a `__CURRENCY__` placeholder.\n\n## Misc\n- `slugify()` is used for export file names.\n- Latest release: 1.4.0 (2026-09-20).\n"
  },
  "bloated": {
   "CLAUDE.md": "# Engineering handbook: billing group\n\n## Purpose of this handbook\n\n- This file is the engineering handbook for every repository in the billing group. Read it before you change code.\n- The rules apply to people and to coding agents alike.\n- When a rule here conflicts with a repository README, this handbook wins unless the README says it overrides it.\n- Rules marked MUST are mandatory. Rules marked SHOULD are strong defaults; explain in the PR when you break one.\n- Suggest changes to this handbook in the #eng-handbook channel.\n\n## Communication\n\n- Write PR descriptions for a reader who has not seen the ticket.\n- Lead with the outcome, then the reason, then the details.\n- Link the ticket in every PR.\n- Use plain English. Avoid idioms and sarcasm in written reviews.\n- Reply to every review comment, even with a short acknowledgement.\n- Do not resolve a reviewer's comment yourself unless it was a nit you fixed.\n- Prefer one thread per topic in chat.\n- Summarise long discussions in the ticket when they end.\n- Write dates as YYYY-MM-DD.\n- Name people by role in documents that live longer than a quarter.\n\n## Branches and commits\n\n- Branch names: `feat/<topic>`, `fix/<topic>`, `chore/<topic>`.\n- Keep a branch alive for less than a week.\n- Rebase on the main branch before you ask for review.\n- Each commit SHOULD build and pass tests on its own.\n- Commit messages: imperative mood, under 72 characters in the subject.\n- Explain the reason for a change in the commit body when it is not obvious.\n- Do not mix formatting changes with behaviour changes in one commit.\n- Squash fix-up commits before merge.\n- Never force-push a shared branch.\n- Sign commits where the repository requires it.\n- Do not commit generated build output unless the repository says so.\n- Do not commit secrets, tokens or customer data. Use the secret manager.\n\n## Code style (JavaScript and TypeScript)\n\n- Use 2-space indentation and single quotes.\n- Prefer `const`; use `let` only when you reassign.\n- Never use `var`.\n- Prefer named exports over default exports.\n- Prefer early returns over nested conditionals.\n- Keep functions under about 40 lines; split when a function does two things.\n- Name booleans as questions: `isPaid`, `hasRefunds`.\n- Name functions with verbs: `createInvoice`, `formatCents`.\n- Avoid abbreviations except well-known ones (id, url, http).\n- Prefer plain objects and functions over classes, unless state needs a lifecycle.\n- Use `Object.freeze` for constant maps.\n- Do not mutate function arguments.\n- Prefer `Array.prototype.map/filter/reduce` over manual loops when it reads better.\n- Use optional chaining and nullish coalescing instead of long guards.\n- Do not leave commented-out code.\n- Use JSDoc for exported functions whose types are not obvious.\n- Keep imports sorted: node built-ins, then packages, then local files.\n- No unused variables or imports.\n- Avoid clever one-liners; optimise for the next reader.\n- Prefer template literals to string concatenation.\n\n## Error handling\n\n- Fail fast on invalid input at the boundary of a module.\n- Do not swallow errors. Log and rethrow, or handle completely.\n- Error messages SHOULD say what was wrong and what was expected.\n- Do not include secrets or personal data in error messages.\n- Use typed or coded errors so callers can branch on them.\n- Do not use exceptions for normal control flow.\n- Validate external input with explicit checks, not with try/catch around casts.\n- Retry only idempotent operations, with backoff and a limit.\n\n## Testing\n\n- Every behaviour change needs a test that fails before the change and passes after it.\n- Test behaviour, not implementation details.\n- Prefer small, fast unit tests; add integration tests for module boundaries.\n- One logical assertion per test where practical; name tests as sentences.\n- Use fixed clocks and seeded randomness; tests must be deterministic.\n- Do not depend on test order.\n- Do not hit the network in unit tests.\n- Keep fixtures small and inline when they are short.\n- Delete tests only together with the behaviour they cover.\n- A flaky test is a bug: fix it or quarantine it with a ticket.\n- Measure coverage, but do not chase a number.\n- When a bug is reported, write the failing test first.\n\n## Reviews\n\n- Every change needs one approving review; risky changes need two.\n- Review within one working day.\n- Review the design first, then the details.\n- Mark nits as `nit:` so the author can skip them.\n- Approve with comments when only nits remain.\n- Ask questions instead of making claims when you are unsure.\n- Do not review your own PR by switching accounts.\n- Large PRs (over 400 changed lines) SHOULD be split.\n\n## Documentation\n\n- Update the README when you change how to install, run or test the project.\n- Document every public function's contract: inputs, outputs, errors.\n- Keep architecture decisions in `tally/docs/adr/` with a date and status.\n- Remove documentation for removed features in the same PR.\n- Prefer examples over long prose.\n- Keep line length under 100 characters in Markdown.\n\n## Security\n\n- Treat all external input as untrusted.\n- Never log secrets, tokens, full card numbers or passwords.\n- Use parameterised queries; never build SQL with string concatenation.\n- Pin dependency versions and review new dependencies with the security team.\n- Do not add a dependency for a few lines of code.\n- Rotate any credential that was committed by mistake, even if the commit was reverted.\n- Use least privilege for service accounts.\n- Personal data stays in the regions the privacy policy names.\n- Report suspected incidents in #sec-incident at once.\n\n## Project notes: tally\n\n- Money is integer cents everywhere (`amountCents`). Parse input with `parseAmount()` and print with `formatCents()` from `tally/src/money.js`. Never use floats, `parseFloat` or `toFixed` for money.\n- Errors: throw `new TallyError(CODES.X, message)` from `tally/src/errors.js`. Add a new code to `CODES` when no code fits. Never `throw new Error`.\n- Time: use `nowIso()` from `tally/src/time.js` (tests inject a clock). Never call `new Date()` or `Date.now()` in `tally/src/`.\n- Logging: `log(event, fields)` from `tally/src/log.js`. No `console.log` in `tally/src/`.\n- Schema changes: add a new file `tally/migrations/NNNN_<name>.json`. Never edit an existing migration.\n- `tally/src/report.gen.js` is generated. Edit `tally/templates/report.tmpl.js`, then run `node tally/scripts/gen-report.mjs`.\n- Every user-visible change gets a line under `## Unreleased` in `CHANGELOG.md`.\n- Late fee: 1.25% of the unpaid balance (finance decision, 2026-09-15), rounded half up to the cent.\n- Run tests with `node --test` (no path). `node --test test/` and `npm test` fail on Node 25.\n\n## Performance\n\n- Measure before you optimise.\n- Prefer O(n) algorithms over clever caching.\n- Avoid synchronous file or network calls in request paths.\n- Paginate list endpoints; never return unbounded lists.\n- Set timeouts on every outbound call.\n- Cache only data with a clear invalidation rule.\n- Record latency percentiles, not only averages.\n- Load test changes to hot paths before release.\n\n## Observability\n\n- Emit structured logs with an event name and fields.\n- Use consistent field names across services: `invoiceId`, `customerId`, `amountCents`.\n- Do not log full request bodies.\n- Add a metric for every new background job: runs, failures, duration.\n- Alerts must be actionable; every alert links a runbook.\n- Trace IDs pass through every service call.\n- Keep dashboards next to the code that emits the metrics.\n\n## Dependencies\n\n- Prefer the standard library.\n- Every new dependency needs a reason in the PR description.\n- Update dependencies monthly; read the changelog of major versions.\n- Remove unused dependencies.\n- Lockfiles are committed and reviewed.\n\n## Releases\n\n- Releases follow semantic versioning.\n- Tag releases as `vMAJOR.MINOR.PATCH`.\n- Write release notes from the changelog.\n- Deploy during working hours; never on Friday afternoon.\n- Every release has a rollback plan.\n- Feature flags SHOULD guard risky changes.\n- Remove a feature flag within two releases after full rollout.\n\n## Data and migrations (general)\n\n- Back up before destructive data operations.\n- Data changes run in a transaction where the store supports it.\n- Large backfills run in batches with progress logs.\n- Never delete customer data without a ticket that names the policy that allows it.\n- Keep migration scripts idempotent where possible.\n\n## Accessibility and UI (for repositories with a UI)\n\n- Every interactive element is reachable by keyboard.\n- Colour is never the only signal.\n- Images have alt text; decorative images have empty alt text.\n- Respect reduced-motion settings.\n- Text contrast meets WCAG AA.\n\n## Working with coding agents\n\n- Give the agent the ticket, the acceptance criteria and the test command.\n- Review agent changes like any other change.\n- Agents MUST NOT push to the main branch.\n- Agents MUST NOT change CI configuration without a person's approval.\n- Ask the agent to explain risky changes in the PR description.\n- Keep agent instructions short and specific.\n- Delete agent instructions that no longer apply.\n\n## On-call\n\n- On-call engineers acknowledge pages within 15 minutes.\n- Write an incident note within one working day of a page.\n- Blameless postmortems for every customer-facing incident.\n- Hand over open issues at the end of each shift.\n\n## Glossary\n\n- Invoice: a request for payment from a customer.\n- Payment: money received against an invoice.\n- Open balance: invoice total minus payments.\n- Statement: the monthly email that contains the report.\n- Billing service: the HTTP service that uses tally.\n"
  },
  "hook": {
   ".claude/settings.json": "{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"node \\\"$CLAUDE_PROJECT_DIR/.claude/hooks/check-conventions.mjs\\\"\" }\n        ]\n      }\n    ]\n  }\n}\n",
   ".claude/hooks/check-conventions.mjs": "#!/usr/bin/env node\n// Stop hook: checks this repository's conventions before the agent finishes.\n// Exit 2 sends the problems back to the agent. After 3 blocks it lets the agent stop.\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();\nconst git = (...args) => execFileSync('git', args, { cwd: dir, encoding: 'utf8' });\nconst base = git('rev-list', '--max-parents=0', 'HEAD').trim().split('\\n')[0];\nconst tracked = git('diff', '--name-only', base).split('\\n').filter(Boolean);\nconst untracked = git('ls-files', '--others', '--exclude-standard').split('\\n').filter(Boolean);\nconst changed = [...new Set([...tracked, ...untracked])].filter((f) => existsSync(join(dir, f)));\nconst isNew = (f) => untracked.includes(f);\nconst inBase = (f) => { try { execFileSync('git', ['cat-file', '-e', `${base}:${f}`], { cwd: dir, stdio: 'ignore' }); return true; } catch { return false; } };\nconst addedLines = (f) => isNew(f)\n  ? readFileSync(join(dir, f), 'utf8').split('\\n')\n  : git('diff', '-U0', base, '--', f).split('\\n').filter((l) => l.startsWith('+') && !l.startsWith('+++')).map((l) => l.slice(1));\n\nconst problems = [];\nconst code = changed.filter((f) => /^(src|templates)\\/.*\\.js$/.test(f));\nfor (const f of code) {\n  for (const line of addedLines(f)) {\n    const l = line.replace(/\\/\\/.*$/, '');\n    if (/\\bnew Error\\(|\\bthrow Error\\(/.test(l)) problems.push(`${f}: throws a plain Error. Use new TallyError(CODES.X, message) from tally/src/errors.js; add a code to CODES if none fits.`);\n    if (f !== 'tally/src/time.js' && /new Date\\(|Date\\.now\\(/.test(l)) problems.push(`${f}: reads the system clock. Use nowIso() from tally/src/time.js.`);\n    if (/console\\.(log|info|warn|error)\\(/.test(l)) problems.push(`${f}: uses console. Use log(event, fields) from tally/src/log.js.`);\n    if (/toFixed\\(|parseFloat\\(/.test(l)) problems.push(`${f}: float money. Money is integer cents: use parseAmount() and formatCents() from tally/src/money.js.`);\n  }\n}\nfor (const f of changed.filter((f) => f.startsWith('tally/migrations/') && inBase(f))) {\n  problems.push(`${f}: an existing migration was edited. Migrations are append-only: restore it and add a new tally/migrations/NNNN_<name>.json.`);\n}\nconst config = JSON.parse(readFileSync(join(dir, 'tally/templates/report.config.json'), 'utf8'));\nconst expected = readFileSync(join(dir, 'tally/templates/report.tmpl.js'), 'utf8').replaceAll('__CURRENCY__', config.currency);\nif (readFileSync(join(dir, 'tally/src/report.gen.js'), 'utf8') !== expected) {\n  problems.push(changed.includes('tally/templates/report.tmpl.js')\n    ? 'tally/src/report.gen.js is out of date. Run node tally/scripts/gen-report.mjs.'\n    : 'tally/src/report.gen.js is generated: put the change in tally/templates/report.tmpl.js and run node tally/scripts/gen-report.mjs.');\n}\nif (code.length) {\n  const unreleased = (readFileSync(join(dir, 'CHANGELOG.md'), 'utf8').split('## Unreleased')[1] ?? '').split(/\\n## /)[0];\n  if (!unreleased.split('\\n').some((l) => l.trim().startsWith('-'))) problems.push('CHANGELOG.md: add a line under ## Unreleased for this change.');\n}\n\nif (!problems.length) process.exit(0);\nconst counter = join(dir, '.git', 'convention-hook-count');\nconst blocks = existsSync(counter) ? Number(readFileSync(counter, 'utf8')) : 0;\nif (blocks >= 3) process.exit(0);\nwriteFileSync(counter, String(blocks + 1));\nprocess.stderr.write(`Convention check failed:\\n${[...new Set(problems)].map((p) => `- ${p}`).join('\\n')}\\nFix these, run the tests, then finish.\\n`);\nprocess.exit(2);\n"
  },
  "curated-hook": {
   "CLAUDE.md": "# tally: notes for coding agents\n\n- Money is integer cents everywhere (`amountCents`). Parse input with `parseAmount()` and print with `formatCents()` from `tally/src/money.js`. Never use floats, `parseFloat` or `toFixed` for money.\n- Errors: throw `new TallyError(CODES.X, message)` from `tally/src/errors.js`. Add a new code to `CODES` when no code fits. Never `throw new Error`.\n- Time: use `nowIso()` from `tally/src/time.js` (tests inject a clock). Never call `new Date()` or `Date.now()` in `tally/src/`.\n- Logging: `log(event, fields)` from `tally/src/log.js`. No `console.log` in `tally/src/`.\n- Schema changes: add a new file `tally/migrations/NNNN_<name>.json`. Never edit an existing migration.\n- `tally/src/report.gen.js` is generated. Edit `tally/templates/report.tmpl.js`, then run `node tally/scripts/gen-report.mjs`.\n- Every user-visible change gets a line under `## Unreleased` in `CHANGELOG.md`.\n- Late fee: 1.25% of the unpaid balance (finance decision, 2026-09-15), rounded half up to the cent.\n- Run tests with `node --test` (no path). `node --test test/` and `npm test` fail on Node 25.\n",
   ".claude/settings.json": "{\n  \"hooks\": {\n    \"Stop\": [\n      {\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"node \\\"$CLAUDE_PROJECT_DIR/.claude/hooks/check-conventions.mjs\\\"\" }\n        ]\n      }\n    ]\n  }\n}\n",
   ".claude/hooks/check-conventions.mjs": "#!/usr/bin/env node\n// Stop hook: checks this repository's conventions before the agent finishes.\n// Exit 2 sends the problems back to the agent. After 3 blocks it lets the agent stop.\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();\nconst git = (...args) => execFileSync('git', args, { cwd: dir, encoding: 'utf8' });\nconst base = git('rev-list', '--max-parents=0', 'HEAD').trim().split('\\n')[0];\nconst tracked = git('diff', '--name-only', base).split('\\n').filter(Boolean);\nconst untracked = git('ls-files', '--others', '--exclude-standard').split('\\n').filter(Boolean);\nconst changed = [...new Set([...tracked, ...untracked])].filter((f) => existsSync(join(dir, f)));\nconst isNew = (f) => untracked.includes(f);\nconst inBase = (f) => { try { execFileSync('git', ['cat-file', '-e', `${base}:${f}`], { cwd: dir, stdio: 'ignore' }); return true; } catch { return false; } };\nconst addedLines = (f) => isNew(f)\n  ? readFileSync(join(dir, f), 'utf8').split('\\n')\n  : git('diff', '-U0', base, '--', f).split('\\n').filter((l) => l.startsWith('+') && !l.startsWith('+++')).map((l) => l.slice(1));\n\nconst problems = [];\nconst code = changed.filter((f) => /^(src|templates)\\/.*\\.js$/.test(f));\nfor (const f of code) {\n  for (const line of addedLines(f)) {\n    const l = line.replace(/\\/\\/.*$/, '');\n    if (/\\bnew Error\\(|\\bthrow Error\\(/.test(l)) problems.push(`${f}: throws a plain Error. Use new TallyError(CODES.X, message) from tally/src/errors.js; add a code to CODES if none fits.`);\n    if (f !== 'tally/src/time.js' && /new Date\\(|Date\\.now\\(/.test(l)) problems.push(`${f}: reads the system clock. Use nowIso() from tally/src/time.js.`);\n    if (/console\\.(log|info|warn|error)\\(/.test(l)) problems.push(`${f}: uses console. Use log(event, fields) from tally/src/log.js.`);\n    if (/toFixed\\(|parseFloat\\(/.test(l)) problems.push(`${f}: float money. Money is integer cents: use parseAmount() and formatCents() from tally/src/money.js.`);\n  }\n}\nfor (const f of changed.filter((f) => f.startsWith('tally/migrations/') && inBase(f))) {\n  problems.push(`${f}: an existing migration was edited. Migrations are append-only: restore it and add a new tally/migrations/NNNN_<name>.json.`);\n}\nconst config = JSON.parse(readFileSync(join(dir, 'tally/templates/report.config.json'), 'utf8'));\nconst expected = readFileSync(join(dir, 'tally/templates/report.tmpl.js'), 'utf8').replaceAll('__CURRENCY__', config.currency);\nif (readFileSync(join(dir, 'tally/src/report.gen.js'), 'utf8') !== expected) {\n  problems.push(changed.includes('tally/templates/report.tmpl.js')\n    ? 'tally/src/report.gen.js is out of date. Run node tally/scripts/gen-report.mjs.'\n    : 'tally/src/report.gen.js is generated: put the change in tally/templates/report.tmpl.js and run node tally/scripts/gen-report.mjs.');\n}\nif (code.length) {\n  const unreleased = (readFileSync(join(dir, 'CHANGELOG.md'), 'utf8').split('## Unreleased')[1] ?? '').split(/\\n## /)[0];\n  if (!unreleased.split('\\n').some((l) => l.trim().startsWith('-'))) problems.push('CHANGELOG.md: add a line under ## Unreleased for this change.');\n}\n\nif (!problems.length) process.exit(0);\nconst counter = join(dir, '.git', 'convention-hook-count');\nconst blocks = existsSync(counter) ? Number(readFileSync(counter, 'utf8')) : 0;\nif (blocks >= 3) process.exit(0);\nwriteFileSync(counter, String(blocks + 1));\nprocess.stderr.write(`Convention check failed:\\n${[...new Set(problems)].map((p) => `- ${p}`).join('\\n')}\\nFix these, run the tests, then finish.\\n`);\nprocess.exit(2);\n"
  }
 }
}
