add-feature — stamp a conforming vertical slice
Spec-first, deterministic-stamp, gate-proven. The script (
qa/scaffold-feature.mjs) does the mechanical work — copy the exemplar file set, whole-word identifier rename, anchor injection into the three shared files (four when the inspector shipped — the preview registry too). You (the AI) only refine spec wording and adapt the feature to its real shape. You are not done untilnode qa/verify.mjsPASSes and the receipt is committed — see this project'sCLAUDE.md.
Step 0 — name the lane, out loud, before anything else
Every post-genesis change enters through one of two lanes (CLAUDE.md §"After genesis"),
and the human must be told which one this request is taking, in your first reply — one
or two plain sentences before any tool runs: what you understood the change to be, which
lane, and why. Never route silently; the human can overrule the triage in a word.
- Brief lane — the request carries decisions a future contributor could plausibly
"simplify" away, or blast radius into other governed artifacts. Say so, e.g.: "This
carries real decisions (day-boundary rules, scheduling semantics) — I'll draft a feature
brief at
docs/features/<name>.mdwith the open decisions for you to close and sign BEFORE I stamp anything." Only after the brief is signed does this skill's stamping start — and if the feature has a UI surface, the design gate comes next, before the spec: draft the screens on stub data, render them, and stop for the human's signature onfeature-design:<name>(brief → design → spec → build). A human signs rendered screens, never a description of screens. - Direct lane — an ordinary feature with no decisions worth recording. Say so, e.g.: "Straightforward slice, no decisions worth a brief — direct lane: spec clauses for your confirmation, then I stamp and prove." Then continue with Step 1 below.
The clone source is configurable
The stamper clones from the project's configured exemplar — qa/approvals.json's
top-level "exemplarFeature" key (absent ⇒ home, the shipped exemplar). This is the same
resolution the approvals registry uses for the governed exemplar-feature artifact, so what
gets stamped is always exactly what the human signed off on. After the genesis walk retargets
exemplarFeature to the user's own first feature, every stamp from then on clones their
pattern in their domain language — do not assume home still exists as the exemplar; read
the config (or just run the stamper: it resolves the source itself).
If the configured exemplar has grown files beyond the canonical 11-file shape (an extra
ViewModel, a helper, a second use case named for its entity), the stamper clones only the
canonical set and prints a WARNING: listing exactly what it skipped — never silently.
When you see that warning, tell the human: the extras are part of the exemplar's pattern in
spirit but not in mechanism, and porting them into the new feature (or slimming the exemplar
back to canon) is a deliberate follow-up, not something to ignore.
Why a stamper and not hand-written files
This project's whole thesis is that determinism beats freehand generation for anything
architecturally load-bearing (see docs/adr/0001-*.md if present, or just: every hand-written
file is a drift chance). qa/scaffold-feature.mjs produces a conforming skeleton by
construction — it passes the architecture conformance gates before you write a line of
feature-specific logic. Your job is to make it behave like the real feature, not to make it
structurally correct — that part is already done.
The flow
1. Interview
Ask the human for the feature name (PascalCase, plural-ish noun — e.g. Favorites,
Bookmarks). Propose a singular entity name by stripping a trailing s/ies (Favorites →
Favorite, Categories → Category). Naive de-pluralization is unreliable for irregular nouns
— always show your proposed entity name and let the human confirm or override it before
proceeding (--entity <EntityName>).
2. Dry-run
Run:
node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName> --dry-run
Show the human the file plan and the anchor-injection diffs it prints. Confirm before stamping for real — this is the last chance to catch a wrong entity name or a naming collision.
3. Stamp
Run the same command without --dry-run:
node qa/scaffold-feature.mjs <FeatureName> --entity <EntityName>
This writes the new Screen/ViewModel/UseCase/Repository(+impl)/tests/fake, wires them into
di/AppModule.kt, presentation/navigation/Screen.kt, and presentation/navigation/AppNavHost.kt
at their // cmp:anchor markers, and writes specs/<feature>.spec.md with a default seven-clause
set (<FEATURE>-01..07: loading, success, error, reload-after-failure, tap-navigates, golden
tree, empty state) — copied verbatim from the configured exemplar's shape.
The stamped screen arrives already wrapped in BaseScreen { … } (SHELL-05): it is a
pushed NavHost destination, so unlike the tab exemplar it must handle its own insets — the
stamper does this for you; do not unwrap it.
If it exits non-zero, read the message — it is actionable (name already taken, an anchor marker is missing, or a name isn't a valid Kotlin identifier). Do not hand-edit around a stamper failure; if an anchor is genuinely missing from a shared file, that is a template defect worth flagging, not something to route around by hand-splicing.
4. Refine the spec, then the behavior
The default spec clauses are placeholders shaped like the exemplar (by default a plain list of
title/subtitle rows). Rewrite the clause prose in specs/<feature>.spec.md to describe the
feature's real behavior — the seven clause ids stay fixed (specCoverage binds tests to ids,
not prose), only the wording changes. Propose the rewritten clauses to the human; get them
confirmed before moving on — this project's contract is spec-first.
Then adapt the generated code to match:
- If the feature isn't shaped like "a list of
{id, title, subtitle}", update the entity's fields indomain/model/<Entity>.kt, the sample data in<Entity>RepositoryImpl.kt, and the screen's rendering inpresentation/<feature>/<Feature>Screen.kttogether — keep them consistent with each other and with the tests. The stamped screen already composes the registry vocabulary (ScreenColumn/AppHeader/ContentStateContainer/ListItemCard,presentation/components/*.kt) — adapt the content shape insideContentStateContainer's trailing slot, don't hand-roll a new header/loading state/list row on top of it. If the feature's data genuinely needs a component the registry doesn't cover, propose the addition to the human explicitly (a new file is a registry change — it invalidates thecomponentsapproval). - Update the copied tests (
<Feature>ViewModelTest.kt,<Feature>ScreenTest.kt) to match whatever you changed. The gate (step 6) will tell you exactly what you missed — a compile error names the mismatch; a spec-coverage failure names an orphaned clause or tag. - Leave the DI wiring, nav route, and screen scaffold as stamped unless the feature genuinely
needs a different shape (e.g. no navigation-on-tap — then remove the
-05clause's citing test and strike the clause through, don't leave it dangling). - The new screen is reachable via
Screen.<Feature>but is not wired into a bottom-nav tab by this generator (MVP scope — pushed-route only). Promoting it to a tab is a manual edit toappTabs()+AppShellcall sites; a future--tabflag may automate this.
5. Capture the golden tree
The golden baseline is not copied by the stamper (a copied one would silently mismatch the adapted screen). Generate it fresh once the screen renders the real behavior:
UPDATE_GOLDEN=1 ./gradlew :composeApp:desktopTest --tests "*<Feature>GoldenTree*"
Review qa/golden/<feature>.json briefly — it should reflect the structure you intended, not a
copy-paste artifact. Commit it alongside the feature.
6. Gate
Before the lane — the device journey. The stamper wrote qa/e2e/<feature>.yaml as a placeholder and
said so; the lane's e2eCoverage gate FAILs until that flow is a real journey citing a <FEATURE>-NN
clause it proves. That red is the gate working, not a bug — make the flow real first.
node qa/verify.mjs
This must PASS. It proves: the spec's seven clauses are all bound to a citing test
(specCoverage), the device journey cites a clause it proves (e2eCoverage), the build compiles, unit tests pass (ViewModel + UseCase + Repository +
fakes), architecture conformance holds (presentation doesn't import data, the new
*Screen.kt is automation-reachable — a literal testTag or screenTag = wiring into a
registry component — the new *ViewModel.kt has a matching test, and it references no
CircularProgressIndicator/LinearProgressIndicator directly), the golden tree
matches what you just captured, and accessibility holds. Not done until this is PASS and the
evidence receipt (qa/evidence/latest.json) is committed with your change — this is this
project's standing definition of done (see CLAUDE.md).
If it fails: read the failing step's reason (it is worded for exactly this), fix the actual behavior or spec/test binding, and re-run. Do not delete or weaken a test to reach green.
Guardrails
- This flow works identically with or without the create-cmp Claude Code plugin installed —
everything it needs (
qa/scaffold-feature.mjs, this file) ships inside the generated project. - Respect whichever toggles this project was stamped with (e.g. if Room or Appium/Maestro were disabled at scaffold time, don't reintroduce them for the new feature).
add-featuregenerates a pushed-nav-route screen, not a bottom-nav tab, and does not generate a "tap → detail" destination (the exemplar'sDetailScreen.ktis intentionally not copied — MVP scope). Both are documented future extensions, not omissions to silently work around.