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:

  1. 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.
  2. How many methods? The Adapter contract has 12 (ADR-0005). A stack pack with 12 methods would be overspecified for its responsibility.
  3. Async or sync? Detection involves filesystem reads; the rest is pure transformation of ProjectContext.
  4. What Context does 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:

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:

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

Negative

Neutral


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

Cons

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

Cons

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

Cons

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

Cons

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

Cons

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:


Validation

This decision is validated when:


References

Internal

External