ADR 0006: Stack-Pack Contract Shape
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-12 |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | 0001 (Runtime+language), 0005 (Adapter shape), 0007 (MCP catalog) |
Summary
The StackPack contract is a TypeScript interface with four required methods (detect, suggestSkills, suggestMCPs, suggestValidationCommands) plus one optional method (defaultArchitecturePattern) and a static manifest. Stack packs contribute data — skill templates, MCP catalog ids, validation commands — that adapters consume and emit in their CLI's native format. Stack packs MUST NOT emit files directly. The interface is intentionally narrower than the Adapter contract (ADR-0005) because stack packs supply project-convention knowledge; they do not target a CLI runtime.
This ADR captures the shape. The content lives in docs/specs/stack-pack-contract.md.
Context
Adapter authors (ADR-0005) know how to drive one CLI. Stack-pack authors know how one technology stack should be developed — what its skills are, which MCPs help, what lint/typecheck/test commands look like. These are orthogonal concerns. A Django stack pack should work whether the adapter is Claude Code or Codex; a Codex adapter should work whether the stack is Django, Node, or Go.
This orthogonality has a concrete consequence: stack packs and adapters must be siblings, not nested, and they must compose without knowing about each other. The Adapter contract (ADR-0005) consumes SkillTemplate[] in emitSkills. Where do those skills come from? From the selected stack packs. The wiring happens in @aidokit/cli, not in either extension.
Before defining the shape, we had to answer four questions:
- Do stack packs emit files, or contribute data? If they emit files, every stack pack needs to know which adapter it's running under — destroying orthogonality. If they contribute data, adapters format it.
- How many methods? The Adapter contract has 12 (ADR-0005). A stack pack with 12 methods would be overspecified for its responsibility.
- Async or sync? Detection involves filesystem reads; the rest is pure transformation of
ProjectContext. - What
Contextdoes each method receive? Detection happens before the user has made any selections; everything else happens after. The same context type would over-share to detection or under-equip suggestion methods.
Each question has a "wrong" answer that breaks one of the principles in .docs/ARCHITECTURE.md §2. This ADR records the right answers and why.
Decision
1. The contract is a TypeScript interface, not a YAML schema
Same reasoning as ADR-0005 §1. Suggestions and detection contain logic (if package.json has "react" AND "next", confidence = high); declarative DSL accretes into a programming language.
export interface StackPack {
readonly manifest: StackPackManifest;
detect(ctx: DetectContext): Promise<DetectionResult>;
suggestSkills(ctx: ProjectContext): SkillTemplate[];
suggestMCPs(ctx: ProjectContext): string[];
suggestValidationCommands(ctx: ProjectContext): ValidationCommand[];
defaultArchitecturePattern?(ctx: ProjectContext): ArchitecturePatternHint;
}
2. Stack packs contribute data; adapters emit files
This is the central rule. suggestSkills returns SkillTemplate[]. The adapter's emitSkills(ctx, skills) formats each template into its CLI's native skill mechanism. Same skill, two adapters, two emission paths — and the stack pack code is unchanged.
The benefit cascades:
- Stack packs are portable across adapters by construction.
- Stack packs cannot accidentally write to the project filesystem.
- The CLI controls staging, atomicity, conflict resolution for every emitted file.
- Conformance tests for stack packs can run without a filesystem; they assert on the data returned.
3. Four required methods, one optional
| Method | Required? | Concern |
|---|---|---|
detect |
Required | Does this project use this stack? |
suggestSkills |
Required | Which stack-specific skills should be available? |
suggestMCPs |
Required | Which catalog MCPs help this stack? (May return [].) |
suggestValidationCommands |
Required | What are the lint / typecheck / test commands? |
defaultArchitecturePattern |
Optional | Strict-conformance only; non-binding pattern hint. |
There is no suggestPrereqs, no suggestRoles, no emitX. Anything a stack pack might want to express is funnelled through these four contributions — or is the adapter's job.
4. defaultArchitecturePattern is opt-in and Strict-only
Architecture pattern hints are non-binding and at Strict conformance only. At Minimum or Standard the method is not consulted. This keeps the surface honest: stack packs don't force an architecture; they recommend one for users who opt into stricter discipline.
5. Detection uses a lighter DetectContext; suggestions use ProjectContext
DetectContext is intentionally weak:
- Filesystem read methods (
fileExists,readFile,glob,readManifest,manifestHasDep). - OS info.
- No user selections, no prereq results, no adapter information.
This is because detection runs before the user has chosen anything. Exposing ProjectContext at detection time would tempt authors to branch on the adapter (if ctx.adapter === 'codex' …), breaking orthogonality.
ProjectContext is passed to the four suggestion methods — at that point the user has selected an adapter, a conformance level, and the set of stack packs to enable, and the suggestions can be context-aware.
6. detect is async; the rest is sync
detect is async because it does filesystem I/O. The suggestion methods are sync because they are pure transformations of ProjectContext. This mirrors ADR-0005 §5 and gives the same testing simplicity: most stack-pack tests are pure function tests.
7. Determinism is mandatory
Given identical DetectContext/ProjectContext and identical filesystem state, every method MUST produce byte-identical output. This is what makes the byte-compare reference test in .docs/ARCHITECTURE.md §18.4 reliable — non-deterministic stack packs would break the dogfood gate.
8. Stack packs depend on @aidokit/core only
Stack packs MUST NOT import from any adapter package, any other stack pack, or @aidokit/cli. They depend only on @aidokit/core for types. This is enforced by ESLint no-restricted-imports once Phase 0 lands.
9. Reserved namespace
@aidokit/stack-pack-* is reserved for first-party packs. Third parties publish under their own scope (@acme/aidokit-stack-pack-django) or unscoped (aidokit-stack-pack-django). Same convention as adapters.
Consequences
Positive
- Portability across adapters. A stack pack written before the Codex adapter exists works under Codex when it ships, with no changes.
- Small surface. Four methods is enough to express "what does this stack look like?" without overspecification. Reviewing a stack pack PR is fast.
- Pure-function suggestions. Three of the four required methods are sync pure functions of
ProjectContext. Unit tests for them are one-liners. - No filesystem footprint at suggestion time. Stack packs cannot accidentally write outside the CLI's staging directory because they have no filesystem access at all post-detection.
- DetectContext keeps detection safe. Untrusted project directories cannot trigger arbitrary shell execution from inside
detect. - Determinism unlocks the dogfood gate. Byte-compare reference tests work because every component of file emission is deterministic.
- Composition is automatic. Selecting multiple stack packs concatenates their
SkillTemplate[]outputs; the adapter sorts and de-duplicates. No coordination between stack packs is required.
Negative
suggestMCPsreturns strings, not full MCP definitions. Stack packs cannot ship MCPs that don't exist in@aidokit/mcp-catalog. This is intentional — runtime MCP discovery is a v2.0 feature — but it adds friction for an author who wants to ship a niche MCP with their stack pack. Mitigation: PR the MCP into@aidokit/mcp-catalogfirst, then reference it.- Adapter must understand all
SkillTemplatetypes. If a stack pack invents a new skill metadata field, adapters need to consume it. Mitigation:SkillTemplateis in@aidokit/core; adding a field is a core release and adapter authors update on the next minor bump. - No "stack-pack hooks" beyond the four methods. A stack pack cannot, e.g., contribute to the
CLAUDE.mdcontent. Anything a stack pack wants to say about the project goes throughSkillTemplatecontent or the validation commands. Mitigation: this is by design; stack packs cannot extend the engine directory. - Naming convention diverges from npm idioms.
@aidokit/stack-pack-node-tsis verbose. We accept the verbosity for searchability.
Neutral
- The optional method is genuinely optional. Most stack packs will not implement
defaultArchitecturePattern. The TypeScript optional marker (?) makes this explicit at the type level. - Versioning follows
@aidokit/coreminor bumps for additive fields, major bumps for shape changes. Same as the Adapter contract.
Alternatives considered
Alternative 1: Stack packs emit files directly
The stack pack writes skills/*.md into the engine directory. The adapter sets the layout; the stack pack fills it.
Pros
- Stack pack author has full control over skill content placement.
- Fewer indirection layers.
Cons
- Stack pack must know the adapter's layout. Claude Code uses
.claude/skills/<slug>/SKILL.md; Codex uses something different. The stack pack now branches on adapter — orthogonality dead. - Filesystem coupling. Stack pack tests need a filesystem mock; the byte-compare reference test must coordinate stack-pack and adapter outputs.
- Conflict resolution fragments. If two stack packs write a skill with the same filename, the CLI doesn't know — the second overwrites the first silently. With data contribution, the CLI sees both and can de-dup or warn.
Reason rejected: orthogonality is the headline benefit of the sibling architecture. Direct file emission destroys it.
Alternative 2: One method, contribute(ctx): StackPackContribution
A single method returns one object containing skills, MCPs, validation commands, and pattern hints.
interface StackPack {
contribute(ctx: ProjectContext): StackPackContribution;
}
Pros
- Smaller interface (one method vs. four).
- Easier to add a new contribution type (just add a field to
StackPackContribution).
Cons
- Per-method conformance is impossible. Saying "Minimum requires
suggestSkillsbutsuggestMCPsis optional" requires per-method granularity. - Tests get coarser. Testing only the MCP suggestions means filtering one field of one return value.
detectcannot be merged intocontributebecause it runs at a different lifecycle stage. So the "one method" idea is at most "two methods" — and the savings vanish.
Reason rejected: the four-method split gives independent testability and per-method conformance with negligible boilerplate cost.
Alternative 3: Stack packs declare detection statically (no detect method)
The manifest declares detection rules: requires: ["package.json with react in dependencies"]. The CLI evaluates them.
Pros
- Detection is data, not code; can be audited without running anything.
- The CLI can run all detectors in parallel without sandboxing.
Cons
- Detection logic for real frameworks is not declarative. "React but not Next.js but not React Native" needs Boolean composition. "Django but only with Django REST Framework" needs a manifest scan. We'd reinvent a query language.
- No support for OS-specific or environment-specific detection. Some stacks have OS-coupled signals (e.g.,
.xcodeprojfor macOS Swift). - No support for parsed-manifest queries.
package.jsonis JSON,pyproject.tomlis TOML,Cargo.tomlis TOML,go.modis its own thing. A declarative DSL would need parsers for each.
Reason rejected: detection is the part of the contract where logic is most needed. Pushing it into static data trades simplicity now for a custom DSL later.
Alternative 4: Stack packs and adapters share one interface (Extension)
Both implement an Extension interface; the CLI discriminates by manifest type.
Pros
- Single extension point in the CLI loader.
- Polymorphism across extension types.
Cons
- Forces the union of both responsibilities onto every extension. Adapters get methods they don't use; stack packs get methods they don't use.
- Conformance is unclear — which methods does a stack pack need to implement at Standard?
- Goes against the explicit "siblings, not nested" decision from
.docs/ARCHITECTURE.md§1.
Reason rejected: the orthogonality is structural, and the interface should reflect it.
Alternative 5: Allow defaultArchitecturePattern at Standard, not only Strict
The optional architecture pattern hint could be consulted at Standard conformance.
Pros
- More projects benefit from the hint.
- Stack pack authors get more leverage.
Cons
- Standard conformance is meant to be unopinionated about architecture. An "architecture pattern" injected at Standard would feel imposed.
- Strict is the right level for "you opted into more discipline; here are the recommended patterns."
Reason rejected: keep Standard light. Users who want pattern hints can opt up to Strict.
Implementation notes
Const-export pattern
Same as adapters (ADR-0005 §"Implementation notes"):
// packages/stack-pack-node-ts/src/index.ts
import type { StackPack, StackPackManifest } from '@aidokit/core';
const manifest: StackPackManifest = {
name: 'node-ts',
displayName: 'Node.js + TypeScript',
languages: ['typescript', 'javascript'],
specVersion: '2.0',
conformance: 'standard',
};
export const stackPack: StackPack = {
manifest,
async detect(ctx) {
/* ... */
},
suggestSkills(ctx) {
/* ... */
},
suggestMCPs(ctx) {
/* ... */
},
suggestValidationCommands(ctx) {
/* ... */
},
};
Stateless, named export, no class.
Composability via concatenation
@aidokit/cli collects from all selected stack packs:
const skills = selectedStackPacks.flatMap((sp) => sp.suggestSkills(ctx));
const mcpIds = [...new Set(selectedStackPacks.flatMap((sp) => sp.suggestMCPs(ctx)))];
const validations = selectedStackPacks.flatMap((sp) => sp.suggestValidationCommands(ctx));
Duplicates in skills are surfaced as conflicts; duplicates in mcpIds collapse via the Set; duplicates in validations are kept (different stack packs may legitimately contribute different test runners).
Test scaffolding
The reference test pattern for a stack pack:
import { describe, it, expect } from 'vitest';
import { stackPack } from '../src/index.js';
import { makeDetectContext, makeProjectContext } from '@aidokit/core/test-utils';
describe('node-ts stack pack', () => {
it('detects on package.json with TypeScript', async () => {
const ctx = await makeDetectContext({
files: { 'package.json': '{"devDependencies": {"typescript": "^5"}}' },
});
const result = await stackPack.detect(ctx);
expect(result.matched).toBe(true);
expect(result.confidence).toBe('high');
});
it('suggests Context7 and Playwright', () => {
const ctx = makeProjectContext({ stack: { hasFrontend: true } });
expect(stackPack.suggestMCPs(ctx)).toContain('context7');
expect(stackPack.suggestMCPs(ctx)).toContain('playwright');
});
});
@aidokit/core/test-utils ships in v0.1 to make this pattern trivial; the same harness backs the conformance tests.
Conformance levels
Per the spec, stack packs declare a conformance level. The harness verifies:
- Minimum:
detect,suggestSkills,suggestMCPs,suggestValidationCommandsare implemented and return well-formed data for every test scenario. - Standard: Minimum +
suggestSkillsreturns at least one skill matching the declared languages; validation commands includelintandtypecheckif the stack supports them. - Strict: Standard +
defaultArchitecturePatternis implemented; suggested MCPs are tagged withtriggersthat resolve under the conformance harness's test contexts.
Validation
This decision is validated when:
- [ ] The
node-tsstack pack implements the full four-method interface and passes Standard conformance (Phase 4). - [ ] The
pythonstack pack (v0.5) passes the same conformance harness without changes to the contract. - [ ] No stack pack written between v0.1 and v1.0 needs an additional method added to the interface.
- [ ] A worked example in
docs/specs/stack-pack-contract.md§16 produces a passing stack pack in under 200 lines of code. - [ ] The Claude Code adapter consumes stack-pack outputs without importing any stack-pack package.
References
Internal
docs/specs/stack-pack-contract.md— the contract itself (the what; this ADR is the why)docs/specs/adapter-contract.md§4 — division of responsibility between adapter and stack packdocs/specs/mcp-catalog.md— the catalog whose idssuggestMCPsreferencesARCHITECTURE.md§6 — Stack-pack contract in system contextARCHITECTURE.md§14 — Stack detection model- ADR 0001 — Runtime + language
- ADR 0005 — Adapter Contract Shape (sibling decision)
- ADR 0007 — MCP Catalog Shape (target of
suggestMCPs)
External
- "Composition over Inheritance" — Eric Elliott (relevant to the siblings-not-nested decision)
- RFC 2119 — keyword conventions used in the contract spec