Three-Layer Architecture
Purpose #
Explain the three-layer dependency structure that all aidokit packages obey, and the non-negotiable rules that protect it.
Big Picture #
From .docs/ARCHITECTURE.md §1, §4 and CLAUDE.md §4. The structure is strict-downward: a package may only depend on packages in its own layer (siblings — limited cases) or lower layers.
Layer 3 @aidokit/cli (user-facing shell)
│
Layer 2 @aidokit/adapter-* @aidokit/stack-pack-* (siblings; compose orthogonally)
│ │
Layer 1 @aidokit/core @aidokit/shared-docs @aidokit/stack-detect
@aidokit/prereq-check @aidokit/mcp-catalog @aidokit/base-skills
%%{init: {
"theme": "base",
"themeVariables": {
"fontFamily": "ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
"fontSize": "14px",
"primaryColor": "#eff6ff",
"primaryTextColor": "#0f172a",
"primaryBorderColor": "#2563eb",
"lineColor": "#475569",
"secondaryColor": "#f1f5f9",
"tertiaryColor": "#ffffff",
"clusterBkg": "#f8fafc",
"clusterBorder": "#cbd5e1"
}
}}%%
flowchart TB
classDef actor fill:#ede9fe,stroke:#6d28d9,color:#1e1b4b,stroke-width:1.2px;
classDef cli fill:#dbeafe,stroke:#1d4ed8,color:#0c1f4a,stroke-width:1.4px;
classDef adapter fill:#cffafe,stroke:#0e7490,color:#083344;
classDef pack fill:#dcfce7,stroke:#15803d,color:#052e16;
classDef core fill:#fef9c3,stroke:#a16207,color:#422006;
classDef artifact fill:#f1f5f9,stroke:#475569,color:#0f172a;
classDef stop fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d,stroke-dasharray:4 3;
classDef ok fill:#ecfdf5,stroke:#047857,color:#064e3b;
classDef external fill:#fff7ed,stroke:#c2410c,color:#431407;
subgraph L3 ["Layer 3 — user-facing shell"]
CLI["@aidokit/cli"]:::cli
end
subgraph L2 ["Layer 2 — extensions (siblings, never import each other)"]
direction LR
AD["@aidokit/adapter-*
claude-code, codex, copilot"]:::adapter
SP["@aidokit/stack-pack-*
node-ts, node-js, python, react, go"]:::pack
end
subgraph L1 ["Layer 1 — core + utilities"]
direction LR
CORE["@aidokit/core
(zero peer deps)"]:::core
SD["@aidokit/shared-docs"]:::core
DT["@aidokit/stack-detect"]:::core
PR["@aidokit/prereq-check"]:::core
MCP["@aidokit/mcp-catalog"]:::core
BS["@aidokit/base-skills"]:::core
end
CLI --> AD
CLI --> SP
AD --> CORE
SP --> CORE
SD --> CORE
DT --> CORE
PR --> CORE
MCP --> CORE
BS --> CORE
How It Works #
Layer 1 — Core + Utilities #
| Package | Knows about |
|---|---|
@aidokit/core |
Nothing else — the standalone contract package |
@aidokit/shared-docs |
@aidokit/core types only |
@aidokit/stack-detect |
@aidokit/core types only |
@aidokit/prereq-check |
@aidokit/core types only |
@aidokit/mcp-catalog |
@aidokit/core types only |
@aidokit/base-skills |
@aidokit/core types only |
The critical invariant: @aidokit/core has zero runtime dependencies on any other @aidokit/* package. A third-party adapter author installs only @aidokit/core to build against the contract. This is the standalone-SDK contract referenced in CLAUDE.md §4.
Layer 2 — Extensions (siblings) #
Adapters and stack packs are siblings — orthogonal extensions of Layer 1.
| Adapters | Stack packs |
|---|---|
@aidokit/adapter-claude-code |
@aidokit/stack-pack-node-ts |
@aidokit/adapter-codex |
@aidokit/stack-pack-node-js |
@aidokit/adapter-copilot |
@aidokit/stack-pack-python |
@aidokit/stack-pack-react |
|
@aidokit/stack-pack-go |
Rules:
- No adapter imports any stack pack.
- No stack pack imports any adapter.
- No adapter imports another adapter.
- No stack pack imports another stack pack.
- All Layer 2 packages depend on
@aidokit/core(directly or transitively).
Layer 3 — CLI Shell #
Only @aidokit/cli lives here. It is the only package allowed to consume all extensions.
Why this shape #
- Adapter portability. A Django stack pack written today must work under tomorrow's Aider adapter without modification — because they never know about each other.
- Standalone SDK. Third-party adapter authors install
@aidokit/coreand build against the interface without dragging in catalogs, prereq checks, or shared docs. - Capability bounding. Layer 1 packages have no IO ambitions; Layer 2 packages declare capabilities in manifests; only Layer 3 (the CLI) performs writes — through a staging directory under user confirmation. See .docs/docs/specs/security-model.md §5.
- Conformance harness sanity. Because adapters return data (not perform IO), the harness can test them in a pure-function style — no filesystem mocking required.
Example: where does a new "skill" land? #
A user wants to ship a new built-in skill across all adapters.
- The skill content goes into
@aidokit/base-skills/src/files/skills/<id>.md. - The skill metadata (
id,name,preloadedBy,required) goes into@aidokit/base-skills/src/skills.ts. - Each adapter's
emitSkillsconsumesSkillTemplate[]and writes it in its CLI's native format. No adapter change required — the new skill flows through automatically as long as@aidokit/clipasses the full base skill list (which it does viaCORE_BASE_SKILLS).
That separation is the load-bearing benefit of the layering.
Diagram #
See diagrams/system-overview.md.
Common Mistakes #
- Adding a runtime dependency from
@aidokit/coreto any other@aidokit/*package. Breaks the standalone-SDK contract — third-party adapter authors must not be forced to install our catalogs. - Importing a stack pack from an adapter (or vice versa). Breaks orthogonality. ESLint
no-restricted-importswill catch this once it lands (planned per CLAUDE.md §4). - Putting orchestration code in a Layer 2 package. Anything that needs to "decide what to do based on user selection across adapters + packs" belongs in
@aidokit/cli. - Creating a Layer 2 package that imports
@aidokit/cli. The dependency arrow only points down.
Checklist #
When proposing a change:
- [ ] Confirm which layer the new code belongs in.
- [ ] Confirm no new arrows go upward.
- [ ] Confirm no new arrows go sideways between Layer 2 siblings.
- [ ] If you must reach across Layer 2, the work belongs in
@aidokit/cli.