taximeter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SPEC.md ADDED
@@ -0,0 +1,474 @@
1
+ # Taximeter specification
2
+
3
+ Taximeter is an open-source TypeScript meter and ledger for agent payments.
4
+ This specification defines the product, architecture, engineering standards, and
5
+ release acceptance criteria. The quickstart should provide useful results in
6
+ under 30 seconds. Implementation decisions and protocol corrections are recorded
7
+ in `DECISIONS.md` and `SPEC-NOTES.md`.
8
+
9
+ ---
10
+
11
+ ## 0. Protocol verification
12
+
13
+ The x402 descriptions below are a summary. Protocol details must be checked against
14
+ the primary sources before implementation:
15
+
16
+ - `https://github.com/coinbase/x402` — README, `specs/x402-specification-v2.md`,
17
+ `specs/schemes/exact/scheme_exact_evm.md`
18
+ - `https://docs.x402.org`
19
+ - The actual published npm packages (`@x402/core`, `@x402/evm`, `@x402/fetch`,
20
+ `@x402/express`) — read their `README` and exported types.
21
+
22
+ Then write `SPEC-NOTES.md` in the repo root recording:
23
+
24
+ 1. The exact v1 and v2 wire formats you found, with real field names.
25
+ 2. Every place where the brief below disagrees with the real spec.
26
+ 3. Which version(s) you decided to support and why.
27
+
28
+ **The real spec wins over this brief, every time.** If they conflict, follow the spec and
29
+ note it. Do not invent field names.
30
+
31
+ ---
32
+
33
+ ## 1. What you are building
34
+
35
+ ### The problem, in one paragraph
36
+
37
+ AI agents now pay for things by themselves — API calls, data, model tokens — thousands of
38
+ tiny payments per task, across several payment rails (x402/USDC, Stripe MPP, cards). The
39
+ rails answer one question well: *can this payment go through?* They do not answer: *has
40
+ this agent blown its budget, which task did this spend belong to, and how do I turn four
41
+ rails' records into one invoice?* Today that ledger responsibility falls on whoever runs
42
+ the agent — which means nobody does it.
43
+
44
+ The analogy to keep in your head while designing: **Visa exists, Ramp doesn't.** Card
45
+ networks move money; expense-management systems govern it. `taximeter` is the
46
+ expense‑management layer for agent payments.
47
+
48
+ ### What `taximeter` is
49
+
50
+ A local‑first meter and ledger that sits between an agent and the payment rails. It
51
+ records every payment, enforces budgets in real time, attributes spend to tasks, and
52
+ exports one clean statement.
53
+
54
+ **Name: `taximeter`.** npm package `taximeter`, CLI binary `taximeter` with the short
55
+ alias `txm`. The name was verified unregistered on the npm registry on 2026‑09‑06 — if it
56
+ has been taken since, fall back in this order: `tabmeter`, `spendbook`, `agenttab` (all
57
+ verified free on the same date), and record the change in `SPEC-NOTES.md`.
58
+
59
+ **The tagline is the product's whole pitch — use it verbatim in the README, the
60
+ `package.json` description, and the GitHub repo description:**
61
+
62
+ > A taximeter for your AI agents.
63
+
64
+ Carry the metaphor deliberately, and know its exact limits:
65
+
66
+ - A taximeter **watches small charges accumulate and shows a running total in a place the
67
+ passenger can see.** That is precisely what this tool does.
68
+ - A taximeter **does not take the money** — the driver does. This maps exactly onto the
69
+ non‑custody promise in §1. Lean on this when explaining what the tool is not.
70
+ - Do **not** stretch the metaphor into cars, taxis, rides, drivers, or yellow cabs
71
+ anywhere in the naming, UI copy, or visual design. No taxi imagery, no checkered
72
+ patterns, no yellow-cab color scheme. The name earns its keep in one sentence; after
73
+ that the product is a ledger and should look like one (§5).
74
+
75
+ **Capitalization — apply this table exactly and consistently. There is one rule behind it:
76
+ lowercase in every technical context, sentence case in prose.** "Taximeter" is also a
77
+ common English noun, so capitalizing it in prose is what marks it as the product rather
78
+ than the device in a cab.
79
+
80
+ | Context | Form | Note |
81
+ |---|---|---|
82
+ | npm package | `taximeter` | npm rejects uppercase in new package names — not a choice |
83
+ | CLI binary + alias | `taximeter`, `txm` | never capitalized; shells are case-sensitive |
84
+ | GitHub repo | `taximeter` | match the package name |
85
+ | Prose, headings, README H1, docs body | `Taximeter` | proper noun; `# Taximeter` |
86
+ | Inline reference to the command | `` `taximeter start` `` | code-styled, lowercase |
87
+ | TypeScript types / classes | `TaximeterConfig`, `PaymentEvent` | PascalCase |
88
+ | Env vars | `TAXIMETER_DB`, `TAXIMETER_PORT` | SCREAMING_SNAKE_CASE |
89
+ | Config file | `taximeter.config.json` | lowercase |
90
+ | State directory | `~/.taximeter/` | lowercase |
91
+ | HTTP headers | `Taximeter-Task`, `Taximeter-Agent` | Title-Case-With-Hyphens; **no `X-` prefix** — RFC 6648 deprecated it |
92
+ | Error codes / enum values | `blocked_by_taximeter` | snake_case, lowercase |
93
+ | Wordmark / logo | lowercase `taximeter` | a deliberate lowercase wordmark alongside sentence-case prose is fine and common |
94
+
95
+ Never write `TaxiMeter`, `TAXIMETER` (outside env vars), or `Taxi Meter`. Do not open a
96
+ sentence with the lowercase form — rewrite the sentence instead.
97
+
98
+ ### Explicit non‑goals — do not build these
99
+
100
+ These are not "later"; building them would make the project worse:
101
+
102
+ - **No custody.** It never holds funds, never holds private keys, never signs a payment.
103
+ - **No facilitator.** It does not verify or settle on‑chain. It observes and gates.
104
+ - **No cloud, no account, no telemetry.** Everything runs on localhost. Zero network calls
105
+ except passing traffic through to the upstream the agent was already calling.
106
+ - **No Stripe MPP or card rails in v1.** But the internal design must be adapter‑shaped so
107
+ a second rail is a new file, not a refactor. Write the `Rail` interface now; implement
108
+ only the x402 adapter.
109
+ - **No auth, no multi‑tenant, no RBAC.** Single developer on one machine.
110
+
111
+ ---
112
+
113
+ ## 2. Architecture
114
+
115
+ ```
116
+ agent process taximeter upstream
117
+ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
118
+ │ fetch(...) │ ──────► │ proxy (rail │ ─────► │ API server │
119
+ │ │ │ adapter: x402) │ │ (402 flow) │
120
+ └──────────────┘ ◄────── │ │ │ ◄───── └──────────────┘
121
+ │ ▼ │
122
+ │ policy engine │ ← budgets, allow/deny
123
+ │ │ │
124
+ │ ▼ │
125
+ │ ledger (SQLite)│
126
+ │ │ │
127
+ │ ▼ │
128
+ │ dashboard + API │ → localhost UI, CSV/JSON export
129
+ └──────────────────┘
130
+ ```
131
+
132
+ Three entry points, one core:
133
+
134
+ 1. **Proxy mode** (primary) — `taximeter start` runs an HTTP proxy. The agent points at it via
135
+ `HTTP_PROXY` / `HTTPS_PROXY` or an explicit base URL. Zero code change in the agent.
136
+ 2. **SDK mode** — `import { withMeter } from 'taximeter'` wraps a `fetch` implementation for
137
+ agents that can't use a proxy. Same core, different intake.
138
+ 3. **Dashboard** — served by the same process on a second port.
139
+
140
+ ### Core modules (each independently unit‑testable, no I/O in pure logic)
141
+
142
+ | Module | Responsibility |
143
+ |---|---|
144
+ | `rails/types.ts` | The `Rail` interface: `detect(req,res)`, `parse(...) → PaymentEvent \| null` |
145
+ | `rails/x402.ts` | Detect 402 responses and the payment replay; parse v1 and v2 shapes |
146
+ | `ledger/` | Append‑only event store; derived running totals; queries |
147
+ | `policy/` | Pure functions: given a proposed payment + current totals + config → allow or deny with a reason |
148
+ | `proxy/` | HTTP/HTTPS proxy wiring; calls rail → policy → ledger; blocks or forwards |
149
+ | `sdk/` | `withMeter(fetch, opts)` |
150
+ | `server/` | Dashboard static files + a small read‑only JSON API |
151
+ | `export/` | CSV, JSON, and a simple invoice |
152
+ | `cli/` | `start`, `report`, `export`, `reset`, `doctor` |
153
+
154
+ **Rule:** `policy/` and `ledger/` derivations must be pure and synchronous. All I/O lives
155
+ at the edges. This is what makes the tests fast and the logic trustworthy.
156
+
157
+ ---
158
+
159
+ ## 3. Data model
160
+
161
+ Append‑only events. Never update a row; derive everything.
162
+
163
+ ```ts
164
+ type PaymentEvent = {
165
+ id: string; // uuid v7 (sortable)
166
+ ts: string; // ISO 8601, UTC
167
+ rail: 'x402'; // future: 'stripe-mpp' | 'card'
168
+ status: 'observed' | 'blocked';
169
+ reason?: string; // set when blocked
170
+ amount: string; // integer string in the asset's smallest unit — NEVER a float
171
+ decimals: number; // e.g. 6 for USDC
172
+ asset: string; // contract address or symbol
173
+ assetSymbol?: string; // 'USDC'
174
+ network: string; // e.g. 'eip155:8453'
175
+ payTo: string;
176
+ payer?: string;
177
+ resource: string; // the URL being paid for
178
+ host: string; // derived, indexed
179
+ txHash?: string;
180
+ taskId?: string; // from Taximeter-Task header or SDK option
181
+ agentId?: string; // from Taximeter-Agent header or SDK option
182
+ raw: string; // the original parsed payload, JSON string, for auditability
183
+ };
184
+ ```
185
+
186
+ **Money rule, non‑negotiable:** all amounts are integer strings in the asset's smallest
187
+ unit. Never `number`, never floating point, anywhere in the codebase — including the UI and
188
+ the CSV. Convert to a display string only at the last render step. Write a lint rule or at
189
+ minimum a test that asserts no `parseFloat`/`Number()` touches an amount field.
190
+
191
+ **Storage:** SQLite via `better-sqlite3`. One table `events`, indices on `(ts)`,
192
+ `(taskId)`, `(agentId)`, `(host)`. Default path `~/.taximeter/ledger.db`, overridable.
193
+ Migrations in `migrations/` with a `schema_version` table — even for v1.
194
+
195
+ ### Config
196
+
197
+ `taximeter.config.json` in cwd, or `~/.taximeter/config.json`, or flags. Precedence:
198
+ flags > cwd > home > defaults. Ship a documented default that works with zero config.
199
+
200
+ ```jsonc
201
+ {
202
+ "budgets": {
203
+ "perTask": { "amount": "5000000", "asset": "USDC" }, // 5 USDC
204
+ "perAgent": { "amount": "50000000", "asset": "USDC", "window": "24h" },
205
+ "global": { "amount": "100000000", "asset": "USDC", "window": "24h" }
206
+ },
207
+ "policy": {
208
+ "allowHosts": [], // empty = allow all
209
+ "denyHosts": [],
210
+ "allowPayTo": [],
211
+ "maxSinglePayment": "1000000" // 1 USDC — anything larger is blocked
212
+ },
213
+ "ports": { "proxy": 8402, "dashboard": 8403 },
214
+ "db": "~/.taximeter/ledger.db"
215
+ }
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 4. Behaviour that must be exactly right
221
+
222
+ 1. **Observe, don't break.** If `taximeter` cannot parse a response, it forwards it
223
+ untouched and records a `parse_failed` diagnostic. **It must never corrupt or drop a
224
+ request it doesn't understand.** This is the single most important behaviour — a tool
225
+ that breaks agents will be uninstalled in one minute.
226
+ 2. **Block cleanly.** When a policy denies a payment, do not forward the replay. Return a
227
+ `402` to the agent with a JSON body: `{ error: 'blocked_by_taximeter', reason, budget,
228
+ spent, remaining }`. Record a `blocked` event. The agent must be able to understand
229
+ what happened from the response alone.
230
+ 3. **Budget arithmetic is exact.** Use `BigInt` for all totals. Windowed budgets are
231
+ computed from the event log, not from a running counter that can drift.
232
+ 4. **Multi‑asset.** Never sum across different assets. If two assets are in play, report
233
+ them separately and say so. Do not invent an exchange rate.
234
+ 5. **Idempotent.** The same payment observed twice (retry, proxy replay) must not double
235
+ count. Deduplicate on nonce/txHash/resource+ts within a small window; document the rule.
236
+ 6. **Graceful upstream failure.** If the upstream 5xx's after a payment was signed, record
237
+ the event with a `settlement_unknown` flag. Do not silently drop it.
238
+
239
+ ---
240
+
241
+ ## 5. The dashboard
242
+
243
+ This is where most tools of this kind look cheap. Do not build a generic admin template.
244
+
245
+ ### Art direction
246
+
247
+ It is a **ledger**, not an analytics dashboard. It should feel like a well‑set financial
248
+ document that happens to be live.
249
+
250
+ - **Type:** one grotesque for UI text, one monospace for every number. Load from Google
251
+ Fonts with real fallback stacks. All figures use `font-variant-numeric: tabular-nums` so
252
+ columns align.
253
+ - **Color:** a single accent. Choose a restrained one and use it only for the accent role —
254
+ the live total, the budget meter fill, active nav. Semantic colors (blocked = warning)
255
+ are separate from the accent and always ship with a text label, never color alone.
256
+ Neutrals should carry a slight hue bias toward the accent, not pure grey.
257
+ - **Both themes.** Define the complete light palette as CSS custom properties on bare
258
+ `:root`; redefine only the tokens under `@media (prefers-color-scheme: dark)`. Never
259
+ define a color only inside a media query. `body` sets an explicit background token.
260
+ - **No** gradients, glassmorphism, emoji section markers, drop shadows on every card, or
261
+ `rounded-2xl` on everything. Spend border/fill/shadow by role: lift the one thing that
262
+ matters, leave the rest flat.
263
+ - **Density over decoration.** A ledger view should show ~30 rows without scrolling on a
264
+ laptop.
265
+
266
+ ### Screens
267
+
268
+ 1. **Now** (default). A hero figure: current spend against the active budget, with a meter.
269
+ Below it: live event stream, newest first, ~30 rows, with a colored left edge only on
270
+ `blocked` rows. This screen is the whole product — someone should understand `taximeter`
271
+ in five seconds of looking at it.
272
+ 2. **By task / by agent / by host.** A sortable table with a proportional bar in the cell,
273
+ not a separate chart. Tabular numerals, right‑aligned amounts.
274
+ 3. **Timeline.** One sparkline of spend over the session plus a bar of spend per hour.
275
+ Single hue, direct value labels on the endpoints only, chart text in theme tokens, all
276
+ labels inside the viewBox, `overflow-x: auto` on the container.
277
+ 4. **Export.** Buttons for CSV / JSON / invoice, with a preview of what will be produced.
278
+
279
+ ### Implementation
280
+
281
+ React + Vite, prebuilt into `dist/ui/` at publish time and served as static files by the
282
+ Node process. `npx` must not trigger a frontend build. Poll a read‑only JSON API every
283
+ 1000 ms; no WebSocket in v1. The page must render a meaningful empty state that explains
284
+ how to point an agent at the proxy — never a blank screen.
285
+
286
+ ---
287
+
288
+ ## 6. The README
289
+
290
+ The README is the product's landing page. Prioritize a working quickstart and a
291
+ short path to the first useful result, with detailed reference material in the docs.
292
+
293
+ Structure, in this order:
294
+
295
+ 1. **Name + one line.** What it does, no adjectives. Then, immediately:
296
+ ```bash
297
+ npx taximeter start
298
+ ```
299
+ 2. **A 20‑second demo.** Leave a `docs/demo.gif` placeholder and write
300
+ `docs/RECORDING.md` with the exact `asciinema`/`vhs` commands to produce it, including
301
+ the script of what to type. Reference it from the README.
302
+ 3. **Why** — three sentences maximum, using the Visa/Ramp framing.
303
+ 4. **Quickstart** — three steps, each a single copyable command. Then a
304
+ "you should now see this" block showing real expected output.
305
+ 5. **How it works** — the ASCII architecture diagram from §2, plus four sentences.
306
+ 6. **Configuration** — a table of every option, its default, and one line of meaning.
307
+ 7. **What this is not** — the non‑goals from §1 stated plainly. This builds more trust than
308
+ a feature list.
309
+ 8. **Contributing / license (MIT).**
310
+
311
+ Rules: no more than three badges. No emoji as section markers. Every code block must be
312
+ copy‑pasteable and actually work. Write in plain English — short sentences, active voice,
313
+ no marketing adjectives.
314
+
315
+ ---
316
+
317
+ ## 7. Engineering standards
318
+
319
+ **Stack (pin exact versions in `package.json`, no `^` on tooling):**
320
+
321
+ - TypeScript 5.x, `strict: true`, `noUncheckedIndexedAccess: true`, no `any` outside
322
+ narrowly-scoped `// eslint-disable` with a reason.
323
+ - Node 20+ (`engines` field). ESM only, with a CJS build via `tsup` if trivial.
324
+ - `better-sqlite3` for storage; `commander` for CLI; `zod` for all config and wire‑format
325
+ parsing — every external input is parsed, never cast.
326
+ - **Biome** for lint + format (one tool, one config). No ESLint + Prettier pair.
327
+ - `vitest` for tests; `tsup` for build; `changesets` for versioning.
328
+
329
+ **Testing — this is not optional:**
330
+
331
+ - **Unit tests** for `policy/`, `ledger/` derivations, and both x402 wire‑format parsers.
332
+ These are pure functions; aim for ≥90% coverage here.
333
+ - **A fixture upstream server** in `test/fixtures/` that speaks real x402: returns a 402
334
+ with payment requirements, accepts the replay, returns 200. Both v1 and v2 shapes.
335
+ - **An integration test** that runs the proxy against the fixture, sends 100 payments
336
+ through, and asserts the ledger totals exactly, to the smallest unit.
337
+ - **A budget test**: configure a cap that allows exactly 20 payments; assert #21 is blocked,
338
+ that the response body is the documented shape, and that a `blocked` event is recorded.
339
+ - **A "don't break things" test**: send traffic the parser does not understand and assert
340
+ the response reaches the client byte‑identical.
341
+ - **A determinism test**: the same event log always produces the same totals.
342
+ - Snapshot the CSV export. Golden files in `test/__snapshots__/`.
343
+
344
+ **CI** — GitHub Actions, one workflow: typecheck → lint → test → build, on push and PR,
345
+ Node 20 and 22 matrix. A second workflow publishes to npm on a changeset release.
346
+
347
+ **Repo hygiene:** Conventional Commits. `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
348
+ `LICENSE` (MIT), `.editorconfig`, `.nvmrc`, issue and PR templates. `CHANGELOG.md`
349
+ generated by changesets. A `SECURITY.md` that says plainly: this tool never handles keys or
350
+ funds, and here is how to report an issue.
351
+
352
+ **`.gitignore` — write it deliberately, not from a generic template.** The correctness test
353
+ is mechanical: after running `npm ci && npm test && npm run build && npm pack`,
354
+ `git status --porcelain` must print **nothing**. Anything a normal workflow generates must
355
+ be ignored; anything a contributor needs must not be.
356
+
357
+ Must be ignored:
358
+
359
+ ```gitignore
360
+ # deps & build
361
+ node_modules/
362
+ dist/
363
+ *.tsbuildinfo
364
+ .vite/
365
+
366
+ # test & coverage
367
+ coverage/
368
+ .nyc_output/
369
+ test-results/
370
+ tmp/
371
+
372
+ # npm pack output
373
+ *.tgz
374
+
375
+ # local ledger state — SQLite writes three files, ignore all of them
376
+ *.db
377
+ *.db-wal
378
+ *.db-shm
379
+ .taximeter/
380
+
381
+ # local user config (an example file is committed instead)
382
+ taximeter.config.json
383
+
384
+ # secrets — this project never needs one, but a contributor's test setup might
385
+ .env
386
+ .env.*
387
+ !.env.example
388
+
389
+ # development logs
390
+ *.log
391
+
392
+ # editors & OS
393
+ .DS_Store
394
+ Thumbs.db
395
+ .idea/
396
+ .vscode/*
397
+ !.vscode/extensions.json
398
+ !.vscode/settings.json
399
+ ```
400
+
401
+ Must **not** be ignored — these are deliverables and belong in the repo:
402
+ `README.md`, `SPEC.md`, `SPEC-NOTES.md`, `DECISIONS.md`, `VERIFICATION.md`,
403
+ `taximeter.config.example.json`, `.changeset/`, `migrations/`,
404
+ `test/__snapshots__/`, `.github/`.
405
+
406
+ Two related rules:
407
+
408
+ - **Do not create a `.npmignore`.** The `files` whitelist in `package.json` already
409
+ controls the tarball, and having both is a known source of "why is this file missing
410
+ from the published package" bugs.
411
+ - **Add a `.gitattributes`** with `* text=auto eol=lf` and `*.sh text eol=lf`. Contributors
412
+ on Windows will otherwise commit CRLF, which breaks the lint step and produces
413
+ whole-file diffs in CI.
414
+
415
+ **Package:** `files` whitelist so the tarball ships only `dist/`, `dist/ui/`, and docs.
416
+ `bin` maps `taximeter` and `txm`. Verify `npm pack` output is under 2 MB and contains no source maps of the
417
+ UI.
418
+
419
+ ---
420
+
421
+ ## 8. Build order — commit at every step
422
+
423
+ Work in this order. After each step, run the full test suite and make a Conventional
424
+ Commit. Do not move on with failing tests.
425
+
426
+ 1. Repo scaffold, tooling, CI, a trivial passing test.
427
+ 2. `SPEC-NOTES.md` from your primary‑source reading.
428
+ 3. Types + zod schemas for the wire formats and config.
429
+ 4. `ledger/` with migrations + unit tests.
430
+ 5. `policy/` pure functions + unit tests (write the tests first here).
431
+ 6. `rails/x402.ts` parsers + unit tests against real captured payloads.
432
+ 7. Fixture upstream server.
433
+ 8. `proxy/` + the integration and "don't break things" tests.
434
+ 9. `cli/` — `start`, `report`, `export`, `reset`, `doctor`.
435
+ 10. `export/` + snapshot tests.
436
+ 11. `sdk/` `withMeter`.
437
+ 12. Dashboard — data API first, then UI, then the art‑direction pass.
438
+ 13. README, `docs/RECORDING.md`, contributing docs.
439
+ 14. `npm pack` and a clean‑machine smoke test (fresh temp dir, `npx ./package.tgz start`).
440
+
441
+ ---
442
+
443
+ ## 9. Acceptance criteria — the build is done when all of these pass
444
+
445
+ Verify each one and paste the evidence into a `VERIFICATION.md`.
446
+
447
+ - [ ] On a clean machine with no config file, `npx <pkg> start` boots the proxy and
448
+ dashboard and prints the two URLs plus a one‑line "point your agent here" hint.
449
+ - [ ] `npm test` passes; coverage on `policy/` and `ledger/` is ≥ 90%.
450
+ - [ ] The integration test pushes 100 payments and the ledger total matches the expected
451
+ value **exactly**, as an integer string.
452
+ - [ ] With a budget allowing 20 payments, payment 21 is blocked, the client receives the
453
+ documented JSON body, and a `blocked` event exists in the ledger.
454
+ - [ ] Unparseable traffic passes through byte‑identical.
455
+ - [ ] `taximeter export --csv out.csv` produces a file whose total equals the dashboard total.
456
+ - [ ] The dashboard renders correctly in both light and dark, at 1440px and 390px wide,
457
+ with a real empty state.
458
+ - [ ] `npm pack` tarball < 2 MB; `files` whitelist honoured.
459
+ - [ ] After `npm ci && npm test && npm run build && npm pack`, `git status --porcelain`
460
+ prints nothing — the `.gitignore` is complete and nothing generated is tracked.
461
+ - [ ] README's quickstart, executed literally on a clean machine, works.
462
+ - [ ] `SPEC-NOTES.md` documents every place this brief disagreed with the real spec.
463
+
464
+ ---
465
+
466
+ ## 10. Resolving uncertainty
467
+
468
+ - Resolve protocol ambiguity against the primary source. Support the narrowest
469
+ verified interpretation and document its limits in `SPEC-NOTES.md`.
470
+ - Product decisions must preserve the agent's traffic. Record each material
471
+ decision and its rationale in `DECISIONS.md`.
472
+ - Document unsupported requirements and specification corrections explicitly.
473
+ - Delivery priorities favor correct CLI behavior, ledger arithmetic, and traffic
474
+ preservation before dashboard polish.