# Feature Map guide

Source: https://featuremaps.org/guide

## What a feature map is

A map is a YAML file `.features/<slug>.yaml` with at least:

- `feature_name`: matches the filename stem
- `purpose`: what it does and why
- `entry_points`: real file paths (and routes if useful)

Recommended: `apps`, `user_flow`, `related_features`, `notes`. Cross-app
features get one map, not one map per app. `related_features` is how
`graph` and `impact --transitive` work.

Maps are research infrastructure, not product features. They do not
replace PRDs or implementation plans. They tell you *where the code
lives and how the pieces connect* before you touch anything.

### What Feature Map does not do

- There is no stored web UI, wiki export, or database of maps.
  `feature-map viewer` opens a local, ephemeral, read-only page and never
  writes HTML into your repo.
- There is no auto-generator that invents maps from the AST. Maps stay
  authored; agents may draft them after reading the code (see
  [existing repositories](#adopt-feature-map-in-an-existing-repository)).
- There is no coupling to a framework. The CLI only reads `.features/`,
  `.feature-map.yaml`, and the filesystem.

## Why a feature map, not a wiki

A Karpathy-style (or any LLM-written) wiki works: agents dump what they
learned into markdown and the next session can read it. The failure mode
is **shape**. Language models write prose. A wiki page on "signup" becomes
a narrative — history, asides, duplicated README, a little architecture —
and the next agent has to **ingest the whole page** to find the three
files that matter. That is slow in wall time and expensive in tokens.

Feature Map stores the same knowledge as **fields**, not essays:

| Need | Wiki | Feature Map |
|------|------|-------------|
| What is this? | Buried in a paragraph | `purpose` |
| Where do I start? | Maybe a link, maybe not | `entry_points` (real paths) |
| Which apps? | Prose | `apps` |
| How does a user move? | A story | `user_flow` |
| What else is coupled? | "See also" if you are lucky | `related_features` → `graph` |
| Is this stale? | You re-read it | `check` (paths on disk), `validate` |

### How an agent finds a feature

```bash
feature-map list                          # names only
feature-map search billing                # slugs + short snippets
feature-map show billing --section entry_points
```

`list` is a few dozen tokens. `search` returns hits, not chapters.
`--section` loads **one key** from one YAML file. The agent opens the
code at those paths instead of reading a 2k-word wiki page and then
still grepping.

That is why it is more efficient:

1. **Addressable.** The CLI is an index (`list`, `search`, `find`,
   `impact`). A wiki is a pile of documents you `cat`.
2. **Bounded reads.** Structured keys and `--section` cap context. A wiki
   has no equivalent of "give me only the files."
3. **Checkable.** `validate` and `check` catch missing fields and dead
   paths. A wiki cannot tell you the controller it mentions was renamed.
4. **Stable voice.** YAML fields do not grow a new introduction every
   time an agent "updates the docs."

### Value to the developer

- You get a **directory of the product**, one slug per capability,
  without maintaining a second documentation site.
- Reviews stay honest: if `entry_points` does not list the file you
  changed, the map is wrong and you fix it in the same PR.
- Onboarding is `feature-map list` then `show`, not "read the wiki and
  hope the architecture section is current."
- You still write PRDs and plans for *what to build*. The map only
  answers *where it lives and what it touches*. That split keeps both
  artifacts small.

A wiki can still hold narrative (vision, history, runbooks). Feature Map
is the **index the agent is required to hit first** so it does not spend
the budget re-discovering the tree.

## Install the CLI

Pick one:

```bash
pip install feature-map-cli
npm install -g feature-map-cli
brew install markschellhas/tap/feature-map
```

Requires Python 3.8+ and PyYAML. The npm package installs the PyPI CLI
into a local venv. The command is `feature-map` whichever way you
install it. The PyPI name is `feature-map-cli` because `pip install
featuremap` is a different (biology) project, and `feature-map` is
blocked on PyPI as too similar to that name.

To try unreleased changes, install from git:

```bash
pip install "git+https://github.com/markschellhas/feature-map.git"
```

Confirm:

```bash
feature-map --version
```

## Bootstrap a repository

From the repo root (a git checkout, or the directory you intend as the
root):

```bash
feature-map init
```

This is idempotent. It:

1. Creates `.features/`
2. Copies the agent skill to `.agents/skills/feature-map/`
   (or `.grok/skills/feature-map/` if that tree already exists), and
   mirrors it into every other known harness directory the repo already
   uses, plus any `--skill-dir` / `skill_dirs:` target
3. Writes `.feature-map.yaml` if missing
4. Appends (or refreshes) an `AGENTS.md` block that **requires** agents
   to use Feature Map before feature work, and names the deployed skill
   by path so an agent that only reads `AGENTS.md` can still find it
5. Writes `bin/feature-map`, a shim that execs `feature-map` on PATH
6. On an empty repo, offers to launch your agent harness to scour the
   code and author the first maps

Skill directories are harness-neutral. `.agents/skills/` stays the primary
target; a known harness is *additionally* mirrored only when the repo already
uses it (its config directory exists), so `init` never seeds an agent tree
nobody asked for. Known: `.agents/skills`, `.claude/skills`, `.grok/skills`.
For anything else, name it yourself (`--skill-dir .my-agent/skills`, or
`skill_dirs:` in `.feature-map.yaml`) and it is always written.

Useful flags: `--no-agents`, `--no-shim`, `--upgrade-skill`, `--force`,
`--skill-dir DIR` (repeatable). `-y/--yes` skips the "start authoring
maps?" prompt and `-h/--harness <name>` picks the agent harness
non-interactively (`claude`, `cursor-agent`, `opencode`, `grok`, `codex`,
`gemini` or `pi`). Answer `n` or pick `skip` to author the maps yourself.

```bash
feature-map init -y -h claude
```

### Configure .feature-map.yaml

Declare the repo's apps so `check` can resolve paths:

```yaml
# .feature-map.yaml
features_dir: .features
apps:
  - api
  - web
  - mobile
required_sections:
  - purpose
  - entry_points
min_cli_version: "1.0.0"
# Extra skill mirror targets for harnesses this CLI does not know about:
# skill_dirs:
#   - .my-agent/skills
```

`apps` prefixes are used by `check` when resolving paths. `skill_dirs`
adds skill mirror targets for harnesses the CLI does not know about.

## Instructions for coding agents

`feature-map init` writes this mandate into `AGENTS.md`. Keep it. Do not
weaken it. Downstream skills (plans, PRDs, debugging) should say:

> **REQUIRED SUB-SKILL:** Use `feature-map` before proceeding.

**Agents must:**

1. **Before any feature work, debug, PRD, or plan**, run
   `feature-map list` (or `./bin/feature-map list`).
2. If the feature name is unclear, run `feature-map search <keyword>` and
   `feature-map find <path-fragment>`.
3. Read the map with `feature-map <slug>`, or
   `feature-map show <slug> --section entry_points` to save tokens.
4. For cross-cutting work, run `feature-map graph <slug>`.
5. After shipping architecture changes, update the relevant
   `.features/*.yaml`, then run `feature-map validate` and
   `feature-map check`.

**Agents must not** implement, plan, or "just grep the repo" when a map
exists for that area. The map is authoritative; the tree is how you
verify the map.

If `list` is empty, or search/find miss the area you are about to touch,
**stop and author maps first** (see the next section). Do not invent
architecture in a PRD or plan without writing the map.

Day-to-day commands:

```bash
feature-map list
feature-map search billing
feature-map find src/invoices
feature-map billing
feature-map show billing --section entry_points
feature-map graph billing --format mermaid
feature-map validate
feature-map check
feature-map impact src/invoices/create.py
```

`./bin/feature-map` is the same CLI if the global binary is not on PATH
in a given environment.

## Adopt Feature Map in an existing repository

`init` does **not** generate maps from source. On a repo that already has
code, an agent (or a developer) must **read the tree and author maps**.
This is expected and should be the first Feature Map task in that repo.

Do not emit one map per file. Cluster by *user-visible capability* or
*deployed subsystem*.

### Step A: inventory the repo

```bash
# top-level apps / packages
ls
# language / package manifests
# Gemfile, package.json, pyproject.toml, go.mod, pubspec.yaml, apps/, packages/
```

Write the `apps:` list in `.feature-map.yaml` from that inventory
(`api`, `web`, `ios`, `worker`, …). Prefer directory names that `check`
can prefix onto paths.

### Step B: find seams where features show up

Scour, in order, whatever exists:

| Signal | Typical paths |
|--------|----------------|
| HTTP routes | `config/routes.rb`, `**/routes.*`, OpenAPI, `app/controllers` |
| App shells | `src/App.svelte`, `lib/main.dart`, `cmd/*/main.go`, `app/page.tsx` |
| Domain models | `app/models`, `src/domain`, `internal/` |
| Background jobs | `app/jobs`, `workers/`, queues |
| CLIs | `bin/`, `cmd/`, `cli/` |
| Docs / PRDs | `docs/`, `README*`, existing architecture notes |
| Tests | test names often name the feature better than folders |

Use the CLI as you go:

```bash
feature-map find <controller_or_package>
feature-map search <domain word>
```

On a first pass those will miss (no maps yet). That is the signal to
**draft** maps, not to skip them.

### Step C: cluster into features

A feature is a slice a human would name in a standup ("signup",
"billing", "push notifications"), usually spanning several files and
sometimes several apps.

Cluster when they share:

- the same user journey
- the same primary models / tables
- the same entry controllers or screens
- a documented product name

Split when journeys, owners, or deployables diverge. Cross-app flows
(one API + one mobile screen + one worker) are **one** map with multiple
`apps` and `entry_points`, not three maps.

### Step D: author each map

```bash
feature-map init signup          # scaffold .features/signup.yaml
```

Then replace placeholders by reading the real code. Required:

```yaml
feature_name: signup
purpose: "How a new account is created and reaches the first authenticated screen."
entry_points:
  - api/src/signup/handler.go
  - web/src/routes/signup/+page.svelte
apps:
  - api
  - web
user_flow:
  primary: "Email submit → API creates user → redirect to /app"
related_features:
  - billing (plan attached after first login)
notes: >
  Status: mapped from existing code on 2026-08-27. Confirm webhook path
  before changing Stripe.
```

Rules while scouring:

- Prefer **paths that exist on disk**. `feature-map check` will flag
  missing ones; invented paths train agents to look in the wrong place.
- Routes (`GET /signup`) may appear in `entry_points` as extra context,
  but put the implementing file next to them.
- `related_features` entries must start with a real slug once that map
  exists (`billing (note)`, not a sentence).
- Record uncertainty in `notes` instead of guessing.

Work in batches: scaffold 5–10 obvious features, `validate`, then fill
gaps `check` and `stats` report.

```bash
feature-map validate
feature-map check --json
feature-map stats --json
```

`validate` without `--strict` treats missing recommended sections as
warnings (exit 0). Use `--strict` in CI once the first pass is honest.

### Step E: when to stop the first pass

Stop when:

- Every app in `.feature-map.yaml` appears on at least one map
- The main user journeys (signup, core loop, billing/admin if any) have maps
- `feature-map search <product noun>` hits something for the nouns in the README
- `check` only reports paths you *intend* to add, not typos

You do not need 100% file coverage. You need **named doors** into the
system so the next agent does not start from zero.

### Step F: keep maps alive

After that first scour:

- New feature → `feature-map init <slug>` in the same PR as the code
- Moved/renamed files → update `entry_points`, run `check`
- Cross-feature work → update `related_features` and re-read `graph`

## Greenfield repositories

If there is almost no code yet, still run `feature-map init`, then add a
map when the first vertical slice lands. An empty `.features/` plus the
`AGENTS.md` mandate is enough: the next agent is required to author the
map with the feature, not after the fact.

## Capture ideas for later

`feature-map idea` saves a product idea as markdown in `docs/ideas/`, so
the backlog lives in the repository next to the maps:

```bash
feature-map idea "let users export invoices as CSV"
# Saved docs/ideas/2026-09-26-let-users-export-invoices-as-csv.md

feature-map idea                      # type several lines; finish with an empty line or Ctrl-D
echo "rate-limit the public API" | feature-map idea
```

The filename is today's date plus a slug of the first line. A second idea
with the same slug on the same day gets `-2`, `-3`, and so on. The folder
is created on first use, and `.features/` does not have to exist yet.
With `--json` the command prints `{"ok": true, "path": ..., "file": ...}`.

Because the ideas are committed, any agent with a checkout can pick one
up, including cloud agents that never saw your terminal. A useful prompt:
*"Implement `docs/ideas/<file>.md`. Use feature-map to find the affected
features first, and add or update their maps in the same PR."*

## Validate feature maps in CI

```yaml
# example GitHub Actions step
- run: pip install feature-map-cli
- run: feature-map validate
# optional, once maps are trusted:
# - run: feature-map validate --strict
```

Trigger on changes to `.features/**` and, if you want a backstop, on
every PR.

## Upgrade

```bash
feature-map update
# or, by hand:
# pip install -U feature-map-cli
# npm install -g feature-map-cli@latest
# brew upgrade markschellhas/tap/feature-map
feature-map init --upgrade-skill
```

`feature-map update` detects whether this copy came from pip, npm, Homebrew,
pipx, or uv and upgrades through that channel. If you are already on the
latest version it says so and exits 0. A source/editable checkout cannot
be upgraded this way; install a released package instead.

`--upgrade-skill` overwrites the deployed `SKILL.md` and references from
the installed package version. Re-run `feature-map init` to refresh the
`AGENTS.md` marked block.

## Command reference

Invocation: `feature-map` (on PATH) or `./bin/feature-map` (repo-local shim).
Global flags: `--json`, `--version`.

| Command | Description |
|---------|-------------|
| `list [--json]` | All feature slugs; JSON includes mtime and app count |
| `show <name> [--section <key>] [--json]` | Full map or one top-level section |
| `<name>` | Alias for `show <name>` |
| `viewer [--no-open] [--timeout SECONDS]` | Local ephemeral browser view of all maps, for humans |
| `search <query> [--json]` | Full-text search across all maps |
| `find <path-fragment> [--json]` | Reverse lookup by path string |
| `graph [name] [--format mermaid\|json\|dot]` | `related_features` graph |
| `validate [--strict] [--json]` | Structural validation |
| `check [--json]` | Staleness check for entry-point paths |
| `impact <file> [--transitive] [--json]` | Features referencing a file |
| `stats [--json]` | Coverage statistics |
| `init [-y] [-h <harness>] [--skill-dir DIR]` | Bootstrap `.features/`, agent skill, config, `AGENTS.md`, shim |
| `init <name> [--force]` | Scaffold `.features/<name>.yaml` |
| `init --upgrade-skill` | Refresh the agent skill in every deployed location |
| `install [--json]` | Verify install and repo setup |
| `update [--json]` | Upgrade this CLI via the package manager that installed it |
| `idea [text] [--json]` | Capture an idea as markdown in `docs/ideas/` (creates the folder) |

Exit codes: `0` success, `1` user error (e.g. feature not found), `2`
validation failure (`validate --strict`).

## Local development

Published PyPI, npm, and Homebrew installs lag the repository. To work on
the CLI itself, clone [the repo](https://github.com/markschellhas/feature-map)
and use an **editable install** in a virtualenv so `feature-map` on PATH
is the code you are editing:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"     # same as `make install`
python -m pytest -q         # same as `make test`
feature-map --version
```

Edits under `src/feature_map/` take effect immediately. Do not iterate via
`npm install -g feature-map-cli` or `brew install`: those pull the last
**published** PyPI version, and `feature-map update` refuses a
source/editable checkout on purpose. Publishing steps are in
[PUBLISH.md](https://github.com/markschellhas/feature-map/blob/master/PUBLISH.md).
