diff --git a/AGENTS.md b/AGENTS.md index 9bb4d1d..c3484c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,16 @@ # AGENTS.md +> **Convention**: `CLAUDE.md` in this repo is a **symlink → `AGENTS.md`** (git +> mode `120000`). Same applies to every nested `**/CLAUDE.md`. Edit +> `AGENTS.md` only; never `Write`/`Edit` `CLAUDE.md` (it would replace the +> symlink with a regular file on Windows). Recreation recipe at the bottom. + +This file is for agentic coding tools (Claude Code, Codex, Cursor, GitHub +Copilot, etc.) working in this repository. + ## Project Overview -ghc-proxy is a reverse-engineered API translation proxy that converts GitHub Copilot's API into OpenAI and Anthropic compatible formats. **Unofficial, may break at any time.** +ghc-proxy is a reverse-engineered API translation proxy that converts GitHub Copilot's API into OpenAI- and Anthropic-compatible formats. It enables Claude Code, Cursor, and any OpenAI/Anthropic-speaking client to use a GitHub Copilot subscription. **Unofficial, may break at any time.** - **Runtime:** Bun >= 1.2 (first-class), Node.js compatible via `@elysiajs/node` fallback - **Language:** TypeScript (ESNext, strict mode) @@ -20,134 +28,140 @@ bun run lint:all # ESLint full scan (used in CI) bun run typecheck # tsc --noEmit bun test # Run all tests (Bun native test runner) bun test tests/validation.test.ts # Run a single test file -bun test tests/api-smoke.test.ts # Run API compatibility smoke tests +bun test tests/api-smoke.test.ts # Publish gate for public schema compatibility bun run start # Production server (NODE_ENV=production) -bun run matrix:live # End-to-end Copilot upstream compatibility (uses real quota) +bun run matrix:live # End-to-end Copilot upstream check (uses real quota — do not use as a sanity check) bun run smoke:packaged # Smoke test the packaged CLI bun run release:patch # Bump patch, commit, tag (then git push manually) ``` -**CI pipeline:** lint:all → typecheck → test → build → smoke:packaged +**CI pipeline order:** `lint:all → typecheck → test → build → smoke:packaged` -**Validation after non-trivial changes:** `bun run lint:all && bun run typecheck && bun test && bun run build` +**Local validation after non-trivial changes:** `bun run lint:all && bun run typecheck && bun test && bun run build` (same order as CI — fails fast before the slowest step). ## Compatibility Contract All public ghc-proxy endpoints must match the official client-facing schema they expose. -- OpenAI-facing routes must stay OpenAI-compatible at the proxy boundary. -- Anthropic-facing routes must stay Anthropic-compatible at the proxy boundary. -- Copilot-specific quirks must be handled inside the proxy via normalization, validation, routing, or translation. - -## Architecture +- OpenAI-facing routes stay OpenAI-compatible at the proxy boundary. +- Anthropic-facing routes stay Anthropic-compatible at the proxy boundary. +- Copilot-specific quirks are handled **inside** the proxy via normalization, validation, routing, or translation — never leaked outward. -### Request Flow +## Architecture (overview) ```text -Client Request → Elysia Route Handler → Zod Validation → Execution Strategy Selection → Adapter/Translator → Copilot Client → Response Translation → Client +Client → Guard → Ingest → Transform → Dispatch → Deliver → Client + (parse) (model chain) (strategy) (SSE/JSON) ``` -### Three Execution Paths for `/v1/messages` +Every route handler is a thin orchestrator of this 5-layer pipeline. Routes live in `src/routes//` as a `route.ts` + `handler.ts` + `strategy.ts` triple (`messages/` has multiple strategies under `strategies/`). -The proxy uses a per-model strategy pattern (`src/routes/messages/strategies/`) to choose the best upstream path: +`/v1/messages` has three execution strategies the registry picks between per model: **Native Messages** (direct passthrough), **Responses Translation** (Anthropic → Responses → Anthropic), and **Chat Completions Fallback** (Anthropic → OpenAI Chat → Anthropic). -1. **Native Messages** — Direct `/v1/messages` passthrough when Copilot supports it -2. **Responses Translation** — Anthropic → Responses → Anthropic when only `/responses` is available -3. **Chat Completions Fallback** — Anthropic → OpenAI Chat → Anthropic (legacy) +For everything beyond this overview — module map, abstractions, strategy details, routing logic, and translation coverage — see the design docs: -See `docs/messages-routing-and-translation.md` for routing logic and `docs/anthropic-translation-matrix.md` for translation coverage. +- `docs/design/execution-strategy.md` — strategy pattern and error handling +- `docs/design/model-routing.md` — model pipeline and context upgrade mechanics +- `docs/design/translation-pipeline.md` — full translation pipeline +- `docs/messages-routing-and-translation.md` — `/v1/messages` routing logic +- `docs/anthropic-translation-matrix.md` — translation coverage +- `docs/solutions/` — documented solutions to past problems (bugs, conventions, patterns), organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in documented areas. -### Request Pipeline +When making architectural changes, update the relevant design doc in the same change. -Every route handler is a thin orchestrator of the 5-layer pipeline: +## Adding a New Route -``` -Guard → Ingest → Transform → Dispatch → Deliver -``` +1. Create `src/routes//{route,handler,strategy}.ts` (use an existing simple route like `models/` or `embeddings/` as the template). +2. Implement an `ExecutionStrategy` (`src/lib/execution-strategy.ts`) — body prep, endpoint selection, response processing, error mapping. +3. Register the strategy in the route's `StrategyRegistry` and the route in the Elysia app (see `src/main.ts`). +4. Add a test under `tests/` and ensure `bun test tests/api-smoke.test.ts` still passes. -1. **Guard** (`src/guard/`) — Auth check and rate limiting, applied as an Elysia plugin. -2. **Ingest** (`src/ingest/`) — Protocol-specific parsing, Zod validation, and metadata extraction via `ProtocolRegistry`. -3. **Transform** (`src/transform/`) — Composable model transform chain (rewrite, beta-header processing, model policy). Messages route uses a 3-step chain; chat-completions and responses use single-step variants. -4. **Dispatch** (`src/dispatch/`) — Strategy selection via `StrategyRegistry`, execution, and error recovery (context-length retry with model upgrade). Messages route has 3 strategies; chat-completions and responses use single-strategy registries. -5. **Deliver** (`src/deliver/`) — Converts `ExecutionResult` into the HTTP response (SSE streaming or JSON serialization, error formatting, model mapping). - -### Key Modules - -| Directory | Purpose | -|-----------|---------| -| `src/routes/` | HTTP route handlers (each route is self-contained) | -| `src/translator/anthropic/` | Anthropic ↔ OpenAI protocol translation with IR, normalization, and streaming transducers | -| `src/translator/responses/` | Anthropic ↔ Responses format translation with signature codec | -| `src/adapters/` | Protocol adapters (OpenAI Chat, Anthropic Messages, Copilot transport) | -| `src/clients/` | GitHub, Copilot, and VS Code API clients | -| `src/core/capi/` | Copilot API compatibility layer (plan builder, profiles, request context) | -| `src/core/conversation/` | Conversation state management | -| `src/lib/` | Utilities (state, config, tokens, errors, model resolution, rate limiting, validation) | -| `src/types/` | TypeScript type definitions | -| `src/state/` | Decomposed state stores (AuthStore, ModelCache, ConfigStore, RateLimiter, EmulatorStore) | -| `src/pipeline/` | Pipeline framework: `runPipeline()` orchestrator with lifecycle hooks, StrategyContext and ModelTransformResult types | -| `src/ingest/` | Protocol registry with per-protocol parsers and validators | -| `src/transform/` | Composable model transform chain (rewrite, beta-headers, policy steps) | -| `src/dispatch/` | Strategy registry, strategy runner, error recovery, ResourceDispatcher | -| `src/deliver/` | Response delivery (SSE, JSON, error formatting, shared utilities) | -| `src/guard/` | Request guard (auth check, rate limiting) | - -### Key Abstractions - -- **ExecutionStrategy** (`src/lib/execution-strategy.ts`) — Unifies request body prep, endpoint selection, response processing, and error handling across all route handlers. -- **TranslationPolicy** (`src/translator/anthropic/translation-policy.ts`) — Tracks exact vs lossy vs unsupported behavior; validation rejects unsupported fields with 400 instead of silently dropping them. -- **ModelResolver** (`src/lib/model-resolver.ts`) — Maps model IDs (e.g. `claude-sonnet-4.6` → actual Copilot model) with configurable fallbacks. Only applies to the chat completions strategy path; native Messages and Responses strategies pass model IDs through as-is. -- **Global State** (`src/lib/state.ts`) — Cached models list, VS Code version, request counters, config. +If the route translates between protocols, add an entry to `docs/anthropic-translation-matrix.md` so coverage stays auditable. ## Code Conventions -- **Imports:** ESNext syntax only. Use `~/*` path alias for `src/*`. Prefer index exports (`~/clients`, `~/types`, `~/translator`). Use `import type` when possible. -- **Style:** `@antfu/eslint-config` flat config. Run `bun run lint --fix` to auto-fix. +- **Imports:** ESNext only. Use `~/*` path alias for `src/*`. Prefer index exports (`~/clients`, `~/types`, `~/translator`). Use `import type` where possible. +- **Style:** `@antfu/eslint-config` flat config. `bun run lint --fix` auto-fixes most issues. - **Types:** Strict TypeScript. No `any`. No unused locals/parameters. No switch fallthrough. `verbatimModuleSyntax` enabled. - **Naming:** `camelCase` for variables/functions, `PascalCase` for types/classes. - **Errors:** Explicit error classes in `src/lib/error.ts` (`HTTPError`, `throwInvalidRequestError`). No silent failures. - **Logging:** `consola` for human-readable output. For machine-readable output (e.g. `--json`), write clean data directly to stdout. -- **Testing:** Bun's built-in test runner (`bun:test`). Tests in `tests/*.test.ts`. Use `describe`/`test`/`expect` pattern. - **CLI:** `start` must remain an explicit subcommand. No default command. -- **Complexity:** Favor direct implementation over unnecessary abstractions. -- **Runtime:** Bun is first-class. Prefer Bun-native APIs unless cross-runtime support is explicitly needed. +- **Complexity:** Favor direct implementation over unnecessary abstractions. Three similar lines is better than a premature helper. +- **Scope discipline:** Fix only the issue the change targets. Don't refactor pre-existing duplication or "while you're there" — small, focused diffs review better and revert cleaner. +- **Runtime APIs:** Bun-native APIs (`Bun.file`, `Bun.serve`, `Bun.sleep`, etc.) are fine in `scripts/` and `tests/`. **Route code under `src/` must work on both Bun and Node** — use Web/Node standard APIs (`fetch`, `Response`, `crypto.subtle`, etc.). The CI smoke test runs the packaged CLI under Node via `@elysiajs/node`. ## Testing -- **Runner:** Bun built-in (`bun:test`). Place tests in `tests/`, name as `*.test.ts`. -- **Test helpers** (`tests/helpers.ts`): - - Model builders: `buildModel()`, `buildGptModel()`, `buildVisionModel()`, `buildModelsResponse()` - - Mock factories: `mockNonStreamingResponse()`, `mockStreamingResponse()`, `mockResponses()`, `mockMessages()`, `mockEmbeddings()` - - State snapshot/restore: `saveStateSnapshot()` / `restoreStateSnapshot()` for test isolation - - SSE stream utilities: `parseSse()`, `createStream()` - - Default state setup: `setupDefaultTestState()`, `clearConfig()` -- Tests use typed fixture arrays for parameterized cases. -- `tests/api-smoke.test.ts` is the publish gate for public schema compatibility. +See `tests/AGENTS.md` for the test-runner conventions, helper inventory, and fixture patterns. + +## Don't / Gotchas + +- **`dist/` is build output.** Never hand-edit; `bun run build` regenerates it from `src/`. +- **`bun run matrix:live` burns real Copilot quota.** Don't run it as a sanity check; use `bun test` or `bun run smoke:packaged` instead. +- **`shouldUse*()` helpers are legacy.** New feature-flag queries go through `ConfigStore` (`src/state/config-store.ts`). Don't add new `shouldUse*` call sites. +- **Don't silently drop unsupported fields in translators.** Use `TranslationPolicy` to mark behavior `exact`/`lossy`/`unsupported`; validation returns 400 for `unsupported`. Silent drops mask client bugs and bite later. +- **Conventional commits.** `fix:`, `feat:`, `docs:`, `chore:`, `refactor:`, `test:`. Squash-merge to `main` preserves the prefix in history. +- **Don't bypass `simple-git-hooks` with `--no-verify`.** If `lint-staged` complains, fix the lint — don't skip it. ## Pre-commit Hooks -`simple-git-hooks` runs `lint-staged` which runs `bun run lint --fix` on all staged files. +`simple-git-hooks` runs `lint-staged`, which runs `bun run lint --fix` on staged files. + +## Branch & PR Workflow + +- Feature branches with PRs; squash-merge into `main`. ## Release Automation -- **Tag-triggered release pipeline:** `.github/workflows/release-npm.yml` handles changelog + npm publish. -- **Version contract:** The workflow validates that `vX.Y.Z` matches `package.json` `version` before publish. -- **Publishing auth model:** npm Trusted Publishing (GitHub OIDC). No long-lived npm tokens. -- **Typical release flow:** `bun run release:patch` (or `:minor` / `:major`) to bump, commit, and tag, then `git push && git push --tags`. -- **Version immutability:** npm does not allow republishing an existing version. Always bump before tagging. +- **Tag-triggered pipeline:** `.github/workflows/release-npm.yml` handles changelog + npm publish. +- **Version contract:** Workflow validates `vX.Y.Z` matches `package.json` `version` before publish. +- **Auth model:** npm Trusted Publishing (GitHub OIDC). No long-lived npm tokens. +- **Typical flow:** `bun run release:patch` (or `:minor` / `:major`) to bump, commit, and tag → `git push && git push --tags`. +- **Immutability:** npm rejects republishing an existing version. Always bump before tagging. -## Design Documentation +## Agent Instruction File Symlink (Windows-safe recipe) -`docs/design/` contains architecture and design documents. When making architectural changes, update the relevant docs to keep them in sync with the code. +`CLAUDE.md` is a git symlink (mode `120000`) pointing to `AGENTS.md`. The +same convention applies to every nested directory: any `**/CLAUDE.md` is a +symlink to the `AGENTS.md` in the **same** directory. See +`docs/solutions/conventions/agent-instruction-symlink.md` for the rationale +and the footguns this avoids. -Key references: -- `docs/solutions/` — documented solutions to past problems, organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in documented areas. -- `docs/messages-routing-and-translation.md` — Routing logic for `/v1/messages` -- `docs/anthropic-translation-matrix.md` — Translation coverage between protocols -- `docs/design/model-routing.md` — Model pipeline design and context upgrade mechanics -- `docs/design/execution-strategy.md` — Strategy pattern and error handling -- `docs/design/translation-pipeline.md` — Full translation pipeline architecture +### Golden rule ---- +**Edit `AGENTS.md` only.** Never `Write`/`Edit` `CLAUDE.md` — on Windows +that replaces the symlink with a regular file and the two files drift again. -This file is tailored for agentic coding agents. +### Recreating a symlink (if it gets clobbered) + +```bash +# 1. Pick the clobbered path. For nested files, the target is still AGENTS.md +# because each CLAUDE.md points to the AGENTS.md in the same directory. +path=CLAUDE.md # or tests/CLAUDE.md, docs/foo/CLAUDE.md, etc. + +# 2. Write target path as file content (no trailing newline) +printf 'AGENTS.md' > "$path" + +# 3. Create blob WITHOUT trailing newline and register as symlink (mode 120000) +git update-index --add --cacheinfo \ + 120000,$(printf 'AGENTS.md' | git hash-object -w --stdin),"$path" + +# 4. Checkout to materialize the OS-level symlink +git checkout -- "$path" + +# 5. Verify +git ls-files -s "$path" # must show mode 120000 +``` + +Do NOT use `ln -s` (Git Bash on Windows silently makes a copy), `cp`, or +here-strings (`<<<`) with `git hash-object` (they append `\n` and break the +target). + +### After a fresh clone on Windows + +```bash +git config --local core.symlinks true +git ls-files -z 'CLAUDE.md' '**/CLAUDE.md' | xargs -0 git checkout -- +git ls-files -s 'CLAUDE.md' '**/CLAUDE.md' # all must show mode 120000 +``` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d4799aa..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,157 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -ghc-proxy is a reverse-engineered API translation proxy that converts GitHub Copilot's API into OpenAI and Anthropic compatible formats. It enables tools like Claude Code, Cursor, and any OpenAI/Anthropic-speaking client to use a GitHub Copilot subscription. **Unofficial, may break at any time.** - -- **Runtime:** Bun >= 1.2 (first-class), Node.js compatible via `@elysiajs/node` fallback -- **Language:** TypeScript (ESNext, strict mode) -- **Framework:** Elysia (HTTP server, `@elysiajs/node` for Node.js runtime), citty (CLI), Zod (validation) -- **Published as:** `ghc-proxy` npm package (single-file CLI at `dist/main.mjs`) - -## Commands - -```bash -bun install # Install dependencies (frozen lockfile in CI) -bun run dev # Start with --watch (hot reload) -bun run build # Bundle with tsdown -> dist/main.mjs -bun run lint # ESLint with cache -bun run lint:all # ESLint full scan (used in CI) -bun run typecheck # tsc --noEmit -bun test # Run all tests (Bun native test runner) -bun test tests/validation.test.ts # Run a single test file -bun run start # Production server (NODE_ENV=production) -bun run matrix:live # End-to-end Copilot upstream compatibility (uses real quota) -bun run smoke:packaged # Smoke test the packaged CLI -bun run release:patch # Bump patch, commit, tag (then git push manually) -``` - -**CI pipeline runs:** lint:all -> typecheck -> test -> build -> smoke:packaged - -**Validation after non-trivial changes:** `bun run lint:all && bun run typecheck && bun run build && bun test` - -## Architecture - -### Request Flow - -```text -Client Request -> Elysia Route Handler -> Zod Validation -> Execution Strategy Selection -> Adapter/Translator -> Copilot Client -> Response Translation -> Client -``` - -### Three Execution Paths for `/v1/messages` - -The proxy uses a per-model strategy pattern (`src/routes/messages/strategies/`) to choose the best upstream path: - -1. **Native Messages** — Direct `/v1/messages` passthrough when Copilot supports it -2. **Responses Translation** — Anthropic → Responses → Anthropic when only `/responses` is available -3. **Chat Completions Fallback** — Anthropic → OpenAI Chat → Anthropic (legacy) - -### Request Pipeline - -Every route handler is a thin orchestrator of the 5-layer pipeline: - -``` -Guard → Ingest → Transform → Dispatch → Deliver -``` - -1. **Guard** (`src/guard/`) — Auth check and rate limiting, applied as an Elysia plugin. -2. **Ingest** (`src/ingest/`) — Protocol-specific parsing, Zod validation, and metadata extraction via `ProtocolRegistry`. -3. **Transform** (`src/transform/`) — Composable model transform chain (rewrite, beta-header processing, model policy). Messages route uses a 3-step chain; chat-completions and responses use single-step variants. -4. **Dispatch** (`src/dispatch/`) — Strategy selection via `StrategyRegistry`, execution, and config-driven error recovery (context-length retry with model upgrade). Messages route has 3 strategies; chat-completions and responses use single-strategy registries. -5. **Deliver** (`src/deliver/`) — Converts `ExecutionResult` into the HTTP response (SSE streaming or JSON serialization, error formatting, model mapping). - -### Route File Pattern - -Each route follows a consistent structure: - -``` -routes// -├── route.ts # Elysia route definition -├── handler.ts # Parsing, validation, strategy dispatch -└── strategy.ts # ExecutionStrategy implementation(s) -``` - -Messages route is more complex with multiple strategies in `strategies/` subdirectory. - -### Key Modules - -| Directory | Purpose | -|-----------|---------| -| `src/routes/` | HTTP route handlers (each route is self-contained) | -| `src/translator/anthropic/` | Anthropic <-> OpenAI protocol translation with IR, normalization, and streaming transducers | -| `src/translator/responses/` | Anthropic <-> Responses format translation with signature codec | -| `src/adapters/` | Protocol adapters (OpenAI Chat, Anthropic Messages, Copilot transport) | -| `src/clients/` | GitHub, Copilot, and VS Code API clients | -| `src/core/capi/` | Copilot API compatibility layer (plan builder, profiles, request context) | -| `src/core/conversation/` | Conversation state management | -| `src/lib/` | Utilities (state, config, tokens, errors, model resolution, rate limiting, validation) | -| `src/types/` | TypeScript type definitions | -| `src/state/` | Decomposed state stores (AuthStore, ModelCache, ConfigStore, RateLimiter, EmulatorStore) | -| `src/pipeline/` | Pipeline framework: `runPipeline()` orchestrator with lifecycle hooks, StrategyContext and ModelTransformResult types | -| `src/ingest/` | Protocol registry with per-protocol parsers and validators | -| `src/transform/` | Composable model transform chain (rewrite, beta-headers, policy steps) | -| `src/dispatch/` | Strategy registry, strategy runner, error recovery, ResourceDispatcher | -| `src/deliver/` | Response delivery (SSE, JSON, error formatting, shared utilities) | -| `src/guard/` | Request guard (auth check, rate limiting) | - -### Key Abstractions - -- **ExecutionStrategy** (`src/lib/execution-strategy.ts`) — Unifies request body prep, endpoint selection, response processing, and error handling across all route handlers -- **TranslationPolicy** (`src/translator/anthropic/translation-policy.ts`) — Tracks exact vs lossy vs unsupported behavior explicitly; validation rejects unsupported fields with 400 instead of silently dropping them -- **ModelResolver** (`src/lib/model-resolver.ts`) — Maps model IDs (e.g. `claude-sonnet-4.6` -> actual Copilot model) with configurable fallbacks. Only applies to the chat completions strategy path; native Messages and Responses strategies pass model IDs through as-is -- **Global State** (`src/lib/state.ts`) — Cached models list, VS Code version, request counters, config -- **ModelTransformChain** (`src/transform/chain.ts`) — Composes rewrite, beta-header, and policy steps into a per-route transform chain. Eliminates inline model pipeline duplication across handlers. -- **ProtocolRegistry** (`src/ingest/registry.ts`) — Registry of protocol-specific parsers and context extractors. Each protocol registers a parse + extractMeta handler. -- **ConfigStore** (`src/state/config-store.ts`) — Typed config query interface replacing scattered `shouldUse*()` functions. Centralizes all feature flag queries. - -## Code Conventions - -- **Imports:** ESNext syntax only. Use `~/*` path alias for `src/*`. Prefer index exports (`~/clients`, `~/types`, `~/translator`). Use `import type` when possible. -- **Style:** `@antfu/eslint-config` flat config. Run `bun run lint --fix` to auto-fix. -- **Types:** Strict TypeScript. No `any`. No unused locals/parameters. No switch fallthrough. `verbatimModuleSyntax` enabled. -- **Naming:** `camelCase` for variables/functions, `PascalCase` for types/classes. -- **Errors:** Explicit error classes in `src/lib/error.ts` (`HTTPError`, `throwInvalidRequestError`). No silent failures. -- **Logging:** Use `consola` for human-readable output. For machine-readable output (e.g. `--json`), write clean data directly to stdout. -- **Testing:** Bun's built-in test runner (`bun:test`). Tests in `tests/*.test.ts`. Use `describe`/`test`/`expect` pattern. -- **CLI:** `start` must remain an explicit subcommand. No default command. -- **Complexity:** Favor direct implementation over unnecessary abstractions. -- **Runtime:** Bun is first-class. Prefer Bun-native APIs unless cross-runtime support is explicitly needed. - -## Testing - -- **Runner:** Bun built-in (`bun:test`). Place tests in `tests/`, name as `*.test.ts`. -- **Test helpers** (`tests/helpers.ts`): - - Model builders: `buildModel()`, `buildGptModel()`, `buildVisionModel()`, `buildModelsResponse()` - - Mock factories: `mockNonStreamingResponse()`, `mockStreamingResponse()`, `mockResponses()`, `mockMessages()` - - State snapshot/restore: `saveStateSnapshot()` / `restoreStateSnapshot()` for test isolation - - SSE stream utilities: `parseSse()`, `createStream()` - - Default state setup: `setupDefaultTestState()`, `clearConfig()` -- Tests use typed fixture arrays for parameterized cases. - -## Pre-commit Hooks - -`simple-git-hooks` runs `lint-staged` which runs `bun run lint --fix` on all staged files. - -## Branch & PR Workflow - -- Feature branches with PRs; squash merge into `main`. - -## Release Flow - -1. `bun run release:patch` (or `:minor`/`:major`) — bumps version, commits, tags -2. `git push && git push --tags` — triggers `release-npm.yml` workflow -3. Workflow validates tag matches `package.json` version, runs full CI, publishes to npm via GitHub OIDC Trusted Publishing (no long-lived npm tokens) - -## Design Documentation - -`docs/design/` contains architecture and design documents. When making architectural changes, update the relevant docs to keep them in sync with the code. - -Key references: -- `docs/solutions/` — documented solutions to past problems, organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Relevant when implementing or debugging in documented areas. -- `docs/messages-routing-and-translation.md` — Routing logic for `/v1/messages` -- `docs/anthropic-translation-matrix.md` — Translation coverage between protocols -- `docs/design/model-routing.md` — Model pipeline design and context upgrade mechanics -- `docs/design/execution-strategy.md` — Strategy pattern and error handling -- `docs/design/translation-pipeline.md` — Full translation pipeline architecture diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/solutions/conventions/agent-instruction-symlink.md b/docs/solutions/conventions/agent-instruction-symlink.md new file mode 100644 index 0000000..2d3b870 --- /dev/null +++ b/docs/solutions/conventions/agent-instruction-symlink.md @@ -0,0 +1,118 @@ +--- +title: AGENTS.md is the single source; CLAUDE.md is a git symlink +date: 2026-05-22 +category: conventions +module: documentation +problem_type: convention +component: documentation +severity: low +applies_when: + - "Repo carries both AGENTS.md and CLAUDE.md" + - "Editing project-wide agent instructions" + - "Running /init, /compound discoverability edits, or any automated writer that targets CLAUDE.md" + - "Adding a new subdirectory that needs its own scoped agent instructions" +tags: [agents-md, claude-md, symlink, windows, conventions] +--- + +# AGENTS.md is the single source; CLAUDE.md is a git symlink + +## Context + +Different agentic tools look for differently-named instruction files +(`AGENTS.md` for Codex/Cursor/general agents, `CLAUDE.md` for Claude Code). +Maintaining independent copies guarantees drift: one file gets the new +architecture note, the other keeps the old release flow, and neither agent +sees the union. + +In this repo, before unification, `AGENTS.md` and `CLAUDE.md` had diverged +across multiple commits — same scaffolding, different sections (one carried +`Compatibility Contract` + detailed `Release Automation`, the other carried +`Route File Pattern` + `ModelTransformChain`). Manual sync was already losing. + +## Guidance + +Keep **one** source of truth (`AGENTS.md`) and make `CLAUDE.md` a git +symlink (mode `120000`) pointing at it. The link is tracked in git as a +`120000`-mode blob whose content is the target path — every clone gets the +same link, and on Windows it materializes as a real symlink as long as +`core.symlinks=true` is enabled. + +**Golden rule**: edit `AGENTS.md` only. Never `Write`/`Edit` `CLAUDE.md` — +on Windows that replaces the symlink with a regular file and you're back to +two diverging copies. + +The exact Windows-safe recreation recipe, including the command to +rematerialize all tracked `**/CLAUDE.md` symlinks after enabling +`core.symlinks`, lives at the bottom of `AGENTS.md` itself (so future agents +discover it when reading the file they're allowed to edit). This doc focuses +on **why** and on the footguns to avoid; the recipe is not re-hosted here to +prevent drift between the two. + +### Subfolder scope + +The convention applies recursively. **Any** `CLAUDE.md` in any subdirectory +must be a symlink to the `AGENTS.md` in the **same** directory (not a path +that climbs back to the root) — that way nested directories can carry their +own scoped instructions without an accidental cross-scope link. + +Verify with: + +```bash +git ls-files -s '**/CLAUDE.md' 'CLAUDE.md' # all entries must be mode 120000 +``` + +## Why This Matters + +- **No drift.** Two agents reading two filenames see identical bytes — the + filesystem guarantees it, no review process required. +- **No tooling rewrites needed.** Claude Code keeps reading `CLAUDE.md`, + Codex/Cursor keep reading `AGENTS.md`. The convention is invisible to the + agents themselves. +- **Stays correct under `/init`-style automation.** Skills that target + `CLAUDE.md` would otherwise silently fork the files; the symlink survives + any `Edit` (which follows the link) but breaks under `Write` (which + replaces the inode). The banner at the top of `AGENTS.md` trains future + agents not to use `Write` on the link side. + +## When to Apply + +- Whenever a repo needs to be readable by **more than one** agentic tool + that expects a differently-named instructions file. +- When `/init`, `/compound`, or any other skill is about to write to + `CLAUDE.md` — redirect the write to `AGENTS.md` instead. +- When adding nested per-directory agent instructions — symlink the + `CLAUDE.md` in that directory to the `AGENTS.md` in the **same** + directory. + +## Examples + +### Wrong — `ln -s` or `cp` on Windows + +```bash +# Git Bash on Windows silently makes a COPY, not a symlink — git sees a +# regular file. The two files immediately start drifting again. +ln -s AGENTS.md CLAUDE.md + +# Same problem — replaces the link with a regular file. +cp AGENTS.md CLAUDE.md +``` + +### Wrong — here-string with `git hash-object` + +```bash +# `<<<` appends a trailing \n to stdin. The blob becomes "AGENTS.md\n", +# and the symlink target literally points to a path called "AGENTS.md\n" +# — broken on every platform. +git update-index --add --cacheinfo \ + 120000,$(git hash-object -w --stdin <<< 'AGENTS.md'),CLAUDE.md +``` + +The correct recipe (using `printf` with no trailing newline, then +`git update-index --cacheinfo 120000,...`, then `git checkout --`) lives at +the bottom of the repo's `AGENTS.md`. + +## Related + +- Root `AGENTS.md` — carries the convention banner and the recreation recipe. +- Sibling repo `Q:/repos/thoughtscape/ob-flow` — same convention with nested + `**/CLAUDE.md` symlinks, documented in `.claude/rules/symlinks.md`. diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 0000000..1dd06c8 --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,59 @@ +# tests/AGENTS.md + +> **Convention**: `tests/CLAUDE.md` is a symlink to this file (git mode +> `120000`). Edit this file only; never `Write`/`Edit` `tests/CLAUDE.md`. +> Recreation recipe lives at the bottom of the root `AGENTS.md`. + +Test-runner conventions and helper inventory for the `tests/` directory. +For project-wide rules see the root `AGENTS.md`. + +## Runner + +Bun's built-in test runner (`bun:test`). Place new tests in this directory +as `*.test.ts` and use the `describe` / `test` / `expect` pattern. + +```bash +bun test # All tests +bun test tests/validation.test.ts # Single file +bun test tests/api-smoke.test.ts # Publish gate for public schema compatibility +``` + +`tests/api-smoke.test.ts` is the **publish gate** for public schema +compatibility — if it fails, the package must not ship. + +## Helper inventory (`tests/helpers.ts`) + +| Group | Exports | +|-------|---------| +| Model builders | `buildModel()`, `buildGptModel()`, `buildVisionModel()`, `buildModelsResponse()`, `buildResponsesResult()` | +| App factory | `createApp()` — assembles an in-process Elysia app wired to mock Copilot transport | +| Mock factories | `mockNonStreamingResponse()`, `mockStreamingResponse()`, `mockResponses()`, `mockMessages()`, `mockEmbeddings()` | +| SSE utilities | `parseSse()`, `createStream()` | +| State isolation | `saveStateSnapshot()`, `restoreStateSnapshot()`, `setupDefaultTestState()`, `clearConfig()` | +| Assertions | `expectCacheCheckpoints()` | + +## Conventions + +- **Use typed fixture arrays for parameterized cases** (`test.each` or a + loop over a typed array of `{ name, input, expected }`). One `test` block + per case keeps failures localized. +- **Wrap state-mutating tests in snapshot/restore.** Call + `saveStateSnapshot()` in `beforeEach` and `restoreStateSnapshot()` in + `afterEach` so cross-test pollution doesn't appear as flaky behavior. + `setupDefaultTestState()` covers the common default config. +- **Don't mock what you can use real.** Prefer `createApp()` over hand-rolled + Elysia wiring; prefer the SSE parsing helpers over inline string splits. +- **Bun-native APIs are fine here.** Unlike `src/`, the test runtime is + always Bun. +- **No fixture files larger than ~5KB inline.** Move large captured payloads + to `tests/fixtures/` and import them. + +## When to add a test + +- Every new route gets at least one test covering the happy path. +- Every translation policy change (`exact` ↔ `lossy` ↔ `unsupported`) gets + a test asserting the new behavior, including the 400 path when + `unsupported`. +- Every new strategy gets a test verifying it's reachable through the + `StrategyRegistry`. +- Bug fixes get a regression test that fails before the fix. diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/tests/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file