# 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.

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](https://stacks.steamfield.io/docs/guides/authoring.md).

## Directory layout

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

```text title="Registry root"
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
```

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](https://stacks.steamfield.io/docs/concepts/registries-and-trust.md) 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.

```yaml title="stacks/go-cli/STACK.md"
---
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
---
```

### 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](#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](#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

```yaml title="fragments/postgres/FRAGMENT.md"
---
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
---
```

| 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](https://stacks.steamfield.io/docs/concepts/verify.md) 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.

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

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](https://stacks.steamfield.io/docs/reference/validation.md#what-validate-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](https://stacks.steamfield.io/docs/reference/validation.md#word-budgets) 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.

```md title="Conditional block"
<!-- stacks:when with=ci-github -->
Run the same make targets in CI that verify runs locally.
<!-- stacks:end -->
```

| 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:

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

## 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](https://stacks.steamfield.io/docs/guides/authoring.md) covers the fields and their limits, and [How search ranks entries](https://stacks.steamfield.io/docs/reference/search.md) explains how they're used.
