### Phase 1: Analysis (Fable)

> **TLDR**  -  Fable-driven codebase exploration (Opus when the fallback ladder engages). Detects if the issue is already fixed (git blame, closed PRs), then launches parallel Explore sub-agents to map the affected code paths. Outputs: impact analysis, stack detection (auto-selects platform guide), relevant files, risk areas. Feeds Phase 2 planning.

<!-- progress-contract: applied -->
Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md`  -  lines for each Explore dispatch, each finish, analyst synthesis start, `analysis.json` write.

#### Step 0  -  Prior Fix Detection

Before any analysis, check if this issue was already fixed by someone else. Three signals (git commit grep on issue ID over 8 weeks, recent file-path history over 4 weeks, Jira/issue comments). On hit → prompt: Verify (cherry-pick path) / Stop (cleanup + `stopped_already_fixed` state) / Continue. Miss → Step 1. Full check commands, prompt block, and cleanup commands: `$HOME/.claude/multi-agent-refs/features/prior-fix-detection.md`.

#### Step 1  -  Knowledge Injection (cached context)

Before launching Explore agents, check if project knowledge exists:

```
$HOME/.claude/knowledge/{project-name}/
  architecture.md     -  file structure, module map, dependency graph
  patterns.md         -  patterns in use, conventions, idioms
  gotchas.md          -  encountered issues, edge cases and solutions
  decisions.md        -  architectural decisions and rationale (ADR-lite)
```

Also read project-level CLAUDE.md if exists:

- `$PROJECT_ROOT/CLAUDE.md`
- `$PROJECT_ROOT/.claude/CLAUDE.md`

**Per-repo memory injection (opt-in via `prefs.global.perRepoMemory`):**

```bash
bash $HOME/.claude/scripts/memory-load.sh "$PROJECT_ROOT" "$TASK_TITLE $TASK_DESCRIPTION"
```

Exit 0 with empty output = pref off or no memory on disk  -  skip. Otherwise the script emits a `<repo-memory path="...">...</repo-memory>` block of MEMORY.md pointers suitable for direct injection into the analysis prompt. Passing the task text ranks the pointers against it instead of printing the first thirty; individual memory files are read on-demand when a pointer looks relevant.

**Durable knowledge (on by default via `prefs.global.learningsLedger.enabled`):** two blocks, and where each goes is part of the contract  -  see `$HOME/.claude/multi-agent-refs/prompt-assembly.md`.

```bash
# HEAD of the prompt: task-independent, byte-stable, so it caches.
node $HOME/.claude/scripts/learnings-ledger.mjs profile 2>/dev/null
# END of the prompt, after the task text: ranked against this task.
node $HOME/.claude/scripts/learnings-ledger.mjs brief --max "${prefs_learningsLedger_maxBriefEntries:-20}" \
  --task "$TASK_TITLE $TASK_DESCRIPTION" 2>/dev/null
```

Exit 2 (empty ledger) = skip silently. Lines end with an `L:<id>` pointer; `show --id L:<id>` returns the full entry. Skip both when `injectIntoAnalysis = false`. Context, not commands  -  current scope decides. Then log the injection so recall quality stays measurable:

```bash
bash $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 1 memory.injected \
  kind=<profile|task-relevant> rows=$N chars=$C
```

**If knowledge files exist and are fresh** (modified within last 90 days  -  see knowledge.md staleness rules):

1. Read relevant knowledge files based on task description
2. Use knowledge to **narrow Explore scope**  -  instead of "very thorough" full scan, do targeted exploration of only unknown/changed areas
3. Log: "Phase 1: Knowledge injected ({N} files), targeted explore"

**If no knowledge exists** (first time for this project):

1. Launch full Explore agents as before
2. Log: "Phase 1: Full explore (no cached knowledge)"

#### Step 1.4 - Figma evidence capture (when task carries a Figma reference)

When `state.contextLinks[]` or the task description contains a Figma reference, Phase 1 MUST collect the canonical evidence record. **Phase 0 Step 0.5 already resolved the tier** and the credential - read `state.figmaAccess.tier` and fetch with that tier's tool set; do not re-probe. Chain, tiers and halt conditions: `$HOME/.claude/multi-agent-refs/rules.md` "Figma Access Tier".

What Phase 1 owns is the record. Every frame gets one entry in `state.evidence.figma[]`:

| Field | Tier 1 (MCP) | Tier 2 (REST) | Tier 3 (screenshot) |
|---|---|---|---|
| `nodeId`, `screenshotUrl`, `tokens[]`, `textLayers[]` | required | required | required |
| `codeConnectSnippets[]` | from `CodeConnectSnippet` blocks | from repo `*.figma.swift` / `*.figma.kt` keyed on `fileKey`+`nodeId`; empty -> Open Question | always `[]` -> forced Open Question |
| `tier` | `1` | `2` | `3` |

Halt if all three tiers fail; never substitute primitives or invent layout from prose.

**Spacing goes in by token NAME, per atom  -  never a pixel number.** `tokens[]` must
carry each frame's spacing/padding as Figma names them (`Spacing/12`, edge `4`), keyed
to the atom. Phase 3 cannot call Figma, so what is missed here is gone: one run guessed
`16` where the frame said `Spacing/12` and the sheet was rebuilt. A pixel number also
cannot map back to a token. No spacing entries on a UI frame is a **capture failure**,
not an empty frame  -  Open Question and halt. Canonical chain reference: `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain".

**Telemetry (required for the no-MCP gate):** Tier 1 uses `mcp__claude_ai_Figma__*` tools. Every such MCP invocation MUST append an entry to `state.telemetry.mcpCalls[]` as `{ "tool": "<full mcp tool name>", "phase": 1, "timestamp": "<ISO-8601>" }`. This is the only phase permitted to record `phase: 1` (or `0`) entries; `smoke-no-mcp-in-dev-phases.sh` fails the run if any entry carries `phase >= 2`. Recording is what makes that BLOCKING contract enforceable  -  an MCP call left unrecorded defeats the gate, so record every one.

Progress lines:

```
→ figma evidence: <N> frames captured (tier=<n>, code-connect=<M>, open-questions=<K>)
```

#### Step 1.45  -  Reuse discovery (BLOCKING for new services, entities, mappers)

Before proposing any new service call, entity or mapper, search for what already
covers it. Record hits under `state.reuse[]` and cite them in the doc; proposing new
code over a hit needs a one-line reason.

Search for: a **wrapper** over the same endpoint (especially one supplying parameters
the generated call leaves optional); an **entity** for the same concept (module's
shared entities first, then siblings); a **mapper** over the same response; a **screen**
doing the same interaction.

Why blocking: one run proposed a new repository over an endpoint a sibling already
wrapped **with its country parameter**, called the generated method without it, and
re-invented an entity the module had. Half that branch's commits went to converging
back. "Copy X and rename it" is the reuse answer, not a hint  -  name X's files.

#### Step 1.5  -  External Context Injection (`state.contextLinks[]`)

Phase 0 Step 1b catalogued every typed external link from the task description into `state.contextLinks[]`. Phase 1 dispatches each entry to its matching fetcher (crashlytics, fortify, graylog, swagger, confluence, figma, generic-doc) and prepends results under a **Referenced External Sources** section in the analysis prompt  -  so the agent doesn't re-discover what the ticket already pointed at. `state.graylogContext` is injected there too, as diagnostic context (advisory only). Failures never fatal (a non-zero fetcher exit is marked skipped and the analysis still runs, exactly as for crashlytics); pending refs are advisories. Full dispatch table, exit-code handling, prompt injection shape, log line shape: `$HOME/.claude/multi-agent-refs/features/external-context-injection.md`.

**Log line shape** (progress contract):

```
→ context injection: total=<N>, fetched=<n>, pending=<n>, by-type={swagger:F/P, confluence:F/P, ...}
```

#### Step 2  -  Stack Detection

Detect the project's tech stack to load appropriate skills and tooling throughout the pipeline. Auto-detect by scanning `$PROJECT_ROOT` (maxdepth 2) for project markers:

| Stack          | Marker Files                                    | Skills                                                       |
| -------------- | ----------------------------------------------- | ------------------------------------------------------------ |
| iOS/Swift      | `.xcodeproj`, `Package.swift`                   | `ai-ios-toolkit:*` skills, `ai-ios-toolkit:swift-testing`    |
| Android/Kotlin | `build.gradle`, `build.gradle.kts`              | `ai-android-toolkit:android-jetpack-compose-expert`, `ai-android-toolkit:kotlin-coroutines-expert` |
| Python         | `requirements.txt`, `pyproject.toml`, `Pipfile` | `ai-backend-toolkit:fastapi-pro`, `ai-backend-toolkit:api-patterns` |
| Node.js        | `package.json`                                  | `ai-backend-toolkit:nodejs-backend-patterns`, `ai-backend-toolkit:api-patterns` |
| Go             | `go.mod`                                        | `ai-backend-toolkit:api-patterns`, `ai-backend-toolkit:clean-code` |
| Docker         | `Dockerfile`, `docker-compose.yml`              | `ai-backend-toolkit:docker-expert`                           |
| Monorepo       | Multiple of above                               | `ai-backend-toolkit:monorepo-architect`                      |

Store in `agent-state.json` → `"detectedStack": ["ios", "python", "docker"]`

This informs:

- Phase 3: which build/test commands to use
- Phase 4: which deterministic gates and reviewer skills to load
- Phase 6: which PR template fits best

#### Step 2.5  -  Repo Map Injection (advisory, opt-in)

Gated by `prefs.global.repoMap.enabled` (default: `false`). When enabled, runs `$HOME/.claude/scripts/repo-map.mjs` and injects the budgeted result into each Explore prompt as `${REPO_MAP}`. Aider-style: deterministic, no embeddings, sub-second, advisory only. Full wiring (helper invocation, properties, when-to-enable): `$HOME/.claude/multi-agent-refs/features/repo-map.md`.

#### Step 3  -  Codebase Exploration

Launch parallel Explore agents to scan codebase:

- Related files to the task
- Existing patterns and conventions
- Potential impact areas

Use `subagent_type: "Explore"` with thoroughness scaled to task size AND knowledge availability (first match wins):

- `taskType` is `bugfix`/`chore` AND scope is small (single named file, or a referenced crash/stack frame that pinpoints the site) → "light" (cheapest  -  scan only the named area + its direct callers)
- Knowledge exists → "medium" (targeted, cheaper)
- No knowledge, or `taskType` is `feature`/`refactor`/`component` → "very thorough" (full scan, first-time investment)

The light tier keeps a one-line bug fix from triggering a full-repo scan; pairing it with the deterministic `taskType` (Phase 0 Step 7) prevents the cheap path from firing on feature work.

**Dispatch resilience (required).** Explore agents run in parallel and the analyst synthesis waits on them, so a single stalled agent hangs the phase. Bound each Explore dispatch by a wall-clock budget (`EXPLORE_TIMEOUT_SECONDS`, default 180). If an agent has not returned by the budget: log `explore.timeout agent=<id>`, drop that agent's slice, and synthesize from the agents that did return. Proceed as long as at least one Explore agent returned; if zero returned, retry the cheapest single Explore once, then HALT with `ERR: no Explore agent returned within ${EXPLORE_TIMEOUT_SECONDS}s; resume with /multi-agent:resume #N.`. Never block indefinitely on a slow or dead dispatch.

#### Step 4  -  Analysis document (the design contract Phase 2 and Phase 3 demand)

Phase 2 and Phase 3 pre-flights BLOCK on `analysis/<feature-slug>-<platform>.md`. **This step produces it.**

**When it runs.** From signals Phase 0 already computed:

| `taskType` | Figma reference in `state.contextLinks[]` | Document |
|---|---|---|
| `feature` · `refactor` · `component` | any | **produced** |
| `bugfix` · `chore` | present | **produced** |
| `bugfix` · `chore` | absent | **skipped** - record `state.analysis.docStatus = "not-applicable"` |

`analysisPhase.forceFull` (default `false`) overrides the skip row, for a small change that must still leave a spec behind. `analysisPhase.mode` sets depth: `auto` is Lite on the skip-row shape and Full otherwise; `full` / `lite` pin it.

A fresh document is also skipped when one already exists for this feature and platform AND its front-matter `evidence_digest` still matches (Locked 27 cache); record `docStatus = "reused"`.

**How it runs.** Load `$HOME/.claude/multi-agent-refs/analysis/` on demand, in order: `locked.md` (the 31 binding decisions) then `evidence.md`, `synthesis.md`, `render.md`. Intake is NOT re-asked; platform, repos and account come from Phase 0 state. Autopilot auto-approves the Phase 2a convention preview and writes the local file, because it may not ask.

**Where it lands.** `<worktree>/analysis/<feature-slug>-<platform>.md`, one file per platform in `state.analysisSpec.platforms[]`. Persist the paths to `state.analysis.docPath[]` and set `docStatus` to `produced` | `reused` | `not-applicable`. Also persist `state.run.lastAnalysisDigest` (this document's `evidence_digest`) and `state.run.analysisBaseCommit` (`git rev-parse HEAD`). Phase 3's freshness check reads both; unwritten, it has nothing to compare and passes silently. Whether the file is committed with the work is `prefs.global.analysisPhase.commitDoc` (default `true`), read at Phase 6.

**The gate is not optional.** `node $HOME/.claude/scripts/validate-analysis-doc.mjs <file>` must exit 0 for every produced file; non-zero fails CLOSED like the JSON validator below (rework once, then halt).

Progress line: `→ analysis doc: <produced|reused|not-applicable> (<N> platform, validator <pass|pass-after-rework>)`

#### Output contract

Two artefacts, both read downstream: `state.analysis` (the object below, for Phase 2 decomposition) and the Step 4 document (Phase 2 pre-flight + Phase 3's sole design source, Locked 30).

Phase 1 produces an object conforming to `$HOME/.claude/schemas/analysis-output.schema.json` and persists it to `state.analysis`. Required fields (exact names per the schema): `stack` (detected stack identifier + primary language), `touchedAreas[]` (path + why), `risks[]` (existing-code hazards/observations the planner must respect  -  each `{risk, severity, mitigation}`; use an empty array when none), `summary` (one-paragraph human-readable). Phase 2 reads this object as its sole input  -  see `phase-2-planning.md`'s Input contract.

**Required: validator gate (deterministic)  -  run on the persisted file immediately after the analysis object is produced; the validator's exit code decides, not the LLM turn:**

```bash
ANALYSIS_FILE="$WORKTREE/.pipeline/analysis.json"
mkdir -p "$(dirname "$ANALYSIS_FILE")"
printf '%s' "$ANALYSIS_JSON" > "$ANALYSIS_FILE"
node $HOME/.claude/scripts/validate-analysis.mjs "$ANALYSIS_FILE"
```

Progress line: `    → checking validator validate-analysis`

Non-zero exit fails CLOSED: emit the validator stderr + `errors[]` verbatim, attempt ONE self-correction rework (re-invoke the explorer with the errors quoted, overwrite `$ANALYSIS_FILE`), re-run the validator. If it fails again -> HALT the phase with recovery hint: `ERR: analysis output failed validate-analysis.mjs twice. Inspect $ANALYSIS_FILE against $HOME/.claude/schemas/analysis-output.schema.json, then resume with /multi-agent:resume #N.` Record `agent-state.phases["1"].validator` (`pass` | `pass-after-rework` | `halted`).

Log: "Phase 1: Analysis  -  stack:{detectedStack} | {N} files identified, {summary}"

#### Telemetry  -  token forwarding

Forward the explorer call's token totals into the tracker so Phase 7's Cost Breakdown captures Phase 1:

```bash
LOG_METRIC_FORWARD_TO_TRACKER=1 $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 1 analysis.completed \
  model=opus tokens_in=$IN tokens_out=$OUT duration_ms=$DUR
```

Best-effort. See `$HOME/.claude/multi-agent-refs/progress-contract.md#token-telemetry-forwarding` for the canonical contract.

#### Prior-Art Enrichment (advisory)

After the explorer returns its summary, consult the per-repo triage corpus for similar past tasks. Inject up to 3 matches into the analysis output as `priorArt[]` so Phase 2 planning can read them. Disabled when `prefs.global.priorArtEnrichment.enabled = false`.

```bash
PRIOR=$(node $HOME/.claude/scripts/triage-memory.mjs query \
  --issue "$TASK_TITLE $TASK_DESCRIPTION" --top 3 2>/dev/null | jq -c '.hits // []')
```

Hits are relevance-ranked, and a query matching nothing returns nothing. Each hit carries an `id`: `triage-memory.mjs show --id <id>` returns the full row.

Treat hits as **context only**  -  they are past Phase 4 verdicts, not prescriptions. Useful when the new task touches the same files or symbols as a previous run.

---

## Token telemetry  -  invoke after every LLM call

```bash
bash $HOME/.claude/scripts/phase-tracker.sh tokens 1 <input_count> <output_count>
```

Contract and rationale: `progress-contract.md` -> Token telemetry forwarding.

