What a feature map is
A map is a YAML file .features/<slug>.yaml with at least:
feature_name: matches the filename stempurpose: what it does and whyentry_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 vieweropens 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).
- 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
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:
- Addressable. The CLI is an index (
list,search,find,impact). A wiki is a pile of documents youcat. - Bounded reads. Structured keys and
--sectioncap context. A wiki has no equivalent of “give me only the files.” - Checkable.
validateandcheckcatch missing fields and dead paths. A wiki cannot tell you the controller it mentions was renamed. - 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_pointsdoes not list the file you changed, the map is wrong and you fix it in the same PR. - Onboarding is
feature-map listthenshow, 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:
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:
pip install "git+https://github.com/markschellhas/feature-map.git"
Confirm:
feature-map --version
Bootstrap a repository
From the repo root (a git checkout, or the directory you intend as the root):
feature-map init
This is idempotent. It:
- Creates
.features/ - 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 - Writes
.feature-map.yamlif missing - Appends (or refreshes) an
AGENTS.mdblock that requires agents to use Feature Map before feature work, and names the deployed skill by path so an agent that only readsAGENTS.mdcan still find it - Writes
bin/feature-map, a shim that execsfeature-mapon PATH - 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.
feature-map init -y -h claude
Configure .feature-map.yaml
Declare the repo’s apps so check can resolve paths:
# .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-mapbefore proceeding.
Agents must:
- Before any feature work, debug, PRD, or plan, run
feature-map list(or./bin/feature-map list). - If the feature name is unclear, run
feature-map search <keyword>andfeature-map find <path-fragment>. - Read the map with
feature-map <slug>, orfeature-map show <slug> --section entry_pointsto save tokens. - For cross-cutting work, run
feature-map graph <slug>. - After shipping architecture changes, update the relevant
.features/*.yaml, then runfeature-map validateandfeature-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:
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
# 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:
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
feature-map init signup # scaffold .features/signup.yaml
Then replace placeholders by reading the real code. Required:
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 checkwill flag missing ones; invented paths train agents to look in the wrong place. - Routes (
GET /signup) may appear inentry_pointsas extra context, but put the implementing file next to them. related_featuresentries must start with a real slug once that map exists (billing (note), not a sentence).- Record uncertainty in
notesinstead of guessing.
Work in batches: scaffold 5–10 obvious features, validate, then fill
gaps check and stats report.
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.yamlappears 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 READMEcheckonly 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, runcheck - Cross-feature work → update
related_featuresand re-readgraph
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:
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
# 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
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
and use an editable install in a virtualenv so feature-map on PATH
is the code you are editing:
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.
Back to the Feature Map overview, or browse the source and changelog on GitHub.