Skip to content
stacks
Docs menu

Entry file format

The layout of a stack or fragment directory and every key, verify step field, marker and template variable in STACK.md and FRAGMENT.md.

View as MarkdownOpen in ClaudeOpen in ChatGPT

A stack is a directory holding STACK.md, and a fragment is a directory holding FRAGMENT.md. Each file is YAML frontmatter the CLI reads, followed by Markdown guidance the CLI copies into the brief. This page is the exact format, version 1. For how to write a good entry, see Write your own stack or fragment.

Directory layout

Every registry root (bundled, index, user or project) has the same shape:

stacks/
  go-cli/
    STACK.md
    files/            # copied into every project
      .gitignore
      Makefile.tmpl
    adapters/
      claude/         # copied only when claude is a target agent
        .claude/settings.json
fragments/
  postgres/
    FRAGMENT.md
    files/
search/               # optional search cards
  stacks/go-cli.json
  fragments/postgres.json
Registry root

The directory name is the entry's id, and it must match the id key. Files under files/ go into every project at the same relative path. Files under adapters/<agent>/ go in only when that agent is a target; the agents are claude, codex, opencode and cursor. Store files under their real names, dotfiles included: files/.gitignore is written as .gitignore.

An entry directory may not be or contain a symlink, and nothing past one is read. In a project registry (./.stacks/), the root and its stacks/ and fragments/ must be real directories; a symlinked one is skipped with a warning. The user and bundled roots may themselves be symlinks.

Your own entries live in the user root, $XDG_CONFIG_HOME/stacks/ (default ~/.config/stacks/). stacks create writes skeletons there. Registries and trust covers the other roots and their precedence.

Frontmatter

The file starts with YAML between two --- lines. Keys are snake_case, and unknown keys are errors, so a typo such as appliesTo fails instead of being ignored. CLI output and JSON spell the same fields in camelCase.

---
schema_version: 1
id: go-cli
kind: stack
name: Go CLI
description: A Go command-line tool with cobra commands, viper config (file, env, flags), ...
tags: [go, cli, make]
deps:
  cli: [github.com/spf13/cobra, github.com/spf13/viper]
  test: [github.com/stretchr/testify]
tools: [go, make, git, golangci-lint]
decisions:
  - "Binary name: the project slug (default; the Makefile's BIN)."
  - "Coverage floor: 50% (default)."
env: {}
merge: [.claude/settings.json]
fragments:
  default: [agent-ops, git-hooks, ci-github]
  optional: [goreleaser, rr, ce-plugin]
verify:
  - { name: install, phase: setup, layer: install, run: make install }
  - { name: build, layer: build, run: make build }
  - { name: lint, layer: lint, run: make lint }
  - { name: test, layer: test, run: make test }
commands:
  install: Download modules and install dev tools that are missing
  build: Build the binary into bin/ with version info
  lint: Run golangci-lint
  test: Unit tests
---
stacks/go-cli/STACK.md

Keys for both kinds

Key Type Required What it holds
schema_version 1 Yes The entry format version. An entry with a newer version than the CLI reads is skipped with a warning
id kebab-case string Yes Lowercase letters and digits separated by single dashes. Must match the directory name
kind stack or fragment Yes Must match the file name: STACK.md or FRAGMENT.md
name string Yes Display name
description string Yes One-paragraph technical synopsis, shown by list and show
tags list of strings No Free tags. Fragments' applies_to and conditional markers can match them. A stack declares exactly one task-runner tag, make or scripts
deps map of role to package names No Packages by role, names only. A version in a name is an error; the agent installs current versions
tools list of strings No Programs that must be on PATH. new and add list missing ones, never fail on them
decisions list of strings No Choices the brief puts to you. Each must contain (default so --yes and sandbox can proceed
env map of string to string No Default environment values, such as ports. verify uses them for keys you haven't set
verify list of steps No The commands that define done. See Verify steps
commands map of name to description No Every project command and what it does. Verify steps may only call declared commands
merge list of paths No JSON files to deep-merge when several entries ship the same path. See Composition rules

Lists default to empty and maps to {}.

Stack-only keys

Key What it holds
fragments.required Fragments always included. --without can't remove them
fragments.default Fragments included unless left out with --without
fragments.optional Fragments added only with --with

Fragment-only keys

---
schema_version: 1
id: postgres
kind: fragment
category: service
name: PostgreSQL with SQLAlchemy for Python
description: "PostgreSQL for the FastAPI stacks (python-react, python-api): ..."
tags: [database, postgres, service]
applies_to: [python-react, python-api]
requires_tools: [docker]
tools: [docker]
deps:
  backend: [sqlalchemy, asyncpg, alembic, greenlet]
env:
  PORT_DB: "5432"
  PORT_DB_TEST: "5433"
verify:
  - { name: db-up, phase: setup, layer: install, run: make db-up }
  - { name: db-migrate, phase: setup, layer: install, run: make db-migrate }
  - { name: db-down, phase: teardown, layer: install, run: make db-down }
commands:
  db-up: Start the database
  db-migrate: Apply database migrations
  db-down: Stop the development database
---
fragments/postgres/FRAGMENT.md
Key Type Default What it holds
category one of agent-ops, app, ci, deploy, release, service, vendor, tooling required What kind of capability this adds. app makes the fragment an app
applies_to list of stack ids or tags [] Stacks this fragment fits: a stack whose id or one of whose tags is listed. Empty means any stack. An app must name at least one
requires list of fragment ids [] Fragments pulled in automatically with this one
conflicts list of fragment ids [] Fragments that can't be in the same composition
requires_tools list of strings [] Tools this fragment can't work without. The missing-tool warning names the --without flag that drops it
needs_own_repo boolean false The fragment must sit at the root of its own git repository. new refuses it inside another repository

An app is a fragment with category: app. A composition holds at most one app, apps get a larger word budget, and their verify steps run as a second stage after the base project passes. In sandbox, deploy, release and app fragments stay out unless named with --with.

Verify steps

Each item in verify is one step. The manifest records the steps with every default filled in, and stacks verify runs them.

Field Type Default What it does
name kebab-case string required The step's name. Unique across the whole composition
run string required The shell command, run through /bin/sh -c
phase setup, check, serve or teardown check When it runs. See Verify for what each phase does on failure
layer install, lint, typecheck, test, build, serve, docs or check none What the step proves. A missing layer is a validation warning
tags list of strings [] Tags --skip-tag can filter. prod steps are skipped in a sandbox project unless verify --all. app is reserved for the CLI
cwd path project root Working directory, which must resolve inside the project
timeout duration such as 500ms, 90s, 10m 10m Applies to setup, check and teardown steps
expect.http URL required for serve The readiness probe for a serve step. An http URL on localhost, 127.0.0.1 or ::1; ${VAR} expands from the step environment
expect.within duration 60s How long verify polls the probe before failing the step

A serve step without expect is an error. A serve command must stay in the foreground: verify starts it, polls the URL every 500 ms until any status below 500, then stops it.

verify:
  - name: dev
    phase: serve
    layer: serve
    run: make dev
    expect: { http: "http://localhost:${PORT_WEB}/", within: 60s }
A serve step

Every stack's base composition must cover the install, lint, typecheck and test layers, plus a serve step with a probe when the stack is tagged web or api. Validation checks enforces that.

Composition rules

When a stack and its fragments are composed, their keys combine in composition order, stack first.

Key How entries combine
verify Concatenated. Two steps with the same name are a ComposeConflict
env Merged. The same key with two different values is a ComposeConflict
commands Merged. A later entry's description replaces an earlier one for the same name
deps Merged per role, duplicates removed
tools, requires_tools Collected, each with the entries that need it
decisions Concatenated. A fragment's decisions are prefixed with its id in the brief
Shipped files One owner per output path. Two entries shipping the same path is a ComposeConflict (file_owner), unless every writer lists the path in merge

A path every writer lists in merge must be JSON in all of them. The CLI deep-merges the files: objects merge recursively, arrays concatenate with duplicates removed, and otherwise the later value wins. A .tmpl file can't be merged. No entry may write under .git/ or .stacks/, in any letter case.

The guidance body

Everything after the closing --- is Markdown for the building agent: layout, setup, rules and pitfalls. The CLI copies it into .stacks/BRIEF.md under the entry's name. Use ATX headings (## Heading); setext underlines get a warning. A stack's body needs a ## Setup section, and keeping its steps numbered helps the agent follow them.

Word budgets apply to the body: 2,000 words for a stack, 1,000 for a fragment, 3,000 for an app. Validation checks has the composed-brief limits.

Exact versions (1.2.3) and dated phrases in prose get a warning outside fenced examples, because they go stale. Dated phrases include "as of 2026", a month and year, and release-channel words such as rc, alpha, nightly, canary and dist-tag. Write a rule that stays true, or tell the agent how to discover the current detail.

Conditional prose

A block between a when marker and an end marker is kept only when its condition matches the composition.

<!-- stacks:when with=ci-github -->
Run the same make targets in CI that verify runs locally.
<!-- stacks:end -->
Conditional block
Key Matches when
stack=<ids> The composition's stack is one of these
tag=<tags> The stack has one of these tags
tag!=<tags> The stack has none of these tags
mode=new|add The project was made by new or by add
preset=default|sandbox The composition uses this preset
with=<ids> One of these fragments (or the app) is in the composition

Commas mean OR within a key (stack=go-cli,python-api), and several keys on one marker mean AND. Each marker sits alone on its line, blocks don't nest, and every when needs an end. A matching block loses its marker lines; a non-matching block is removed whole.

Warning

Markers are found on every line of the body, inside code fences too. To show a marker as an example inside an entry, indent it or break its spelling.

A malformed marker fails composition with EntryInvalid. validate reports it as conditional-syntax, and warns about branches no composition can reach.

Fill markers

<!-- stacks:fill --> and <!-- stacks:fill: text --> mark text in a shipped file the agent must replace, usually in a README. They aren't conditionals and stay as written. Pair one with a verify step that fails while the marker remains:

verify:
  - name: readme-filled
    layer: check
    run: "test -s README.md && ! grep -n 'stacks:fill' README.md"
A file check

Templates

A file whose name ends in .tmpl is rendered and written without the suffix: files/Makefile.tmpl becomes Makefile. Every other file is copied byte for byte with its executable bit. A file named only .tmpl is copied unchanged.

A token is {{ and a variable name and }} on one line, with optional spaces or tabs inside the braces. Anything else is literal, so ${{ secrets.TOKEN }} in a GitHub Actions workflow passes through. There are no conditionals or loops, and these six variables are the whole set:

Variable Value
project_name --name, else the target directory's base name
project_slug The name as lowercase ASCII words joined by dashes, project when it has none
env_prefix The slug uppercased with dashes as underscores, prefixed with _ when it starts with a digit (my-cli gives MY_CLI)
package_scope --scope, else the package_scope setting, without the @; empty when unset
author --author, else the author setting, else git config user.name, with quotes, backslashes and line breaks removed
year The current UTC year

An unknown variable name fails new with EntryInvalid before anything is written, and validate warns about Actions expressions that look like tokens.

Search cards

A search card is optional JSON beside the entry directories, at search/stacks/<id>.json or search/fragments/<id>.json (.stacks/search/ in a project registry). It holds the words stacks search ranks against, plus optional plain-language copy that show and the directory site display. Cards don't change composition fingerprints. Write your own stack or fragment covers the fields and their limits, and How search ranks entries explains how they're used.