Files
claude-agent-acp/AGENTS.md
Ben Brandt bc031f362c test: split src/tests/ into v1/, v2/ and shared tests (#1567)
`src/tests/` was flat, so it didn't follow the `src/v1/` / `src/v2/`
layout.

- **`src/tests/v1/`** holds the 37 tests that import `src/v1/`, plus
`session-doubles.ts`, which builds v1 sessions.
- **`src/tests/v2/`** holds the tests of `src/v2/` and `serve.ts`:
`v2.test.ts`, `v2-integration.test.ts`, `acp-v2.test.ts` and
`acp-scenarios-v2.test.ts`.
- **The top level** keeps the tests of shared modules, plus
`helpers.ts`, `setup-no-network.ts`, the scenario harness
(`acp-scenarios/`) and `fixtures/`.

A test goes to `v1/` when it imports the v1 agent, even if it is mostly
about a shared module. For example, `providers.test.ts` and
`fast-mode-config.test.ts` drive `ClaudeAcpAgent` for some of their
cases.

Only paths change. Imports and `vi.mock`/`vi.importActual` specifiers
gain one `../`. The `here`-relative paths to the goldens, the
origin/main recordings, `package.json` and `fixtures/` were updated too.
`AGENTS.md`, `docs/air-extensions.md` and the scenario files' doc
comments now give the new paths. No test, golden file or recording
changed. A script checked that every relative module specifier under
`src/tests/` resolves, because a `vi.mock` path that doesn't resolve
mocks nothing and raises no error.

Test results:
- `npx tsc --noEmit`, `npm run check`, `npm run build`: pass
- `npx vitest run`: 3939 passed / 33 skipped, the same as before the
move.
- `RUN_INTEGRATION_TESTS=1 npx vitest run`: 3964 passed / 8 skipped.
2026-11-10 09:07:09 +01:00

7.3 KiB

Agent guide

An ACP-compatible coding agent powered by the Claude Agent SDK. Source in src/, TypeScript, ESM, Node >= 22. src/v1/ holds the v1 agent (ClaudeAcpAgent in src/v1/acp-agent.ts), src/v2/ the v2 agent, and the rest of src/ the modules both share. The package exports only src/lib.ts. ESLint rejects imports between src/v1/ and src/v2/, and from shared modules into either agent. The entry points (index.ts, lib.ts, serve.ts, binary-entry.ts) are exempt.

Commands

npm run build         # tsc
npm run test:run      # vitest, single pass
npm run check         # eslint + prettier --check
npm run lint:fix      # eslint --fix
npm run format        # prettier --write

CI runs format:check, lint, build and test:run. Run npm run check before opening a PR.

Coding guidelines

  • Keep edits consistent with the surrounding code's formatting, naming and idioms.
  • Keep comments concise and concrete. Name the actual state, call or failure instead of alluding to it, prefer a short example over an abstract description, and avoid vague phrases like "keeps it honest". A comment or doc must be true on every code path it describes, and a doc that lists what a result omits or rejects must name the actual cases.
  • When a request can't be served, throw a RequestError whose message says what happened and what to do (e.g. "Try again when the turn ends"). Don't drop the request silently.
  • Don't report success for work you start without awaiting it. If the detached call can still refuse and its .catch only logs, check the refusal condition first or report the failure to the client.
  • A result must not depend on what an earlier call happened to cache. A request right after a restart, with cold caches, must return the same thing as one after a warm-up.
  • If code relies on an ordering or call sequence that nothing enforces (e.g. an echo arriving before the result), say so in a short comment.

Architecture and design fit

  • Before coding, read the code the change plugs into and find the module that owns the state or concept. Build on that module, not beside it.
  • Don't add new features or components to src/v1/acp-agent.ts. If no existing module fits the new code cleanly, create a dedicated one that owns the feature's state and logic, and keep the change in src/v1/acp-agent.ts to the calls that wire it in.
  • Prefer extending an existing mechanism over adding a new one, even if it takes a small refactor (moving a helper, generalizing a function). Add a parallel path, special case or flag only when the existing structure can't support the change.
  • Before writing new logic, check whether it already exists in a sibling handler of the same flow or in a helper that works with the same data, and reuse it. Paths that handle the same input must apply the same rules through the same code: the live stream and the session/load replay, the command list and command dispatch, prompt and steer. Two copies of one check drift apart.
  • Put guards, filters and cleanup in the module that owns the state, so every caller gets them. If guards, flags or fallbacks pile up in one place, restructure instead of adding another.
  • If a result is meant to match another request's output, reuse that request's logic.
  • The adapter serves ACP v1 and v2. A change that is not meant for one protocol version only must work correctly in both. ClaudeAcpAgent serves v1, and src/v2/ serves v2; they share modules (the SDK mapping, tool call rendering, permissions, config options). Put a change meant for both versions in the shared module, or make it in both cores.
  • A session runs one turn at a time. On v1, "Claude is working" means startTurn's check (an unsettled turn in turnQueue, or foregroundWork for the session); on v2, an open turn of the v2 session. Don't write a narrower one. Anything that sends session/update must go through the turn queue or be refused while the session is busy, or its output lands inside another turn. Keep the session marked busy across awaited network calls.
  • Don't branch on a local copy of state that Claude Code or a remote service owns: it goes stale when the other side changes. Have a command set the state the user asks for instead of toggling it.
  • A command or name the adapter adds must not shadow one that Claude Code or the user's own commands and skills already provide.
  • New optional request fields and _meta keys must not reject requests that were valid before.
  • If a workaround or new path is unavoidable (e.g. a Claude Code or SDK limitation), say why in a short code comment and in the PR description.

Testing guidelines

  • Tests live in src/tests/: v1/ holds the tests that drive the v1 agent, v2/ the tests of the v2 agent, and the top level the tests of shared modules, with helpers.ts, the scenario harness (acp-scenarios/) and fixtures/. Before adding a test, search src/tests/ for existing tests of the same API and scenario, and add to their file and describe block. Use the helpers in helpers.ts and v1/session-doubles.ts instead of copying setup.
  • Don't add cases that take the same code path. Tests that differ only in input and expected output belong in one it.each. Don't repeat another test's assertions; assert only what this test is about.
  • Reach a state the shortest way (e.g. set session.tornDown = true) instead of driving a full flow with extra mocks, unless the flow is what you test.
  • Build test doubles from what the bundled Claude Code CLI really sends, including fields like origin it echoes back and fields it omits. A double simpler than the real message hides bugs. If unsure, check against the real CLI (npm run test:integration).
  • Cover both ACP v1 and v2 when a change affects what a client receives (updates, responses, replay). A change to internal logic only does not need its tests duplicated per version.
  • For each behavior the docs or comments promise, including what is omitted or rejected, have a test that would fail if it broke.

Pull requests

Squash merges use the PR title as the commit subject, and release-please parses it to compute the next version. Titles must be conventional commits using one of: feat, fix, perf, revert, docs, style, chore, refactor, test, build, ci. conventional-prs.yml rejects anything else.

The title also decides the release: feat: bumps the minor, fix:/perf: the patch, and chore:/ci:/test: and friends do not release at all.

Releasing

Stable releases are fully automated by release-please. There is no manual release workflow, and the version is never chosen by hand — it follows from the commit history.

npm run release:preflight     # verifies it is safe to release, prints the PR and version
gh pr merge <pr-number> --squash

The preflight is the guard-list as code; if it exits non-zero, follow what it prints rather than merging.

Every other push to main publishes a preview to npm — 0.73.1-preview.4 and so on — under the preview dist-tag, tags the commit it came from, and updates the agent registry the same way a release does. Only latest is reserved for real releases. So anything merged to main is published within minutes; there is no staging branch. Full runbook, including how to recover a stalled release: docs/RELEASES.md.