ADR 0005: Adapter Contract Shape
| Field | Value |
|---|---|
| Status | Accepted |
| Date | Pre-alpha |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | 0001 (Runtime+language), 0006 (Stack-pack shape), 0007 (MCP catalog) |
Summary
The Adapter contract is a TypeScript interface with 12 distinct methods split by concern (emitAgentRulesFile, emitVerbs, emitRoles, emitSkills, emitWatchdog, installMCP, etc.) plus a static manifest declaring identity and capabilities. Methods return data (EmittedFile[], ShellCommand[]) rather than performing IO; the CLI orchestrates the actual writes. Adapters and stack packs are siblings that compose orthogonally — neither imports the other.
This ADR captures the shape; the content lives in docs/specs/adapter-contract.md.
Context
The Adapter contract is the most consequential abstraction in aidokit. Every CLI runtime (Claude Code, Codex, future tools) that wants to be supported must implement this contract. A poorly-shaped contract means:
- Adapter authors fight the abstraction when their CLI doesn't match our model
- Testing becomes hard if methods are too coarse or too entangled
- Type safety leaks if the contract isn't expressible in the type system
- Capability creep if methods are added ad hoc whenever a new CLI feature is needed
- Breaking changes accumulate because the shape can't accommodate evolution
We had to pick the contract shape before any adapter was implemented, because:
- The Claude Code adapter is the v0.1 deliverable, and it builds against this contract
- The Codex adapter (v1.0) is the second proof; the contract must accommodate two different CLI mechanics
- Third-party adapters (post-1.0) are an explicit roadmap item, and they can't be served by a contract that only fits Claude Code
- The
@aidokit/cliorchestration code is written to this contract; changing the contract means rewriting the CLI
A contract redesign post-1.0 means a spec major bump (per docs/specs/adapter-contract.md §13) and migration scripts for every existing adapter. The cost is high enough that we need to commit to a shape before the first line of adapter code is written.
Decision
1. The contract is a TypeScript interface, not a YAML schema or declarative manifest
export interface Adapter {
readonly manifest: AdapterManifest;
emitAgentRulesFile(ctx: ProjectContext): EmittedFile;
emitEngineConfig(ctx: ProjectContext): EmittedFile[];
emitVerbs(ctx: ProjectContext): EmittedFile[];
emitRoles(ctx: ProjectContext): EmittedFile[];
emitSkills(ctx: ProjectContext, skills: SkillTemplate[]): EmittedFile[];
emitWatchdog(ctx: ProjectContext): EmittedFile[];
emitOutputStyles(ctx: ProjectContext): EmittedFile[];
installMCP(mcp: MCPDef, roles: RoleName[], ctx: ProjectContext): ShellCommand[];
removeMCP(mcpId: string, ctx: ProjectContext): ShellCommand[];
listInstalledMCPs(ctx: ProjectContext): Promise<InstalledMCP[]>;
postInstall(ctx: ProjectContext): ShellCommand[];
preSync(ctx: ProjectContext): SyncPlan;
doctor(ctx: ProjectContext): Promise<DoctorCheck[]>;
}
Adapter authors implement this interface in TypeScript (or JavaScript with TS types as type-only imports). The conformance harness verifies behavior at runtime.
2. Methods are split by concern, not consolidated
Each emission concern gets its own method:
emitAgentRulesFile— the CLAUDE.md / AGENTS.md fileemitEngineConfig— settings/config filesemitVerbs— workflow verb implementations (slash commands, prompt files)emitRoles— role definitionsemitSkills— skill filesemitWatchdog— hook scripts / validatorsemitOutputStyles— optional output-format enforcement
Rather than a single emit(ctx): EmittedFile[] method that returns everything mixed together.
3. Manifest + behavior split
Adapters expose two things:
- Static manifest (
AdapterManifestobject) declaring identity, conformance level, capabilities, gaps, target CLI version - Behavioral methods (the 12 above) implementing the actual work
The CLI can inspect the manifest without invoking any method. This enables aidokit manifest, aidokit search, conformance verification, and capability audits without executing adapter code.
4. Methods return data; the CLI performs IO
Every emission method returns plain data objects (EmittedFile[], ShellCommand[]). The CLI is responsible for:
- Actually writing files to disk
- Actually executing shell commands
- Staging, atomicity, rollback
- User confirmation gates
Adapters do not call fs.writeFile. They do not spawn child processes. They are pure providers of intent.
5. Sync where possible, async where IO-bound
- File emission methods are synchronous (
emitAgentRulesFile,emitVerbs, etc.) because they're pure computation - IO-bound methods are async (
listInstalledMCPs,doctor) because they query the live target CLI
This matches the actual nature of each operation and lets the conformance harness test sync methods without ceremony.
6. Adapters and stack packs are siblings, not nested
- Adapter contract: how to talk to a target CLI
- Stack-pack contract: how to provide project-convention knowledge
- They compose orthogonally: one adapter + N stack packs
- Neither imports the other; both import from
@aidokit/core
7. Capability profile mapping is the adapter's job
The spec defines three capability profiles (read-only, docs-only-write, scope-limited-write). The adapter maps each profile to its CLI's mechanism (permissionMode + hooks for Claude Code; sandbox + approval policy for Codex). The CLI does not know how the mapping works; it only knows that the adapter declares one and the manifest disclosure says how strong the enforcement is.
8. The manifest declares gaps honestly
When an adapter cannot satisfy a spec requirement (e.g., Codex has no hook mechanism), the manifest's gaps array documents it. Marketing prompt-level enforcement as deterministic is a conformance violation. Honest gaps > hidden gaps.
Consequences
Positive
- Type safety across the contract surface. Adapter authors get IDE autocomplete, refactoring tools, and compile errors when their implementation diverges from the interface. This catches whole classes of bugs before tests run.
- Methods are independently testable. Unit tests can verify
emitVerbswithout runninginstallMCP. Mock fixtures stay small. - Conformance harness is mechanical. The harness invokes each method against known inputs and asserts the shape and content of the output. No file system, no shell — pure I/O of data.
- Manifest enables introspection without execution.
aidokit manifest,aidokit doctor, marketplace listings, and conformance audits all work without instantiating the adapter's methods. - CLI controls IO atomicity. Because adapters return data and the CLI writes, the CLI can stage all writes atomically — partial failures are recoverable.
- Dry-run is automatic.
--dry-runjust skips the write step; adapters don't have to implement a separate dry-run path. - Adapters are portable across CLI versions. As long as the adapter's
Adapterinterface matches the contract version, the CLI invokes it the same way regardless of how the target CLI evolves. - Composition is straightforward. Multiple stack packs feed
SkillTemplate[]intoemitSkills; the adapter formats them into its CLI's native skill mechanism. No coupling. - Async only where it makes sense. Sync methods are simpler to test, debug, and reason about. The async methods (
listInstalledMCPs,doctor) are async because they actually need to be. - Honest gaps surface as data. The
gapsarray is a first-class field, not an afterthought. Adapter authors disclose; users see.
Negative
- 12 methods is more surface than necessary for trivial adapters. A minimum-viable adapter (the worked example in adapter-contract.md §12) still implements all 12. Mitigated: most return
[]or simple shells for stubs. - TypeScript interface excludes non-TS adapter authors. A Python or Go developer must work in TS to implement an adapter. Mitigated: adapters are mostly file emission, not language-dependent algorithms; the TS layer is thin.
- No runtime-pluggable extension points. Adding a new method to the interface is a breaking change requiring a spec bump. This is the right trade — runtime-pluggable interfaces would be more flexible but less verifiable.
- Manifest-vs-behavior split adds boilerplate. Adapters must implement both. Mitigated: most fields in the manifest are static strings; the manifest object is one literal.
- Stack pack contract sits separately. Adapter authors need to know about stack packs to consume
SkillTemplate[]fromemitSkills. Mitigated: a 2-paragraph mention in adapter-contract.md §4 explains the division.
Neutral
- The interface will evolve. Spec major bumps (v2.0, v3.0) will refine the contract. The migration script policy in adapter-contract.md §14 makes this manageable.
@aidokit/corebecomes a peer dependency. Adapter packages declare@aidokit/coreaspeerDependencyto avoid duplicate type instances. Standard pattern.
Alternatives considered
Alternative 1: Pure declarative manifest (YAML/JSON only, no code)
The adapter is just a static config file declaring "for verb X, emit file Y with template Z."
Pros
- Trivial to write
- No code execution required
- Language-agnostic — Python and Go authors can write adapters too
- Easy to audit
Cons
- Cannot make runtime decisions (e.g., "if conformance is Strict, emit additional output styles")
- Cannot integrate with the host CLI's runtime APIs (
claude mcp addis a shell command, but config layouts may vary by version) - Cannot perform any computation (interpolation, validation, version checks)
- Template expressiveness becomes a programming language by accretion (every new requirement adds DSL syntax)
Reason rejected: the moment we need a single conditional branch, we'd reinvent a programming language. JSON/YAML is fine for data (manifest, MCP catalog); not for behavior (contract methods).
Alternative 2: Single emit(ctx): EmittedFile[] method
One method that returns every file the adapter wants to emit.
Pros
- Simpler interface (one method instead of seven
emit*methods) - Adapter author has total flexibility internally
Cons
- Testing requires mocking the whole adapter for any single concern
- The CLI can't ask "just emit the skills" — it gets everything
- Conformance harness must reverse-engineer which output corresponds to which concern
- Per-method conformance levels become impossible (can't say "Minimum needs verbs but skills are optional")
- Refactoring inside the adapter is harder because all concerns live in one method
Reason rejected: split methods are more boilerplate but vastly more testable and granular. The conformance harness alone justifies the split.
Alternative 3: Adapter as plugin with arbitrary hooks
The CLI defines lifecycle phases (e.g., 50+ hooks: beforeDetect, afterDetect, beforePromptAdapter, afterPromptAdapter, …). Adapters subscribe.
Pros
- Maximum extensibility — adapter authors can hook anywhere
- Decouples adapter timing from the CLI's flow
Cons
- 50+ extension points become a maintenance nightmare
- Hook order ambiguity (which adapter runs first?)
- Hook conflicts (two adapters subscribing to the same hook with incompatible side effects)
- Plugin frameworks of this shape (think: webpack, Babel) are notoriously hard to reason about
- Testing requires mocking the hook system
Reason rejected: complexity explosion. The 12-method interface gives the same expressive power with vastly less surface area.
Alternative 4: Inheritance hierarchy (BaseAdapter class)
abstract class BaseAdapter {
abstract emitVerbs(ctx): EmittedFile[];
emitAgentRulesFile(ctx): EmittedFile {
/* default impl */
}
// ...
}
class ClaudeCodeAdapter extends BaseAdapter {
emitVerbs(ctx) {
/* override */
}
}
Pros
- Default implementations reduce boilerplate
- Familiar OOP pattern
Cons
- TypeScript / modern JS favor composition over inheritance
- Class-based abstractions are harder to compose (can't be a "partial adapter")
- Default implementations can hide spec violations (an author forgets to override and the default passes silently)
- Multiple inheritance issues if we ever need cross-cutting concerns
Reason rejected: interfaces force every author to make every decision explicitly. Defaults that hide gaps are anti-aligned with the "honest about gaps" principle.
Alternative 5: Interface segregation into many tiny interfaces
interface AgentRulesEmitter { emitAgentRulesFile(ctx): EmittedFile; }
interface VerbEmitter { emitVerbs(ctx): EmittedFile[]; }
interface RoleEmitter { emitRoles(ctx): EmittedFile[]; }
// ...
class ClaudeCodeAdapter implements AgentRulesEmitter, VerbEmitter, RoleEmitter, ... {}
Pros
- True interface segregation principle
- Adapters could implement only the interfaces they need
Cons
- Conformance becomes "which interfaces did you implement?" — fragmented
- The CLI has to runtime-check which interfaces are implemented
- Type union types for the CLI become unwieldy
- No real flexibility gained — every adapter implements ~all the interfaces anyway
Reason rejected: ISP solves a problem we don't have. One coherent interface with documented "MAY return empty array" for optional methods is simpler.
Alternative 6: Adapter writes files directly (no CLI orchestration of IO)
The adapter performs fs.writeFile calls internally; the CLI just invokes it.
Pros
- Less data shuffling — adapter writes what it wants directly
- Adapter has more control over file layout
Cons
- No staging / no atomicity. Partial failure leaves the project in a broken state.
- No dry-run. Adapter must implement its own dry-run path or skip files conditionally.
- No conflict detection. Adapter would have to read existing files and decide whether to overwrite. Currently the CLI does this once.
- Testing requires mocking the filesystem. Currently methods are pure functions of
ProjectContext. - Capability auditing is broken. The manifest's
writesPathsclaim is unverifiable without static analysis of the adapter's code. - Conformance harness can't intercept writes. Currently the harness gets
EmittedFile[]and asserts on its content.
Reason rejected: every advantage of direct writes is illusory; every disadvantage is severe.
Alternative 7: JSON Schema describing files to emit
The adapter declares "I will emit files matching this JSON Schema" and the CLI generates them from the schema + project context.
Pros
- Maximally declarative
Cons
- JSON Schema can't express the actual file content, only its structure
- Wildly insufficient for things like a 200-line skill markdown file
- We'd end up embedding templates in JSON anyway, with JSON-escaped multiline strings
Reason rejected: Schemas describe shapes, not content. Wrong tool.
Implementation notes
How adapter authors implement the contract
// packages/adapter-claude-code/src/index.ts
import type { Adapter, AdapterManifest } from '@aidokit/core';
const manifest: AdapterManifest = {
name: 'claude-code',
// ... full manifest
};
export const adapter: Adapter = {
manifest,
emitAgentRulesFile(ctx) {
/* ... */
},
emitEngineConfig(ctx) {
/* ... */
},
// ... all 12 methods
};
The package exports adapter as a named export. @aidokit/cli does:
const adapterPkg = await import('@aidokit/adapter-claude-code');
const adapter: Adapter = adapterPkg.adapter;
Why a const object, not a class
// Preferred
export const adapter: Adapter = {
/* ... */
};
// Not preferred
export class ClaudeCodeAdapter implements Adapter {
/* ... */
}
A const object cannot be instantiated multiple times with different state — which is what we want, since adapters are stateless. Classes invite state via this, which we don't need and which complicates the conformance harness.
Type-only imports for non-TS authors
JavaScript-only adapter authors can use type-only imports:
// packages/aidokit-adapter-mytool/src/index.js
/** @type {import('@aidokit/core').Adapter} */
export const adapter = {
manifest: {
/* ... */
},
emitAgentRulesFile(ctx) {
/* ... */
},
// ...
};
This gives JSDoc-based type checking without compiling TypeScript.
Conformance harness shape
import { runConformanceTests } from '@aidokit/core/conformance';
const results = await runConformanceTests(adapter, 'standard');
// returns ConformanceResult[] with per-requirement pass/fail
Internally, the harness:
- Builds a synthetic
ProjectContextfor each test scenario - Invokes the relevant method
- Asserts the returned data matches the requirement
- Reports a pass/fail with the specific RFC clause violated
Capability profile mapping in practice
Each adapter declares how it maps capability profiles. For Claude Code:
const PROFILE_MAP = {
'read-only': { permissionMode: 'plan' },
'docs-only-write': { permissionMode: 'default', preToolUseHook: 'validate-docs-only.mjs' },
'scope-limited-write': { permissionMode: 'default', preToolUseHook: 'validate-scope.mjs' },
};
For Codex (sketch):
const PROFILE_MAP = {
'read-only': { sandbox: 'read-only', approvalPolicy: 'untrusted' },
'docs-only-write': { sandbox: 'workspace-write' /* + role-prompt discipline */ },
'scope-limited-write': { sandbox: 'workspace-write' /* + role-prompt discipline */ },
};
The adapter's emitRoles method uses its PROFILE_MAP to populate the right capability fields in each role's frontmatter / config.
Validation
This decision is validated when:
- [ ] The Claude Code adapter implements the full 12-method interface and passes Standard conformance (Phase 4)
- [ ] The Codex adapter implements the full 12-method interface and passes Minimum conformance (v1.0)
- [ ] Conformance harness catches at least one intentional violation in a test adapter (Phase 4–8)
- [ ] An external developer can read
docs/specs/adapter-contract.md+ ADR 0005 and produce a working stub adapter in under 2 hours - [ ] No method has been added or removed from the interface between v0.1 alpha and v1.0 GA (signal that the shape was right)
References
docs/specs/adapter-contract.md— the contract itself (what; this ADR is the why)docs/specs/stack-pack-contract.md— sibling contractARCHITECTURE.md§2.2 — design principle: contracts as TypeScript interfacesARCHITECTURE.md§5 — Adapter interface in system context- ADR 0001 — Runtime + language (Node.js + TypeScript)
- ADR 0006 — Stack-pack shape (sibling decision)
- ADR 0007 — MCP catalog shape
- Upstream
orchestrator-spec.md§17 — Adapter contract requirements at the spec level
External
- "Designing Effective Interfaces" — TypeScript Handbook
- "Composition over Inheritance" — Eric Elliott
- "Interface Segregation Principle" — Robert C. Martin