# Discovery Phase (Steps 0-2)

## Step 0 — Context Recovery

Run BEFORE any other step.

1. **Worktree lookup (since v3.22.0)** — read `.worktrees/registry.json` from
   the main repo and scan for an entry with `kind: "docs"` whose `slug` matches
   the feature the user just mentioned (or that you can infer from their
   message — e.g., `/prd-add` for an active PRD).
   - **Found — worktree path exists on disk**: `cd` into the worktree's `path`
     (save as `$WORKTREE_PATH`), verify that `pwd` prints the worktree path,
     log "Resuming PRD session in worktree `<path>`", then continue to step 2.
     All subsequent file paths in this skill resolve under `$WORKTREE_PATH`
     (HARD RULE 17).
   - **Found — worktree path MISSING on disk** (`stale-registry-but-worktree-exists`):
     the registry entry is present but the directory was deleted (e.g. after a
     failed `/cw` or disk cleanup). HALT and surface:
     `WORKTREE_STALE: Registry entry for slug '<slug>' points to '<path>' but
     that path does not exist on disk. Options: (a) re-create the worktree with
     the same slug via nw-docs and restore files from git history, (b) start a
     fresh session (new slug).`
     **Do NOT proceed** — writing files without a valid working directory will
     silently corrupt the session.
   - **Not found, but main repo has a stale state file** at
     `${paths.prd_dir}/sessions/*-<slug>-state.md`: this is a pre-v3.22.0 PRD
     session created before the worktree mandate. ASK the user: "Found a
     legacy PRD state file outside any worktree. Migrate it to a fresh docs
     worktree before resuming, or continue on the main repo (legacy mode)?"
     If migrate: create the worktree via `nw-docs`, `git mv` the relevant
     files into it. If legacy mode: STOP and warn that parallel sessions
     will pollute `git status`.
   - **Not found and no state file**: fresh session, proceed to Step 1.
2. **Only when `$WORKTREE_PATH` is non-null** (i.e. this is a resume, not a
   fresh session): check if `$WORKTREE_PATH/${paths.prd_dir}/sessions/` contains
   a state file for the current feature. If `$WORKTREE_PATH` is null (fresh
   session), skip this check entirely and proceed to Step 1.
3. If state file found: read it, identify last completed step and any
   in-progress sub-state, then apply the **resume dispatch table** below.
4. If state file not found: proceed to Step 1.

**Resume dispatch table** — after reading the state file, check its status
field and apply the matching branch before resuming the normal step sequence:

| State file status | Branch |
|---|---|
| `mockups.status: requested` AND `mockups.format: pending` | **`mockups pending-input`**: the session was waiting for the user to provide mockup paths/images (Step 1.6.3 STOP). Re-ask: "Stiamo riprendendo la sessione PRD per `<slug>`. Stavi per fornirmi i mockup — in che forma me li passi? (immagini in chat / path locali / archivio .zip)" STOP and wait. |
| `status: research-in-progress` (background researcher was running) | **`research in-progress`**: log "Resuming — `senior-researcher` was running in background. Attempting to retrieve result." Invoke `senior-researcher` retrieve (or re-launch if result is unavailable). Process findings per the Research Trigger exit check (§ Exit condition) before proceeding. |
| `status: pending-external-handoff` | **`pending_external_handoff`**: a prior session ended mid-flow waiting on an external dependency (API contract, design handoff, credentials, etc.). Inform the user: "La sessione era in attesa di: `<state.pending_reason>`. Il blocco è stato risolto?" Wait for confirmation before resuming. |
| Any other status | Resume from the next uncovered step/dimension normally. |

Triggers: session start, context compression, re-invocation of `/prd`, every
`/prd-add` invocation.

## Step 1 — Kickoff

**Precondition:** user has described the feature; Step 0 ran and confirmed
this is a fresh session (no existing state file).

1. Derive `<slug>`: kebab-case, lowercase, max 40 chars, no special chars.
   Example: "Nutritional info on menu" -> `menu-nutritional-info`
2. **Obsidian spec detection (and back-reference setup):** scan the user's message
   for a reference to an Obsidian note used as the documental starting point. Accept
   any of these forms:
   - Wikilink: `[[Note Name]]` or `[[Note Name|Alias]]`.
   - `obsidian://open?vault=<Vault>&file=<rel/path/Note>` URI (URL-decode `file`).
   - A filesystem path to a `.md` file the user pastes ("la nota è in `…/Vault/…/Note.md`").

   If found, populate the `## Obsidian Spec Note` section of the state file with:
   - `wikilink:` — the `[[Note Name]]` form (for the PRD Canonical Sources row, see
     prd-writing-phase.md Step 4 point 2).
   - `path:` — the **absolute** filesystem path of the note. Resolve it now so the
     end-of-run back-reference (validation-phase Step 7.6) can write to it without
     re-deriving:
     - URI/full path given → use it directly (URL-decode + join `vault`+`file`).
     - Only a bare wikilink given → you need the vault root to resolve it. Use the
       vault path recorded in memory (`reference_obsidian_vault.md`) if present; else
       **ASK the user once** for the vault root (or the note's full path) and persist
       their answer in this state section. Do NOT assume a default. If the user
       declines, set `path: none` — detection still works for pre-population, but the
       Step 7.6 writeback is skipped (logged, never fatal).
   - `status: detected`.

   Then read the note content (from `path:`) to pre-populate discovery dimensions
   where possible — treat it as a user-provided spec. This same note becomes the
   target of the back-reference snippet at the end of the run (HARD RULE 19).

   **Provisional back-reference (NON-BLOCKING — v4.27.0).** Immediately after detection,
   write a *provisional* snippet into the note so it reflects "a PRD was started here" even
   if this session is later paused or abandoned before merge (the FINAL snippet at
   validation-phase Step 7.6 is gated on a successful merge and would otherwise never run
   for an incomplete PRD). Follow [obsidian-backref.md](obsidian-backref.md) **§ Mode:
   PROVISIONAL** — the marker block is keyed on `<slug>`, so the FINAL write later replaces
   it in place (no duplicates). All skip conditions in that module apply (no note / `path:
   none` / unwritable → log and continue, never fatal). On success the module sets
   `## Obsidian Spec Note / status: provisional-written`. The branch is the deterministic
   `prd/<slug>` that `nw-docs` creates in the next point.
3. **Create the docs worktree (MANDATORY — HARD RULE 17)** — invoke the
   `worktree-manager` skill in programmatic docs mode BEFORE any file write:

   ```
   Skill: worktree-manager
   Mode:  programmatic, nw-docs
   Input: { "slug": "<slug>" }
   ```

   On success the skill returns `{ path, branch, kind: "docs" }`. Save
   `path` as `$WORKTREE_PATH` for the entire session.

   Verify the working directory is the worktree:

   ```bash
   cd "$WORKTREE_PATH"
   pwd
   # MUST print the absolute worktree path. If pwd shows the main repo root,
   # STOP — every Write/Edit you make next will pollute the main checkout.
   ```

   **Failure handling.** If `nw-docs` fails (e.g., `.worktrees/` not in
   `.gitignore`, slug already in registry as a non-docs entry, repo not
   initialized), STOP and surface the error verbatim. Do NOT fall back to
   writing on the main repo — that breaks HARD RULE 17.

4. **Create state file** inside the worktree:
   `$WORKTREE_PATH/${paths.prd_dir}/sessions/YYYY-MM-DD-<slug>-state.md`
   using template from `assets/state-template.md`. Populate the `## Worktree`
   section with `path`, `branch`, `kind: docs`, `created_at` (ISO-8601 UTC).
5. Create 5 macro-tasks via TaskCreate:
   - `[PRD] Discovery & comprensione feature`
   - `[PRD] Design UI & approvazione`
   - `[PRD] Conferma specifiche complete`
   - `[PRD] Creazione backlog cards`
   - `[PRD] Validazione, commit & merge`
6. Mark task 1 as `in_progress`.
7. **Context Loading (MANDATORY):** invoke the context-primer skill (`/cont`) with
   2-4 keywords extracted from the user's feature description and
   `task_type: "new-feature"` (always explicit — do NOT let context-primer
   default to `"query"` for domain keywords, which produces a narrower
   context). context-primer internally calls `codebase-architect`
   which routes through the canonical registry, checks docs, backlog, source code,
   and git history. The returned context informs all subsequent discovery
   dimensions. **Do NOT issue a separate redundant retrieval pass** — context-primer
   already covers it, and a second pass with the same query wastes context window.
8. Mark clearly irrelevant dimensions as "N/A" with reason
   (e.g., backend-only -> UI impact = N/A).
   If UI impact is N/A, immediately mark task 2 as `completed` ("Skipped — no UI").
9. **Business Rationale Extraction (MANDATORY):** Before entering the discovery loop,
   evaluate the user's initial feature description for business rationale signals:
   - WHY is this feature needed? (business motivation)
   - What customer/market signal triggered it?
   - What is the expected business impact?
   - How does it align with the product strategy?

   If the user's description already covers these clearly, capture them in the state
   file under `## Business Rationale` and mark the dimension as covered
   (source: "user-provided").

   If the description is purely functional (WHAT, not WHY), leave the dimension
   uncovered — it will be the FIRST question in the discovery loop (Dimension 0).

**Output:**

```
Ho creato la sessione PRD per **<slug>** in un worktree dedicato.

Worktree:    <relative-worktree-path>
Branch:      prd/<slug>
Descrizione: <user's description>
Contesto:    <one-line summary from context-primer>

Tutti i file del PRD vivono nel worktree — il main repo resta pulito.
Al termine commit + merge automatico in `git.trunk_branch` + cleanup worktree, seamless.
(Se in qualunque momento mi dici "non mergiare", mi fermo dopo il commit.)

Ora comincio la fase di **Discovery** — ti faro delle domande per capire bene
la feature prima di scrivere qualsiasi documento.

<Progress Bar>
```

**Gate (BLOCKING).** Before leaving Step 1 you MUST verify, on disk, that the
docs worktree actually exists: `pwd` prints the absolute worktree path AND its
registry entry (`kind: "docs"`) is present AND the state file exists inside the
worktree AND all tasks were created AND context loaded. If the worktree was NOT
created — e.g. `nw-docs` aborted because no trunk branch could be resolved, or
`.worktrees/` is not in `.gitignore` — **HALT and surface the `nw-docs` error
verbatim**. Do NOT show the "Ho creato la sessione PRD … in un worktree
dedicato" message and do NOT proceed to Discovery: writing PRD artefacts on the
main checkout breaks HARD RULE 17 and pollutes every parallel session. This is a
hard gate, not an observability note — the kickoff output above is emitted only
after this gate passes.

**Immediately proceed to Step 1.6** — do NOT stop here. Show the kickoff message
and ask the mockup intake question in the same turn.

## Step 1.6 — Mockup Intake (MANDATORY)

Runs between Kickoff (Step 1) and Discovery (Step 2). Cannot be skipped.

**Purpose:** detect whether the user already has mockups for this feature, archive
them as canonical artefacts, and feed the visual analysis into Discovery alongside
the codebase context from `/cont`. The Step 3 (UI Design) decision tree consumes the
result.

### 1.6.1 — Ask

After the kickoff output (Step 1, point 9 — Business Rationale Extraction is the final point of the Kickoff step since the v3.22.0 renumbering), append this question to the SAME message:

```
### Domanda — Mockup

Hai dei mockup a disposizione per questa feature?

- **Sì** → posso analizzarli prima di iniziare la discovery, così le domande
  saranno calibrate su quello che è già definito visivamente.
- **No** → procediamo con la discovery standard; se serve, in Step 3 genero
  io 3 opzioni di design.
```

**STOP.** Display Progress Bar (row `1.6 Mockup intake: 🔄`) and wait for the user.

### 1.6.2 — Answer = "No"

- Update state file `## UI Design`: `mockups.status: none`.
- Mark progress bar row `1.6 ✅ (no mockups)`.
- **Immediately proceed to Step 2** in the same turn. No further STOP.

### 1.6.3 — Answer = "Sì"

Append this follow-up to the SAME message:

```
Perfetto. In che forma me li passi?

- **Immagini in chat** — incollale direttamente nel prossimo messaggio.
- **Path locali** — incolla i percorsi (assoluti o relativi al repo). Funzionano:
  - file singoli (PNG, HTML, SVG, …) → li copio in `${paths.prd_dir}/<slug>/mockups/`.
  - **archivi `.zip`** (es. export "Download as .zip" da Claude Design già
    scaricato sul tuo disco) → lo unzippo io, analizzo TUTTI i mockup dentro
    e seleziono automaticamente quelli rilevanti per questa feature.
- **Misti** — combinazioni delle modalità sopra.
```

- Update state file `## UI Design`: `mockups.status: requested`, `mockups.format: pending`.
- **STOP.** Wait for the user.

### 1.6.4 — Mockup reception & archival

When the user replies with the mockups:

1. **Detect format** and update `mockups.format` ∈ {`chat-images`, `local-paths`, `local-archive`, `mixed`}.
   - `local-archive` applies when at least one of the provided local paths is a
     `.zip` (case-insensitive) — also if mixed with other regular files.
2. **For each local path provided:**
   - Resolve to absolute path. If file does not exist: ask the user to confirm/correct
     (do not silently skip).
   - **If the path is a `.zip`** (extension match OR `file <path>` reports a Zip
     archive): route it to Step 1.6.4b and DO NOT copy the zip itself into
     `${paths.prd_dir}/<slug>/mockups/`. Only the user-confirmed subset extracted
     from it ends up there.
   - **Otherwise** (regular mockup file): ensure `${paths.prd_dir}/<slug>/mockups/`
     exists (create if missing), then copy the file into that directory,
     preserving filename. On collision (file with same name already there):
     append numeric suffix (`<name>-2.png`).
   - Record both paths in state: `mockups.original_paths[]` and `mockups.canonical_paths[]`.
3. **For chat images:** record in state `mockups.original_paths[]` as `chat://image-N`
   (the images remain in the conversation context window).
4. **For zip archives:** execute Step 1.6.4b BEFORE marking `status: provided`. The
   archive flow has its own scan + selection + confirmation gate; only the
   user-confirmed subset is copied into `${paths.prd_dir}/<slug>/mockups/`.
5. Update `mockups.status: provided` (or `partial` only if the user explicitly says
   "ho solo questi 2 ma servono anche altri").

### 1.6.4b — Archive ingestion & relevance selection (when `mockups.format` includes `local-archive`, OR the user gives a Claude-Design bundle directory)

Triggered when the user hands a local `.zip` path (typically a Claude Design
"Download as .zip" export already on disk) **OR** a path to an already-extracted
**bundle directory** (the user downloaded the whole Claude Design workspace as a
folder). The flow resolves the subset of files belonging to THIS feature, then asks
the user to confirm before analysis.

Resolution is two-tier: a **structural pass** (step b2) that exploits Claude
Design's native bundle structure when present, with the **keyword scorer** (step c)
as the fallback. The STOP confirmation gate (step d) is identical in both modes.

#### Step a — Detect & accept root

For each path provided:

```bash
file "<user-path>"   # only meaningful for files
```

- **Directory** (the path is a folder): skip unpacking. Record:
  - `mockups.archive.kind: directory`
  - `mockups.archive.extracted_dir: <the user dir>` (absolute)
  - `mockups.archive.local_zip_path: n/a`
- **`.zip`** (`file` reports a Zip archive): record:
  - `mockups.archive.local_zip_path: <user-zip-path>` (absolute)
  - `mockups.archive.kind: zip`
  - Unpack into a scratch directory under the PRD's mockup folder:

  ```bash
  mkdir -p "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive"
  unzip -q "<user-zip-path>" \
    -d "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive/extracted"
  ```

  - Record `mockups.archive.extracted_dir` (the `extracted/` path).
  - If `unzip` fails (corrupted/encrypted): STOP, surface the exit code, ask the
    user to verify the zip or provide an alternative.
- **Regular non-zip file**: treat as a single file — go back to step 1.6.4 point
  2's "Otherwise" branch, no special handling.

Both directory and zip branches converge on a single `root` =
`mockups.archive.extracted_dir`. Everything below operates on `root`.

#### Step b — Enumerate candidates

Scan the extracted tree for mockup-shaped files. The candidate filter is:

- Extensions: `.html`, `.htm`, `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp`, `.gif`, `.pdf`.
- Exclude: `node_modules/`, `.git/`, anything under a directory named `assets/`
  whose parent is itself excluded, `*.min.*`, `thumbs.db`, `.DS_Store`.
- Size sanity: skip files > 50 MB (likely video/binary blobs, never a mockup).

```bash
find "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive/extracted" \
  -type f \
  \( -iname '*.html' -o -iname '*.htm' -o -iname '*.png' -o -iname '*.jpg' \
     -o -iname '*.jpeg' -o -iname '*.svg' -o -iname '*.webp' -o -iname '*.gif' \
     -o -iname '*.pdf' \) \
  -size -50M \
  -not -path '*/node_modules/*' -not -path '*/.git/*' \
  -not -name '*.min.*' -not -name 'thumbs.db' -not -name '.DS_Store'
```

For each candidate, build a lightweight index entry:
- `path` (relative to `extracted/`)
- `filename`
- `ext`
- `size_kb`
- For HTML: `<title>` content + first `<h1>` (use `grep -oE` or a single Read of
  the first ~200 lines; do NOT load the full file — they can be massive).
- For images: nothing extra at this stage (filename is the only cheap signal).

Cap the indexed list at 200 entries. If the archive contains more, sort by
`size_kb DESC` (larger files are typically the rendered screens) and keep the
top 200, recording `mockups.archive.truncated: true`.

Record the full index in state under `mockups.archive.candidates[]`.

#### Step b2 — Claude-Design structural resolution (Feature-Subset Resolution)

A Claude Design export is **not** a flat pile of files: it carries native
structure that pins each feature's screens deterministically. When that structure
is present, resolve the subset *structurally* and skip the keyword scorer. When it
is absent (or the canvas anchor stays ambiguous), fall through to step c unchanged.

> **Global contract.** The structural resolver is an **accelerator**. Any failure
> at any step (A0–A6) falls through to the keyword scorer (step c) + the STOP gate
> (step d), which is already proven. **The PRD never aborts on intake.**

**A0 — Signature gate.** On `root`, compute:
- `has_manifest` — `[ -f "$root/.design-canvas.state.json" ]`.
- `has_canvas_jsx` — a `design-canvas.jsx` exists, or some `*.html` loads it via
  `<script type="text/babel" src=…>` and references `data-dc-section`.
- `feature_html_count` — number of top-level `*.html` whose **source** contains an
  `id="s<N>"` slot (see A2 — this is the static signal; `data-dc-slot` only exists
  at runtime after Babel renders, so do NOT grep for it here).

Set `mockups.archive.bundle_kind`:
- `claude-design` iff `has_manifest` OR (`has_canvas_jsx` AND `feature_html_count ≥ 1`) → run A1–A6.
- otherwise `generic` → **skip to step c** (keyword scorer), unchanged.

**A1 — Canvas anchor (feature_title → ONE canvas `*.html`).**
- Candidate set = top-level `*.html` MINUS the denylist of component/system
  canvases: `Design System.html`, `BottomNav*.html`, `*-bundle-src.html`,
  `* · standalone.html`. **Do NOT pre-filter by slot presence** — feature canvases
  in the wild use mixed slot schemes (some `id="s<N>"`, some semantic like
  `id="supplier-edit-a-add"`), and an `id="s<N>"`-only allowlist would wrongly drop
  a feature whose canvas doesn't use that numbering. Slot presence is a *confidence
  signal* (below), not a candidacy gate.
- For each candidate read its `<title>` + filename, score against `feature_title`
  + scope keywords (reuse step c's case-insensitive whole-word splitter): `<title>`
  match `+50`, filename `+40`, `id="s<N>"` slot-range hint `+10` (boosts canvases
  that expose clean screen-IDs, enabling A2's exact join). Record in
  `mockups.archive.canvas_candidates[]`.
- **Resolution:**
  - Exactly 1 candidate with a confident margin (top score ≥ 50 AND ≥ 2× the
    runner-up) → `mockups.archive.canvas_ref = that file`, `anchor_confidence: high`.
  - 0 clear, OR ≥ 2 within the 2× margin → **AMBIGUOUS**. Do NOT guess. Use
    `AskUserQuestion` listing the candidate canvases (filename + `<title>` + slot
    count): "Quale canvas è questa feature?". If the user says none/multiple →
    fall through to step c over all candidates (`anchor_confidence: fallback-keyword`).
    Multiple confirmed → A2 unions the slot sets and `canvas_ref` becomes a list.

**A2 — Slot extraction (canvas → screen IDs).** From `canvas_ref` source only.
Anchor on the **section** element that carries the screen title, NOT a loose
`title=` (sibling attributes like `subtitle="…"` contain the substring `title="`
and a greedy `id=…title=…` regex grabs the wrong one):
```bash
# DCSection is the per-screen container; its `title` is the screen label.
grep -oE '<DCSection id="s[0-9]+[a-z]*" title="[^"]*"' "<canvas_ref>"
#   <DCSection id="s31" title="3.1 · Wizard Step 3"
```
→ `{ slot_id, screen_id, title_raw }` per section. (If the bundle uses a different
section tag, anchor on whatever element holds the `id`+dotted-`title` pair.)
- Derive `screen_id` from the title's dotted prefix (e.g. `"3.1 · …"` → `3.1`).
  Cross-check against the slot number (`s31` → `3.1`). On disagreement, **trust the
  `title=` prefix** and log `slot_id_number_mismatch` (handles non-`s<N>`
  numbering). If neither yields a dotted id, leave `screen_id: null` — that screen
  joins downstream by the fuzzy fallback, never blocks.

**A3 — Manifest read (enrich, never gate).** If `has_manifest`, parse
`.design-canvas.state.json`. For each slot in A2:
- `sections["s31"].labels` → attach as candidate `states_visible[]` / variant labels.
- `sections["s31"].title` → prefer as the human label; else keep the A2 `title=`.
- Manifest is **partial and mixed-keyspace** (some keys are slot-ids, some are
  semantic section names belonging to OTHER canvases). Keys not in this canvas's
  slot set are ignored. A slot missing from the manifest is the **normal case**,
  not an error. Set `screens[].source` = `manifest` (labelled) or `title` (not).

**A4 — Sidecar gather.**
- **JSX deps** — the `<script type="text/babel" src="X.jsx">` list inside
  `canvas_ref` IS the authoritative dependency set (needed for A5 render). Record
  for materialization in step e.
- **Pre-rendered PNGs** — scan `$root/screenshots/` (and any image dir) for files
  whose name confidently matches a slot number / slug (`ap-31.png` → `s31`). On a
  confident match set `screens[].prerender_png` + `source: prerender`. Naming is
  ad-hoc (`modale__valido.png`) so matching is **best-effort only**; unmatched
  images are parked, never force-assigned.

**A5 — Render canvas-only artboards.** Screens that exist only inside the canvas
(no confident `prerender_png`) must be rendered. Invoke
[canvas-intake-recipe.md](canvas-intake-recipe.md) (http-server + Playwright over
`[data-dc-section="s<N>"]`). Output `mockups/<slug>__<screen-id>.png`, record under
`mockups.canvas_render`. If render is impossible (Playwright/server unavailable),
**degrade**: keep pre-render PNGs, mark remaining slots `source: live-html`
referencing the canvas HTML directly, set `render_status: failed|skipped`, and
continue — never abort.

**A6 — Discard-and-log the rest.** Everything in `root` NOT in { `canvas_ref`, its
jsx deps, matched `screenshots/*`, shared assets (`*tokens*.css`, `*icons*.jsx`,
`*shell*.jsx`) } is out-of-feature: the other feature canvases, their jsx,
`frames/`, `uploads/`, `docs/`. Record in `mockups.archive.discarded[]` with a
one-word `reason`. Shown for transparency at the STOP gate (step d) but **not
copied**.

After A6, build the screen-centric selection and go to **step d** (its structural
variant). Skip step c entirely — it is the fallback path only.

#### Step c — Score against feature scope

> **Fallback path.** Runs ONLY when step b2 set `bundle_kind: generic`, or the
> canvas anchor stayed unresolved (`anchor_confidence: fallback-keyword`). For a
> resolved Claude-Design bundle, skip directly to step d.

Build the scope vector from the Kickoff state (Step 1):
- `feature_title` (from `## Feature Description`).
- `slug`.
- ISA touchpoints (if Discovery dimension 9 already ran).
- Verbatim screen names the user mentioned in the initial description.
- Keywords from `feature_objective_one_sentence` (strip stopwords).

For each candidate, compute a relevance score 0-100
[CALIBRATION-NEEDED: weights below are heuristic; validate against a labeled mockup corpus]:
- `+40` if any scope keyword appears in the filename (case-insensitive,
  whole-word; for kebab/snake/camelCase, split before matching).
- `+30` if any scope keyword appears in the HTML `<title>` or `<h1>`.
- `+15` if the file path contains a directory name matching a scope keyword.
- `+10` if the filename matches typical screen patterns (`screen`, `page`,
  `view`, `flow`, the user's domain entity names).
- `-20` if the filename matches typical non-screen patterns (`icon`, `logo`,
  `favicon`, `thumbnail`, `og-image`, `sitemap`, `manifest`).

Selection threshold [CALIBRATION-NEEDED: ≥30/≥20/min-3 are heuristic; no labeled
dataset validates these cut-offs]: keep candidates with `score ≥ 30` as the
auto-selected set. If fewer than 3 candidates clear the threshold, lower to
`≥ 20` and surface the fact. If zero candidates clear `≥ 20`, surface
"selezione non automatizzabile — ti chiedo di indicarmi a mano quali sono
rilevanti" and present the full candidate list.

Record per-candidate: `score`, `matched_keywords[]`, `selected: bool`.

#### Step d — Confirmation gate (STOP)

Two table variants, same STOP/adjust/re-present loop:

**Structural variant** (step b2 resolved a `canvas_ref`). Present **screen-centric**
— one row per resolved screen, plus the discarded-canvas summary:

```
Canvas individuato: «Approvazione Ordini.html» (feature «<feature_title>») — <K> schermate.

**Schermate risolte** (slot → screen-ID):
| # | Slot | Screen | Label                              | Sorgente  |
|---|------|--------|------------------------------------|-----------|
| 1 | s31  | 3.1    | Wizard Step 3 · CTA approvazione   | manifest  |
| 2 | s32  | 3.2    | Elenco approver                    | rendered  |
…

**Scartati** (altre feature / asset di supporto):
| File / dir                    | Motivo                |
|-------------------------------|-----------------------|
| Budget Pianificazione.html    | other-feature-canvas  |
| frames/, uploads/             | unrelated / upload    |

Procedo all'analisi delle <K> schermate? Oppure indicami modifiche
(es. "togli s37", "il canvas giusto è Budget Pianificazione").
```

**Keyword-fallback variant** (step c ran). Present file-centric — selected
candidates first, then up to 10 discarded with the highest scores, then a footer
count for "altri N scartati":

```
Bundle scaricato e analizzato: <N> file candidati, <K> selezionati come rilevanti.

**Selezionati** (score ≥ 30 vs. feature scope):
| # | File                          | Score | Match                  |
|---|-------------------------------|-------|------------------------|
| 1 | screens/lista-fornitori.html  | 75    | "fornitori", "lista"   |
| 2 | screens/dettaglio-ordine.html | 55    | "ordine"               |
…

**Scartati ad alto score** (per trasparenza):
| # | File                          | Score | Motivo                 |
|---|-------------------------------|-------|------------------------|
| 1 | screens/landing-marketing.html| 22    | nessun keyword match   |
…

Altri <M> file scartati (score < 20 o pattern non-schermata).

Procedo all'analisi visiva dei <K> selezionati? Oppure indicami modifiche
(es. "aggiungi il #3 degli scartati", "togli il #2 dei selezionati").
```

**STOP.** Wait for explicit confirmation or an adjustment list. On adjustment,
recompute the selection (`mockups.archive.candidates[].selected` in keyword mode,
the per-slot row set in structural mode), then re-present and STOP again until the
user confirms.

#### Step e — Materialize the confirmed subset

Copy ONLY the confirmed selection into `${paths.prd_dir}/<slug>/mockups/`,
preserving relative directory structure under `root` so that relative asset
references (CSS, images) keep working. On filename collision, use the
`<name>-2.<ext>` rule from 1.6.4 point 2.

**Structural mode also copies the render dependencies**, so the canvas stays
renderable/analyzable inside the PRD folder:
- `canvas_ref` itself + every jsx dep from A4 + shared assets (`*tokens*.css`,
  `*icons*.jsx`, `*shell*.jsx`), preserving relative paths so `<script src>` /
  `<link>` keep resolving.
- the rendered PNGs (`mockups.canvas_render.rendered_slots[].png`) and matched
  pre-render PNGs.
- **Write `screens_in_scope[]`** here — one row per resolved screen with `name`,
  `slot_id`, `screen_id`, `canvas_ref`. This is what lets the Step 3 (UI Design)
  Hybrid routing do an **exact join** instead of a fuzzy substring match (see
  [ui-design-phase.md](ui-design-phase.md) § "Build the per-screen routing table").

Record `mockups.original_paths[]` as `archive://<relative-path-in-zip>` (zip) or
`archive://<relative-path-in-root>` (directory) and
`mockups.canonical_paths[]` as the new path under `mockups/`. When `kind: zip`,
the scratch `_archive/extracted/` directory is kept for the duration of the PRD
session (may be needed to resolve `<link>` / `<img src>` references during
analysis) and cleaned up at Step 7 (Commit & Merge) — add a `.gitignore` entry for
`mockups/_archive/` if not already present. When `kind: directory`, `root` is the
user's own folder outside the worktree — never modify or clean it; only read.

Then continue to Step 1.6.5 with the selected subset.

### 1.6.5 — Mockup analysis (MANDATORY before Discovery)

Analyze ALL provided mockups (visual analysis for chat images, Read for copied files
that are textual like HTML/SVG). Populate `## UI Design` in the state file using the
**Mockup analysis schema** (see appendix at the end of this file).

When `features.has_design_system: true`, BLOCKING reads of `${paths.design_system}/INDEX.md`
and `${paths.design_system}/tokens-reference.md` apply BEFORE running the alignment
check — same protocol as Step 3 (UI Design). Flag every hardcoded value (hex / shadow /
radius / spacing) that conflicts with a known token as a violation in
`mockup_analysis.design_system_alignment.violations[]`.

### 1.6.6 — Output and handoff to Discovery

After analysis is complete, present to the user:

```
Mockup acquisiti e analizzati: N schermate identificate.

Schermate: <list>
Flusso utente inferito: <one-line summary>
Componenti riconosciuti: N (X riusati dal registry, Y nuovi)
Stati visibili: <empty/loading/error/success per schermata se rilevanti>
Allineamento design system: <aligned | N violations to resolve in Step 3>
Gap da chiarire in discovery: <list of ambiguities, max 5>

Ora entro in **Discovery** — le domande saranno calibrate su quanto già definito
nei mockup. Le ambiguità rilevate diventeranno domande mirate.

<Progress Bar>
```

- Mark progress bar row `1.6 ✅` (or `🔄` if `partial`).
- **Immediately proceed to Step 2** in the same turn.

### Coordination with `/cont`

The `/cont` skill is invoked at **Step 1, point 7** (after the worktree and
state file are created and macro-tasks are registered, but before the Business
Rationale Extraction at point 9). It runs synchronously and completes before
the kickoff output is displayed. The mockup intake question (Step 1.6.1) is
asked in the same output message; if the user answers "Sì", the STOP gives the
codebase context time to settle in conversation history while we wait. When the
mockups arrive, the visual analysis combines with the already-loaded codebase
context to inform Discovery dimension selection (especially dimension 5 UI
impact and partially dimension 2 User journey, both of which can be
pre-populated from `mockup_analysis`).

## Step 2 — Discovery Question Loop

Multi-turn loop. Each iteration covers ONE uncovered dimension.
Repeats until ALL relevant dimensions are covered.

**CRITICAL:** ONE question, then STOP. User answers. Process answer,
update state, send NEXT question, STOP again.

### Dimension 0 — Business Rationale (ALWAYS FIRST)

If already covered in Step 1, point 9 (Business Rationale Extraction), skip to dimension 1.

Otherwise, present this question:

> ### Domanda — Motivazione di business
>
> Capisco COSA vuoi costruire. Ora ho bisogno di capire il **PERCHE** — questa
> informazione viene documentata permanentemente per il team e per gli agenti futuri.
>
> 1. **Perché adesso?** Cosa rende questa feature urgente/importante in questo momento?
> 2. **Quale segnale** l'ha innescata? (feedback clienti, dati d'uso, pressione competitiva, requisito normativo, altro)
> 3. **Quale impatto** ti aspetti? (ricavi, retention, efficienza operativa, riduzione rischio)
> 4. **Come si inserisce** nella visione del prodotto?
> 5. **Alternative considerate?** Hai valutato altre strade e perché le hai scartate?
>
> Anche risposte brevi vanno bene — l'importante è che il "perché" resti documentato.

**STOP.** Wait for user response.

When user responds: record in Discovery Log AND populate `## Business Rationale`
section of the state file. Mark dimension as covered.

### Per-iteration sequence (dimensions 1-10)

**Precondition:** at least one dimension has status `-` (uncovered).

1. Select next uncovered dimension (in checklist order).
2. **Use the context already loaded by context-primer (Step 1, point 7)** to
   check if the codebase already answers this dimension. The context-primer
   output includes key docs, key code paths, status, and gotchas — use these
   to evaluate.
   If more specific detail is needed beyond what context-primer provided,
   invoke `codebase-architect` agent (foreground) with a targeted question:
   ```
   For feature `<slug>`: regarding the dimension `<dimension>`, does the codebase
   already answer this? Specifically check: existing collections, routes, components,
   permissions, and patterns. Report what you find with file references.
   ```
   Avoid launching codebase-architect when the context-primer output already
   provides sufficient information for the dimension.
3. **If the loaded context (or codebase-architect) provides a COMPLETE answer (no ambiguity):**
   - Mark dimension as covered (source: "codebase", with file refs)
   - Log: `**Auto-risolto:** dal contesto caricato [details]`
   - Show user: `Dimensione **<dimension>**: risolta automaticamente dal codice.
     [1-line summary]. Passo alla prossima.`
   - Continue to next dimension in SAME message (no user input needed).
   - If ALL remaining auto-resolved: show them all, display Progress Bar,
     proceed to next phase.

4. **If user input is needed:**
   Present ONE question with:

   ```
   ### Domanda N — <dimension>

   [Question]

   **Raccomandazione**: **[Option]** (Confidenza: ALTA/MEDIA/BASSA)
   > [Brief rationale]

   **Perche e importante**: [one line]

   **Alternative**:
   - **A)** [Recommended] <- consigliato
   - **B)** [Alternative]
   - **C)** [Alternative if relevant]
   ```

   **STOP.** Display Progress Bar and wait.

5. **When user responds:**
   - Record in Discovery Log: question, recommendation, user answer, source
   - Update checklist: mark dimension with source and detail
   - Recalculate comprehension: `covered / relevant * 100`
   - **Scope Expansion Check** (mandatory on every user answer):
     Scan the user's answer for NEW entities not in the original feature description:
     new endpoints, new collections, new pages, new roles, new flows, new integrations.
     If detected:
     - Inform: `Ho rilevato un possibile ampliamento di scope: **<entity>**.
       Questo sembra una nuova funzionalita non coperta dalla descrizione originale.
       Vuoi che avvii un'analisi di impatto (/prd-add) per integrarla nel PRD,
       oppure la notiamo come out-of-scope?`
     - **STOP.** Wait for user response.
     - If user confirms: record in the state file under `## Discovery Cursor`
       the current dimension index (e.g. `cursor: 5`) and the current
       comprehension score before entering the CR flow. Then invoke the
       canonical **`prd-add` skill** ([../../prd-add/SKILL.md](../../prd-add/SKILL.md))
       — the worktree-aware implementation, NOT the `prd-add-phase.md` stub — and
       execute the Change Request flow. After it completes, read `## Discovery Cursor` from the state
       file to restore position, then resume from the next uncovered dimension.
     - If user says out-of-scope: note in Discovery Log and continue.
   - **New-Screen Check** (only when `mockups.status` ∈ {`provided`, `partial`}):
     When the user's answer reveals a UI screen NOT present in
     `mockup_analysis.screens[]`, do NOT treat it as a scope expansion. Instead,
     append the screen to `screens_in_scope[]` in the state file with
     `covered_by_mockups: false`. Log inline:
     `Ho rilevato una nuova schermata non presente nei mockup: **<name>**. La
     genero in Step 3 (modalità ibrida) solo per quella schermata.`
     Continue with the next dimension. This is a routine signal for Step 3, not
     a scope expansion — the Scope Expansion Check above still applies for
     non-UI entities (endpoints, roles, collections).
   - Update state file
   - Proceed to next uncovered dimension (back to action 1)

### Dimension 9 — Integration Surface Analysis (ISA)

This dimension uses **automated codebase scan** — do NOT ask the user a generic question.
Instead, run the ISA scan and present findings for validation.

**ISA Scan Procedure:**

1. Invoke `codebase-architect` agent with this specific prompt:

   ```
   For feature `<slug>`: perform an Integration Surface Analysis (ISA).
   Find ALL existing code that should reference, link to, or interact with this
   new feature. Scan these categories systematically:

   - NAVIGATION: sidebar links, menu items, breadcrumbs, tab bars, footer links
   - PERMISSIONS: permission type enums/constants, role-check lists, guard clauses,
     permission management UI
   - SETTINGS/CONFIG: settings pages, feature flag lists, config files, env vars
   - SEARCH/DISCOVERY: search indexes, filter dropdowns, autocomplete sources,
     listing pages that should include this entity
   - NOTIFICATIONS: notification type enums, handlers, templates, push configs
   - DASHBOARD/OVERVIEW: summary pages, widgets, counters, activity feeds,
     recent-items lists
   - API CONSUMERS: existing client code that calls related APIs, shared API
     utilities, API route indexes
   - SHARED STATE: stores, contexts, providers that manage related data,
     session/cache keys

   For each touchpoint found, report:
   - Category (from above)
   - File path and line number
   - What action is needed (add link, add entry, add handler, etc.)
   - Risk if omitted (e.g., "page unreachable", "no access control", "data invisible")

   Also check: are there existing features that do something similar? How are THEY
   integrated? Use those as a pattern reference.
   ```

2. **Process findings:**
   - Build the ISA table from codebase-architect results.
   - Assign sequential ISA-N IDs to each touchpoint.
   - Classify each by risk: HIGH (feature broken/unreachable), MEDIUM (degraded UX),
     LOW (nice-to-have completeness).

3. **Auto-resolve (no user confirmation needed):**
   - Record final ISA table in state file under `## Integration Surface Map`.
   - Mark dimension as covered (source: "codebase-scan").
   - Show a brief summary inline (do NOT stop):

   ```
   **Auto-risolto:** ISA scan completato — N touchpoint trovati
   (X HIGH, Y MEDIUM, Z LOW). Dettagli nel state file.
   ```

   - Continue to next dimension in the SAME message.

**If codebase-architect finds 0 touchpoints** (rare — e.g., completely standalone utility):
   - Mark dimension as "N/A — standalone feature, no integration surface detected"
   - Log inline: `**Auto-risolto:** nessun punto di integrazione trovato — feature standalone.`
   - Continue to next dimension (no user confirmation needed).

### Dimension 10 — Test Strategy & Credentials (special handling)

This dimension has **built-in evaluation logic** — do NOT ask the user a generic question.
Instead, run the E2E Evaluation Decision Tree and present the result.

**E2E Evaluation Decision Tree:**

```
1. Does the feature have UI impact? (check dimension "UI impact")
   → N/A or no UI → E2E: NOT NEEDED (reason: "no UI impact")
   → Yes → continue to 2

2. Is it a multi-step flow? (booking, onboarding, checkout, wizard, multi-form)
   → Yes → E2E: REQUIRED
   → No → continue to 3

3. Is it on a critical path? (auth, payment, booking, permissions, transaction)
   → Yes → E2E: REQUIRED
   → No → continue to 4

4. Does it have >3 UI acceptance criteria?
   → Yes → E2E: RECOMMENDED
   → No → continue to 5

5. Is it a simple display/component with no complex interaction?
   → Yes → E2E: NOT NEEDED (reason: "simple display, no complex interaction")
   → No → E2E: RECOMMENDED
```

**Result categories:**
- **NOT NEEDED**: auto-resolve — log reason, mark dimension covered, show inline
  summary, continue to next dimension (no STOP, no user confirmation needed).
- **RECOMMENDED**: present the verdict and ask for explicit confirmation — do
  NOT auto-accept. Show inline:
  ```
  E2E: RECOMMENDED (>3 UI acceptance criteria, no critical-path triggers).
  Vuoi includerla nel piano di test? (Sì / No)
  ```
  **STOP.** If the user confirms: proceed to credential gathering. If the user
  declines: log `e2e: skipped (user declined RECOMMENDED)`, mark dimension
  covered, continue.
- **REQUIRED**: E2E tests MUST be planned. Proceed to credential gathering.

**When E2E is REQUIRED or user accepts RECOMMENDED:**

1. Identify test scenarios from user stories and acceptance criteria (2-5 scenarios max).
2. **Determine test personas** by intersecting:
   - The user-stories' `As a <persona>` field, and
   - `identity.audience_segments[]` from `baldart.config.yml` (single source of truth
     for the project's personas — NEVER hardcode persona names in the skill).

   For each persona that needs E2E coverage, gather credentials using the following
   resolution order:

   **a. Consult `.baldart/overlays/prd.md`** (if present) for a `## Test Credentials`
      section. If the overlay defines a default credential pattern for the persona
      (e.g. `customer → phone OTP`, `merchant → username/password`), use it and
      log inline: `Credenziali test {{persona}}: caricate da overlay.`
      Do NOT print the actual credential values inline — only confirm the source.

   **b. Otherwise ASK the user**, ONE question per persona:
      ```
      ### Domanda — Credenziali test {{persona}}

      Questa feature richiede test E2E con la persona **{{persona}}**.
      Fornisci le credenziali di test (o un riferimento all'ambiente di test):

      - Metodo di auth: <es. password / OTP via SMS / magic link / API token>
      - Identificativo: <es. username, email, phone>
      - Secret: <password, OTP source, token — meglio NON incollarlo in chat,
        usa un riferimento al vault>
      - Contesto (se rilevante): <es. tenant, workspace, store, organization>
      ```
      **STOP.** Wait for user response. Store the *minimum-necessary* credential
      reference in the state file under `## Test Plan` — prefer a reference to a
      secret manager / 1Password / `.env.test` path over the literal secret.

      Suggerisci all'utente di consolidare la risposta in `.baldart/overlays/prd.md`
      (sezione `## Test Credentials`) per evitare la stessa domanda ai PRD futuri.

   **c. Mixed personas:** ripeti b. per ciascuna persona non risolta da a.

3. After credentials are gathered, summarize the test plan:
   ```
   ### Test Plan Summary

   E2E: REQUIRED | RECOMMENDED (accettato)
   Scenari: N test scenarios
   Personas coinvolte: <list from identity.audience_segments[] involved>
   Sorgente credenziali: [overlay | user-provided | mixed]

   Scenari pianificati:
   1. [scenario description — maps to US-X / AC-X]
   2. [scenario description — maps to US-X / AC-X]
   ```

4. Mark dimension as covered. Update state file `## Test Plan` section.

> **Security note:** the framework MUST NOT contain any real test credential
> (phone numbers, usernames, passwords, tokens). All credential examples in this
> file are intentionally generic. Project-specific defaults live in
> `.baldart/overlays/prd.md` (consumer-owned, not redistributed).

### Research Trigger (SEMANTIC gate — evaluated at ~75% comprehension)
[CALIBRATION-NEEDED: "6 non-test dimensions" ≈ 75% of 10 is the trigger point; validate
threshold against session data — earlier triggers may reduce re-work on complex features.]

When 6+ non-test dimensions are covered, evaluate complexity signals from
discovery answers and `codebase-architect` findings using the signal table
S1–S6 below. (The same table also appears in `research-phase.md`; either
source is authoritative — do NOT issue a separate file-read mid-discovery.)

| ID | Signal | Example |
|----|--------|---------|
| S1 | Unfamiliar technical domain | edge caching, payment SCA, WebRTC |
| S2 | Regulatory / compliance requirement | GDPR, PCI-DSS, PSD2 |
| S3 | Complex algorithm / heuristic | scoring, ranking, scheduling |
| S4 | Integration with external system | third-party API, webhook consumer |
| S5 | User expressed uncertainty | "non so se X sia fattibile" |
| S6 | Prior art uncertain | no known precedent in codebase |

**SEMANTIC decision:**
- **2+ signals OR regulatory (S2) OR user uncertainty (S5)** → HIGH confidence
  → launch `senior-researcher` in **background immediately**, continue discovery.
  Update progress bar: `Ricerca: 🔍 in corso (background)`.
- **1 non-critical signal** → MEDIUM confidence → note for later (ask at exit).
- **No signals** → skip research. Update progress bar: `Ricerca: ⏭️ non necessaria`.

If research launched in background: it runs in parallel with remaining questions.

### Exit condition

Comprehension >= 99% OR user explicitly says to proceed.

**Comprehension formula:** `covered / relevant * 100`, where:
- **`relevant`** = all dimensions whose status is not "N/A".
- **`covered`** = dimensions whose status is "covered" (any source).
- Dimension 9 (ISA — auto-scan) and dimension 10 (Test Strategy) ARE included
  in both `relevant` and `covered` once they resolve. The Research Trigger
  uses "6+ non-test dimensions" to avoid waiting for dim 10, but the exit
  formula counts all non-N/A dimensions uniformly.

**Before exiting, resolve mockup gaps (BLOCKING check):**
Scan `mockup_analysis.screens[].gaps[]` across all screens in the state file.
Every gap entry must be either:
- Converted to a Discovery question (already asked and answered), OR
- Explicitly recorded in the Discovery Log as `"skipped — outside mockup scope"`.

If any gap remains unresolved (neither answered nor skipped), DO NOT proceed
to Step 3/4. Either ask the gap as the next Discovery question, or ask the
user: `"Gap '<gap text>' dal mockup non è stato risolto. Lo includo come
domanda di discovery, oppure lo segniamo come fuori scope?"` **STOP** and wait.

**Before exiting, check research status** (refer to signal table in § Research
Trigger; read `research-phase.md` only if you need its credential-gathering
appendix or extended context, not for the signal table):
1. **HIGH confidence, researcher returned** → process findings, ask follow-up
   questions if gaps found, then exit.
2. **HIGH confidence, researcher still running** → inform user, wait for result.
3. **MEDIUM confidence (not yet asked)** → ask user if they want research.
   If yes, launch in foreground and process. If no, mark skipped.
4. **No research needed** → exit normally.

**Before exiting, run the Discovery Completeness Cross-Check (Codex — since v4.19.0):**

This is the **final, single cross-model sweep** of discovery, run ONLY once all the
gates above pass (99% comprehension OR user-proceed, mockup gaps resolved, research
resolved) and BEFORE flipping the status to `ui-design`. Its purpose is the one thing
the 99% formula structurally cannot do: catch the questions that *should* have been
asked but weren't. The formula measures coverage of the **predefined** dimensions; a
different model reading the discovery transcript catches the **questioner's own blind
spots** — latent ambiguities, missing NFRs, unhandled error/edge paths, data lifecycle,
permission boundaries, integration touchpoints that no dimension modelled. Catching a
requirements gap HERE (pre-design) is far cheaper than at the Step 6.6d plan audit
(post-PRD, post-cards). [DESIGN-CHOICE: cross-model diversity has its highest marginal
value against the blind spots of the model that conducted the interview]

**Once-per-session guard (MUST — prevents a re-sweep loop).** The exit condition is
re-evaluated every time the loop tries to exit; addressing a surfaced gap re-enters the
question loop and then re-reaches this point. So FIRST: if the Discovery Log already
contains a `discovery-completeness:` entry (from a prior pass this session), **skip this
block entirely and proceed to exit** — the cross-check is run AT MOST ONCE per discovery
session, never re-run after its gaps have been processed. Only if no such entry exists do
you run the sweep below.

**This check is NEVER a hard block** (unlike the mockup-gap check, which gates on physical
screens). It surfaces gaps and lets the user decide; it never STOPs the exit on its own.

```bash
SESSION_ID="${SESSION_ID:-$(date +%s%N | md5sum | head -c8)}" && \
CHECK_FILE="/tmp/codex-discovery-check-<slug>-${SESSION_ID}.md" && \
CODEX_SCRIPT="$(ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1)" && \
[ -z "$CODEX_SCRIPT" ] && echo "CODEX_NOT_FOUND" && exit 1; \
node "$CODEX_SCRIPT" task --wait "
<task>
Adversarial DISCOVERY-completeness review — a pre-design quality gate (review-only, DO NOT edit).
Claude has just finished interviewing the user to scope a feature. Your job is to find the
questions that SHOULD have been asked but were NOT, and the requirements that remain ambiguous —
the blind spots of the interviewer. Default to skepticism; assume the interview missed something.

Read the discovery state file below — it contains the dimensions, the answers, the Discovery Log,
AND the original kickoff brief in its '## Kickoff' section (everything you need is in this one file):
  ${STATE_FILE_PATH}
</task>

<attack_surface>
- Unasked questions: a concern specific to THIS feature that none of the standard dimensions captured.
- Latent ambiguity: an answer marked 'covered' that is actually vague, shallow, or under-specified.
- Non-functional requirements never raised: performance budgets, scale/limits, concurrency, auth/permission boundaries, data retention/lifecycle, privacy/PII, observability.
- Missing error/edge/failure paths (the interview only covered the happy path).
- Integration touchpoints not discussed: external systems, webhooks, third-party APIs, downstream consumers.
- Conflicting or unbounded requirements ('all', 'every', no limit) accepted without a boundary.
</attack_surface>

<grounding_rules>
Every finding MUST quote the exact dimension/answer (or note its absence) from the state file.
Do not invent gaps without evidence. Prefer a few strong, high-cost gaps over many weak ones.
Suppress a finding when the strongest argument that it is already covered is convincing.
</grounding_rules>

<structured_output_contract>
For each genuine gap, one line:
- **Gap title** — what is missing/ambiguous. (Severity: HIGH/MEDIUM/LOW) [Target: <dimension or topic>]
  > Evidence: exact state-file quote or 'no dimension covers X'
  > Suggested question: the discovery question that closes it
End with: 'COMPLETE — no material gaps' if the discovery is genuinely sufficient.
</structured_output_contract>
" 2>&1 | tee "$CHECK_FILE"
```

- `${STATE_FILE_PATH}` = `$WORKTREE_PATH/${paths.prd_dir}/sessions/YYYY-MM-DD-<slug>-state.md` (the single artifact Codex reads — it already embeds the kickoff brief in its `## Kickoff` section, so no separate brief file is passed). **Timeout**: `timeout: 300000` (5 min).
- **Present + process the findings (ONE pass):**
  - **Presentation is ONE message** (an explicit, documented exception to HARD RULE 6's one-question-per-turn, exactly like the mockup-gap presentation): show the gaps as a single concise completeness summary, grouped by severity. This summary is a *report*, not a discovery question, so it does not violate the one-question rule.
  - For each HIGH/MEDIUM gap the user chooses to address, it becomes the **next Discovery question** and follows the **normal per-iteration flow** — asked one-per-turn (HARD RULE 6), its answer folded into the state file, and run through the **Scope Expansion Check** like any other answer (so a gap whose answer reveals new scope routes to `/prd-add` exactly as a normal answer would — no special-casing, no double-trigger). Any gap the user declines → record in the Discovery Log as `"skipped — <reason>"`. LOW gaps are advisory: log them, never block.
  - **Respect explicit user intent**: if the user reached this point by explicitly saying *proceed* / *basta domande* (the OR-branch of the 99% gate), keep the check **advisory-only** — present the one summary, default to recording every gap as `skipped — user proceeded`, and do NOT re-open the interrogation unless the user opts in. Do not turn an explicit "proceed" into a fresh question loop.
  - **After processing (whatever the outcome), write ONE `discovery-completeness:` entry to the Discovery Log** (e.g. `discovery-completeness: 2 gaps asked, 1 skipped` / `COMPLETE — no material gaps`). This entry is the once-per-session guard above — its presence means the sweep has run and MUST NOT run again this session.
- **Fallback (Codex unavailable — `$CHECK_FILE` empty, contains `CODEX_NOT_FOUND`, or holds no parseable findings)**: **skip the cross-check** and log `discovery-completeness: SKIPPED (Codex unavailable)` in the Discovery Log (this also satisfies the once-per-session guard). This gate is **additive** — the 99% formula + mockup + research gates already gate exit. Do NOT fall back to a same-model Claude pass: the entire value here is cross-model diversity, and Claude just conducted the interview (it carries the very blind spot this check exists to catch), so a Claude self-review adds little and is not worth the spawn. Proceed to exit.

When loop exits: mark task 1 as `completed`. Update state file status to
`ui-design` (or `specs-confirmed` if UI is N/A).

**Output:**

```
Discovery completata! Tutte le dimensioni sono coperte.

[Brief summary of key decisions made]
[If research: brief summary of key findings]

Passo alla fase successiva: [UI Design / Scrittura PRD]

<Progress Bar>
```

---

## Appendix — Mockup analysis schema

Used by Step 1.6.5. Populate the `## UI Design` section of the state file with this
structure. Be specific — vague analysis defeats the purpose of skipping Step 3.

```yaml
mockups:
  status: provided | partial | none | requested
  format: chat-images | local-paths | local-archive | mixed
  original_paths:
    - <user-provided path, chat://image-N, or archive://<rel-path-in-zip>>
  canonical_paths:
    - mockups/<file>.png         # only for local-paths and local-archive
  archive:                       # only when format includes local-archive OR a Claude-Design bundle dir
    local_zip_path: <abs path to the user-provided .zip>   # n/a when kind: directory
    kind: zip | directory
    extracted_dir: <abs path to _archive/extracted/ (zip) or the user dir (directory)>
    truncated: false             # true when candidate list capped at 200
    bundle_kind: zip | directory | claude-design | generic   # set by step b2 A0
    canvas_ref: <chosen feature canvas *.html>               # claude-design only
    canvas_candidates:                                       # claude-design only
      - file: <*.html basename>
        title: <HTML <title>>
        slot_count: <int>        # count of id="s<N>" slots
        score: <0-100>
        anchor_confidence: high | user-resolved | fallback-keyword
    discarded:                   # claude-design only — out-of-feature, NOT copied
      - path: <rel path under root>
        reason: other-feature-canvas | support-asset | upload | unrelated
    candidates:                  # full enumerated list (post-filter) — keyword-fallback path
      - path: <rel path under extracted/>
        filename: <basename>
        ext: html | png | jpg | svg | …
        size_kb: <int>
        html_title: <optional, HTML only>
        html_h1: <optional, HTML only>
        score: <0-100>
        matched_keywords: [<keyword>, …]
        selected: <bool>         # true = part of the confirmed subset
  canvas_render:                 # only when step b2 A5 rendered canvas-only artboards
    served_port: <int>
    rendered_slots:
      - slot_id: s31
        png: mockups/<slug>__3-1.png
    render_status: ok | partial | failed | skipped
mockup_analysis:
  screens:
    - name: <inferred name, e.g. "Lista ordini">
      slot_id: <s31, claude-design only — else omit>
      screen_id: <"3.1" dotted, claude-design only — joins handoff prompt § 3 — else omit>
      mockup_ref: <canonical_path or chat://image-N>
      source: manifest | title | rendered | prerender   # claude-design only — provenance of the label/image
      prerender_png: <mockups/<file>.png — only when a designer pre-render matched this slot>
      purpose: <one line — what this screen lets the user do>
      components:
        - <component name from ${paths.design_system}/INDEX.md if matched, else free-form>
      affordances:   # interactive + iconographic artifacts visible in the mockup
        - label: <e.g. "Export CSV button", "bell icon", "Filters tab">
          traces_to: <US-id / AC-id / discovery dimension this artifact serves — or "" if none yet>
          classification: backed | decorative | orphan
      states_visible: [empty, loading, error, success, default]
      copy_excerpts:
        - <verbatim text visible in the mockup, max ~10 strings per screen>
      gaps:
        - <ambiguity to clarify in Discovery, e.g. "filtro per data: range o singola?">
  user_flow:
    - <step 1, e.g. "Utente apre Lista ordini → tap su riga → Dettaglio ordine">
  design_system_alignment:
    status: aligned | violations | not-applicable
    violations:
      - mockup: <screen name>
        kind: hardcoded-color | hardcoded-spacing | hardcoded-shadow | hardcoded-radius | custom-component
        detail: <e.g. "background #FF6A00 — token equivalent: --color-brand-primary">
        fix_in_step_3: <token name or component to swap in>
```

**When NOT to fill a field:**
- Omit `design_system_alignment` entirely (do NOT write `not-applicable`) when
  `features.has_design_system: false` — the field becomes irrelevant.
- Omit `copy_excerpts` for screens that have no readable text (icon-only).
- Omit `states_visible` for screens that show only the default state — do not
  invent states the mockup doesn't show.

**Gap discipline:** every entry in `gaps[]` MUST become either a Discovery question
or an explicit non-question (recorded in Discovery Log as "skipped — outside
mockup scope"). Gaps that remain unresolved by the end of Discovery block PRD
writing in Step 4.

**Affordance discipline (Functional Traceability):** classify every interactive
or iconographic artifact the mockup shows. The mockup is a fidelity target, not
a requirements oracle — handed-off designs (Claude Design / Figma) routinely add
buttons, icons, and menu entries no requirement asked for, and implementing them
1:1 ships dead code. So:
- `backed` — the artifact serves a user story / AC already in scope. Record
  `traces_to`.
- `decorative` — legitimately present but carries no behaviour (section icon,
  brand mark). It will render static/`aria-hidden` at implementation, never as a
  live control.
- `orphan` — interactive-looking but traces to nothing in scope. Each orphan is
  a **BLOCKING Discovery item**: ask the user (one per turn, like a gap) whether
  it should (a) gain a real function → routes through the Scope Expansion Check /
  `/prd-add`, (b) be downgraded to decorative, or (c) be dropped from the design.
  Never carry an unresolved `orphan` past Discovery — it would silently become
  dead UI. Record the resolution in the Discovery Log.

This classification is the seed of the UI Element Inventory `function_ref`
column (Step 3d) — the allowlist `ui-expert` and `code-reviewer` enforce at
implementation/review via the Functional Traceability Gate
(`framework/agents/design-system-protocol.md`).
