···66 "Bash(npm test:*)",
77 "Bash(npm run typecheck:*)",
88 "Bash(npm run lint:*)",
99+ "Bash(npm run lint:fix:*)",
1010+ "Bash(git checkout:*)",
911 "Bash(npm run format:*)"
1012 ]
1113 }
+59
CLAUDE.md
···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44+55+## Commands
66+77+```bash
88+npm run dev -- <args> # Run CLI in development (use this, not ./tangled)
99+npm run build # Compile TypeScript to dist/
1010+npm test # Run all tests once
1111+npm run test:watch # Run tests in watch mode
1212+npm run typecheck # Type-check without building (prefer over npx tsc --noEmit)
1313+npm run lint # Check with Biome
1414+npm run lint:fix # Auto-fix lint/format issues
1515+```
1616+1717+Run a single test file:
1818+```bash
1919+npx vitest run tests/commands/issue.test.ts
2020+```
2121+2222+## Architecture
2323+2424+`src/index.ts` is the entry point — it registers all commands and parses `process.argv`.
2525+2626+### Layer structure
2727+2828+- **`src/commands/`** — Commander.js command factories (e.g. `createIssueCommand()`). Each command: resumes session, gets repo context, calls lib functions, outputs results.
2929+- **`src/lib/`** — Business logic with no Commander dependency:
3030+ - `api-client.ts` — `TangledApiClient` wraps `AtpAgent`; `isAuthenticated()` is **synchronous**
3131+ - `session.ts` — OS keychain storage via `@napi-rs/keyring`; throws `KeychainAccessError` if keychain is inaccessible (not just missing)
3232+ - `context.ts` — Infers repo from `git remote` URLs; resolves `RepositoryContext` with owner DID/handle and repo name
3333+ - `issues-api.ts` — All issue CRUD; exports `IssueData` (canonical JSON shape), `getCompleteIssueData`, `resolveSequentialNumber`
3434+- **`src/utils/`** — Stateless helpers:
3535+ - `auth-helpers.ts` — `requireAuth(client)` throws if unauthenticated (use in lib functions); `ensureAuthenticated(client)` for commands (calls `resumeSession`, exits on failure)
3636+ - `validation.ts` — **All** validation logic lives here (Zod schemas + boolean helpers)
3737+ - `formatting.ts` — `outputJson<T extends object>(data, fields?)`, `formatDate`, `formatIssueState`
3838+ - `at-uri.ts` — Parse/build AT-URIs and repo AT-URIs
3939+ - `body-input.ts` — Reads `--body` / `--body-file` / stdin (`-F -`)
4040+- **`src/lexicon/`** — Auto-generated AT Protocol type definitions; regenerate with `npm run codegen`
4141+4242+### Key patterns
4343+4444+**Issue numbering** — Sequential numbers are not stored; they are computed by sorting all issues for a repo by `createdAt` ascending. The 1-based index is the display number.
4545+4646+**Issue state** — Stored as separate `sh.tangled.repo.issue.state` records. The latest record wins; default is `'open'` if no record exists.
4747+4848+**JSON output** — All issue sub-commands use `IssueCommand extends Command` (in `issue.ts`) to share a `--json [fields]` option. The canonical field set is: `number, title, body, state, author, createdAt, uri, cid`. Use `getCompleteIssueData()` to populate all fields.
4949+5050+**Auth flow** — Commands call `client.resumeSession()` directly, then proceed. Lib functions call `requireAuth(client)`. `KeychainAccessError` from `session.ts` propagates through `resumeSession()` without clearing metadata.
5151+5252+### Tests
5353+5454+Tests mirror `src/` under `tests/`. Command tests mock the entire `issuesApi` module:
5555+```typescript
5656+vi.mock('../../src/lib/issues-api.js');
5757+// Use importOriginal to preserve exported classes/errors if needed
5858+```
5959+`isAuthenticated()` is synchronous — mock as `vi.fn(() => true)`, not `vi.fn(async () => true)`.