# Keep the feature map current

Source: https://flow-next.dev/guides/keep-feature-map-current/

How the committed feature map stays true as the app changes - work updates the routes its change moved, every reader reports drift, and a due trigger tells you when to run a maintain pass, by hand or from a loop you start.

The [feature map](https://flow-next.dev/skills/features/) at `.flow/features/` records how a user reaches each feature, so QA, drive, and bug intake read the route instead of rediscovering it. That only pays while the map matches the app. A stale route sends the next run the wrong way, which costs more than having no map.

Three mechanisms keep it current as a side effect of work you already do. You run the full maintain pass only when Flow tells you it is due.

## Seed it once

```text
/flow-next:features
```

With no `.flow/features/` directory, this seeds the map: it reads the repo, names the main user-facing features, and proves each route with one live drive before writing it. Setup and prime recommend this step on every repository, whether or not live QA is on. Neither runs it for you, because seeding launches and drives the live app. A repository with no drivable surface (a library, for example) gets the same recommendation, and the seed pass then refuses with its reason.

## What keeps it true

### Work updates the routes its change moved

When a spec’s change alters how a user reaches a mapped feature (a renamed button, a moved page, a changed CLI invocation), [`/flow-next:work`](https://flow-next.dev/skills/work/) updates the map in the same change, at its quality phase:

* It edits **only** the feature files whose route this change altered. It adds no new features and leaves every other file alone.
* It proves each new route with one live drive before writing it, then refreshes that file’s `**Last proven:**` line.
* The map diff rides the same commit, so the PR shows it beside the code diff.

When work cannot prove the new route, it leaves the map unchanged and files a drift note describing the expected change instead. That happens when the app cannot start, when no driver is usable, or when work cannot tell which feature file a changed surface belongs to. The work run is never blocked by this. A repository without a map skips the step after one existence check.

Besides `/flow-next:features` itself, this is the only thing that writes the map.

### Every reader reports drift

QA, drive, bug intake, and later live-app routes read the map and never edit it mid-run. When one of them finds a mapped route that no longer matches the live app, it files a drift note and continues by discovering the route live:

* The note lives in project memory (knowledge track, tag `feature-map-drift`) with the title `drift: <surface>/<feature-slug> <sub-feature-id>`. The fixed title means a second report of the same drift updates one note instead of adding another.
* Its body is two lines: `Expected:` (the mapped route) and `Observed:` (what the live app did).

When a maintain pass or a work update proves the route a note names, it marks that note stale. An open note therefore always means drift nobody has fixed yet.

If the same route drifts again after its note was retired, the reader reopens that note (`flowctl memory mark-fresh`), so a recurrence counts as open drift again. With memory disabled, drift is recorded in the stage’s run notes instead, and nothing is filed or retired.

### A due trigger names the moment

Each feature file can carry a provenance line directly under its surface line:

```markdown
**Surface:** web
**Last proven:** 2026-09-20 at 4f2c9ab
```

The date is the day a live drive proved the route, and the commit is the short `HEAD` at that drive. Seed, maintain, and work’s update step write it. A file without the line reads as never proven, and a malformed line reads the same way (maintain reports it by name).

The map is **due** a maintain pass when either holds:

* at least one open `feature-map-drift` note exists, or
* a feature’s last proof is at least `features.staleAfterCommits` (default 50) default-branch commits old, counting only commits that touch the product. A commit that changes only `.flow/` or documentation does not count. A never-proven or malformed file counts as due.

With memory disabled, only the age condition applies.

## Where you see it

| Surface                                                                  | What it shows                                                                                                                            |
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| [`/flow-next:setup`](https://flow-next.dev/skills/setup/)                | Always one line: seed when no map exists, maintain when the map is due, or `Feature map: current`.                                       |
| [`/flow-next:prime`](https://flow-next.dev/skills/prime/)                | The same recommendation in the report, beside the QA-readiness line.                                                                     |
| [`/flow-next:flow`](https://flow-next.dev/skills/flow/) with no argument | When the map is due, an extra line after `Next:`: `Also recommended: /flow-next:features - feature map due a maintain pass (<reasons>)`. |
| `flowctl features status`                                                | The facts behind all three.                                                                                                              |

All of these recommend; none of them runs the skill. `flow --auto` and land never dispatch `/flow-next:features`, and the skill refuses when an autonomy marker is present.

## Check the status

```bash
flowctl features status          # one-line verdict plus reasons
flowctl features status --json   # the full rows
```

```text
Feature map due a maintain pass: run /flow-next:features
  - 1 open feature-map-drift note(s)
  - settings-export.md: never proven
  - notes-list.md: 57 surface commits since last proven (threshold 50)
```

The other verdicts are `Feature map current` and `No feature map: run /flow-next:features to seed .flow/features/`. With memory disabled, a trailing line notes that drift notes were not counted.

The JSON form carries `recommendation` (`seed`, `maintain`, or `none`), `due`, `reasons`, `open_drift` (note ids and titles, or `null` with memory disabled), `threshold`, the default branch it measured against as `base`, and one row per feature file with its `state` (`proven`, `never-proven`, or `malformed`), `last_proven`, `commits_since`, and `stale`. The command is read-only; it never edits the map and never dispatches the skill. Full field list: [CLI reference](https://flow-next.dev/flowctl/cli-reference/#feature-map).

### Code in sibling repos

In a home-base workspace, `.flow/` lives in a planning repo and the product code in sibling clones beside it, so the planning repo’s history says little about whether a route changed. Pass each sibling with `--repo`, relative to the repo that holds `.flow/`:

```bash
flowctl features status --repo ../payments-api --repo ../checkout-web
```

Each proven feature then counts every sibling’s product commits since its proof date on top of the planning repo’s own, and the JSON row adds `commits_since_by_repo` (`"."` is the planning repo) beside a top-level `repos` list. A path that is not a git repo root, or has no default branch that resolves, exits `2` naming it, so a typo never reads as zero commits. Maintain, flow, prime and setup pass `--repo` for the siblings your project instructions name; without the flag the output is exactly as before.

Tune the age threshold per repository:

```bash
flowctl config set features.staleAfterCommits 100
```

A busy repository with many small commits may want a higher number; a repository whose UI changes often may want a lower one. Values below 1 read as the default.

## Run maintain when it is due

```text
/flow-next:features
```

With a map present, the same command runs the maintain pass: it pulls in the open drift notes, checks every feature against source, drives each one live, fixes the map where the route changed, refreshes the last-proven line of every file it proved, and retires the drift notes whose routes it re-proved. It ends `CLEAN` (nothing to change, no PR) or `CHANGED` (one chore PR of proven corrections), or `BLOCKED` naming what stopped it. Full phases: [Features: maintain](https://flow-next.dev/skills/features/#maintain).

Run it from a checkout on the default branch with no uncommitted edits under `.flow/features/`. Maintain proves routes against the code its PR will ship on, so a diverged branch or dirty map ends `BLOCKED` before it drives anything.

Good moments to run it:

* setup, prime, or flow reports the map due;
* a release reshaped navigation across many features at once;
* QA or drive runs keep falling back to live discovery on the same screens.

## Run it on a schedule

The maintain pass stays user-invoked, but “user-invoked” includes a loop you start yourself. Two shapes work:

* **A host loop in a session you open.** In Claude Code, start a session on a default-branch checkout and run:

  ```text
  /loop 1d /flow-next:features
  ```

  Each tick ends with one `FEATURES_VERDICT=<CLEAN|CHANGED|BLOCKED|REFUSED> features=<n> reason="..."` line, so the loop output stays readable. To skip the live pass on quiet days, give the loop a prompt instead: check `flowctl features status` and run `/flow-next:features` only when it reports the map due.

* **A scheduled job you own.** A scheduler you set up (cron, for example) that opens an ordinary agent session in a dedicated default-branch worktree and invokes `/flow-next:features` works the same way.

Either way the loop must be a plain session, not a run inside `flow --auto` or land. The skill scans the environment for autonomy markers (any `FLOW_*AUTONOM*` variable, or a `mode:autonomous` argument) and ends `REFUSED` when it finds one. A `CHANGED` pass opens a PR and never merges it; you or [land](https://flow-next.dev/autonomy/land/) decide that.

## Where it stops

* **No automatic maintain.** No pipeline stage, post-merge hook, or autonomous driver runs the full pass.
* **No third writer.** Work’s scoped update and `/flow-next:features` are the only map writers. QA, drive, and every other reader only file drift notes.
* **No unproven edits.** Every route written to the map was driven live first; a route that could not be driven becomes a drift note.
* **Navigation only.** The map stays how a user gets there. Specs still say what to prove, and captured live evidence is still the only proof.
* **No source paths.** Provenance is a date and a commit.
