Stack Plugin Guide
TL;DR. Each stack is a
stack.jsondeclaration that lives instacks/<name>/. The framework loads them via aStackRegistry. To add a new stack you write one JSON file; to customize an existing one you drop an override in.tausik/stacks/<name>/.
Why a plugin layout
Before v1.6 every stack was hardcoded in five places: project_types.DEFAULT_STACKS, bootstrap_config.STACK_SIGNATURES, gate_stack_dispatch._EXT_TO_STACKS, default_gates.DEFAULT_GATES, and project_config.STACK_GATE_MAP. Adding a stack meant editing all five, with a high drift risk. The plugin model collapses them into a single source of truth: stacks/<name>/stack.json. Consumers read the registry; the registry reads the JSON.
Built-in stacks
stacks/
├── _schema.json ← JSON Schema for stack.json (Draft-07)
├── python/
│ ├── stack.json
│ └── guide.md
├── typescript/
│ ├── stack.json
│ └── guide.md
└── ... ← 25 built-in stacks totalEach stack.json follows stacks/_schema.json. guide.md is the per-stack human-facing guide that surfaces in skill output.
stack.json fields
| Field | Type | Purpose |
|---|---|---|
name | string (required) | Lowercase slug. Must match ^[a-z][a-z0-9_-]*$. |
version | string | Free-form decl version. Default "1". |
extends | string | "builtin:NAME" to inherit from a built-in stack. Used in user overrides. |
detect | list of {file, type, keyword?} | Project-root signatures used by detect_stacks(). type ∈ exact / glob / dir-marker. Optional keyword filters file content. |
extensions | list of strings | File extensions (with leading dot) that signal this stack. Powers _EXT_TO_STACKS. |
filenames | list of strings | Lowercase filenames (e.g. dockerfile) that signal the stack regardless of extension. |
path_hints | list of strings | Path fragments (/playbooks/) used to disambiguate YAML/JSON in IaC trees. |
gates | object | Gate-name → gate-config dict. null value disables an inherited gate. |
guide_path | string | Relative path to the stack guide. Default "guide.md". |
extensions_extra | list of strings | Additive on top of inherited extensions. Useful in user overrides. |
Adding a new built-in stack
Create
stacks/<name>/stack.json. Minimum:json{ "name": "ruby", "detect": [{ "file": "Gemfile", "type": "exact" }], "extensions": [".rb"] }(Optional) Add
stacks/<name>/guide.mdwith sections:Testing,Review Checklist,Conventions,Common Pitfalls. The skills-maturity test asserts these are present.(Optional) Declare gates in the
gatesfield — for example, an RSpec gate scoped to ruby. The gate config followsdefault_gates.UNIVERSAL_GATESshape:enabled,severity,trigger,command,description,timeout,stacks.Run
python bootstrap/bootstrap.py --no-detectto refresh.claude/stacks/,_STACKS_ENUMin MCPtools.py, and the rest of the consumers.Run
pytest tests/test_stack_registry.pyandpytest tests/test_gates.pyto confirm nothing regressed.
Where the registry is read
The single import point is scripts/stack_registry.default_registry(). After v1.6 every consumer routes through it:
| Consumer | Field consumed |
|---|---|
project_types.DEFAULT_STACKS | all_stacks() |
bootstrap_config.STACK_SIGNATURES | signatures_for(name) |
gate_stack_dispatch._EXT_TO_STACKS | extensions_for(name) |
gate_stack_dispatch._FILENAME_TO_STACKS | filenames_for(name) |
gate_stack_dispatch._PATH_HINTS | path_hints_for(name) |
default_gates.DEFAULT_GATES (stack-scoped) | gates_for(name) |
project_config.STACK_GATE_MAP | derived from DEFAULT_GATES |
MCP tools.py _STACKS_ENUM | regenerated by bootstrap from all_stacks() |
Every consumer carries a hardcoded fallback so module import never crashes if the registry can't load.
Validation
scripts/stack_schema.validate_decl(decl, source) returns a list of actionable errors. Each error names the source file, the offending field, and the rule violated. Empty list = valid. Use it programmatically when generating decls, or run the full lint via tausik stack lint.
Testing
tests/test_stack_registry.py covers:
- Loading: missing dir, malformed JSON, schema-invalid, hidden directories, duplicates.
- Overrides:
extends,extensions_extraadditive merge,nullgate disable, per-key gate override, unknown extends target, standalone user stacks. - Reload + cache invalidation.
- Source tracking (
source_for,is_user_overridden).
Run: pytest tests/test_stack_registry.py -v.
DEFAULT_STACKS (25)
python, fastapi, django, flask, react, next, vue, nuxt, svelte, typescript, javascript, go, rust, java, kotlin, swift, flutter, laravel, php, blade, ansible, terraform, helm, kubernetes, docker. The list is open for extension via .tausik/config.json → custom_stacks.
See also
- Customization Guide — how to override a built-in stack without touching
stacks/<name>/. - Architecture — three-layer model and registry placement.