`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.
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
RequestErrorwhose 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
.catchonly 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 insrc/v1/acp-agent.tsto 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/loadreplay, the command list and command dispatch,promptandsteer. 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.
ClaudeAcpAgentserves v1, andsrc/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 inturnQueue, orforegroundWorkfor the session); on v2, an open turn of the v2 session. Don't write a narrower one. Anything that sendssession/updatemust 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
_metakeys 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, withhelpers.ts, the scenario harness (acp-scenarios/) andfixtures/. Before adding a test, searchsrc/tests/for existing tests of the same API and scenario, and add to their file anddescribeblock. Use the helpers inhelpers.tsandv1/session-doubles.tsinstead 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
originit 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.