### Phase 0: Init

> **TLDR**  -  8 sequential steps: load prefs → parse input (Jira/GitHub/free-text) → select project → detect remote + pick base branch → optional design input → branch name → git identity → instruction files → create worktree/local branch + agent-state.json. Every input type (Jira URL, GitHub issue, free-text) flows through the same 8 steps.

#### Step −1  -  Bootstrap the cross-CLI tracker (FIRST thing in every run)

Before anything else  -  initialize the visual tracker so the user sees the pipeline's shape immediately. This is the **only** progress signal Copilot CLI users get; Claude users also get native TaskCreate tiles.

```bash
TASK_ID="${INPUT_TASK_ID:-pipeline-$(date +%Y%m%d-%H%M%S)}"
$HOME/.claude/scripts/phase-tracker.sh init "$TASK_ID"
for p in 0:Init 1:Analysis 2:Planning 3:Dev 4:Review 5:Test 6:Commit 7:Report; do
  $HOME/.claude/scripts/phase-tracker.sh add "${p%%:*}" "${p#*:}"
done
$HOME/.claude/scripts/phase-tracker.sh update 0 in_progress
```

If `INPUT_TASK_ID` isn't known yet (free-text, project not selected), use a placeholder; rename later via `mv` once parsed in Step 1.

Every subsequent phase (1-7) MUST call `phase-tracker.sh update <N> in_progress` on entry and `phase-tracker.sh update <N> completed|failed|skipped` on exit. Sub-phase milestones use `phase-tracker.sh sub <N> <subN> "<name>" <status>`. See `$HOME/.claude/multi-agent-refs/phases.md` "Visual Phase Tracker" for the full contract.

##### TaskCreate ordering on Claude Code (strict)

On Claude Code, fire all `TaskCreate` calls in strict phase-number order (0 → 7) BEFORE any `TaskUpdate`. Full contract: `$HOME/.claude/multi-agent-refs/tracker-contract.md` section "TaskCreate ordering (strict)".

---

#### Step 0  -  Load Preferences

Read preferences: `PREFS_FILE="$HOME/.claude/multi-agent-preferences.json"` (if missing, start with `{"projects": {}, "global": {}}`).

**Apply language preference IMMEDIATELY** (before any status output):

```bash
OUTPUT_LANG=$(jq -r '.global.outputLanguage // "en"' "$PREFS_FILE" 2>/dev/null || echo en)
```

From this point on, everything the user reads renders in `$OUTPUT_LANG`: conversational lines, `AskUserQuestion` `question`/`description`, and external payload bodies (PR/Jira/Confluence). English stays only on `label`/`header`, commit messages, branch names, PR titles, identifiers. Full matrix: `rules.md` "Language Application".

**Model fallback date gate** (same step, once per run): read `prefs.global.modelFallback`. If `premiumTierUntil` is set and in the past, apply the date-gate trigger from `$HOME/.claude/multi-agent-refs/features/model-fallback.md` - `preferredModel` personas dispatch on `fallbackModel` for this run, with the one-line WARN. Dispatch-error and budget triggers in that contract apply per-dispatch later; nothing else to do here.

**First-run guard**: After loading prefs, check if `keychainMapping` has at least one non-null value. If ALL values are null (template defaults  -  setup never ran), show:
```
⚠ No keychain tokens mapped. Run /multi-agent setup first to discover
  and map your tokens. Without this, the pipeline cannot authenticate
  with Jira, Bitbucket, GitHub, or other services.

  → /multi-agent setup    (interactive, ~2 min)
```
Then STOP Phase 0  -  do not proceed with a broken token state. This prevents the user from hitting auth errors deep in Phase 1-6 and wondering why.

**Preferences schema**  -  see `prefs.schema.json` for full definition. Key paths: `projects[{name}]` (branches, lastIdentity, jiraProjectKeys, jiraTeams, jiraComponents, jiraVersions, confluenceUrls, remoteType, jiraTeamFieldId, taskCount, lastUsed) + `global` (identities, keychainMapping, defaultJiraKey, jiraCommentDefault, confluenceDefault, recentProjects, recentBranches, recentGroups, platformIdentityRouting, serviceStatus, settings, triageCrossCheck, promptLanguage).

**Key design**: Every field is an **array of all historical values** (most recent first) → "recently used" list.

**Token resolution rule**: ALWAYS check `prefs.global.keychainMapping.<service_id>` first for the key name  -  this holds the user's actual Keychain key name (which may differ from the standard convention). Fall back to standard key name ONLY when setup has never run (mapping absent). The standard fallback exists as a safety net, not as the primary path. Service IDs: `jira`, `bitbucket_token`, `bitbucket_user`, `github`, `confluence`, `figma`, `figma_mcp`, `fortify`, `firebase`, `jenkins`, `graylog`.

**Jira project key resolution** (first match wins):

1. `prefs.projects[{project}].jiraProjectKeys[0]`  -  project-specific
2. `prefs.global.defaultJiraKey`  -  global default
3. Ask user → save to `prefs.global.defaultJiraKey`

Used for: input parsing, branch naming, commit messages.

**UX pattern**: Show `Recent: → {value}` suggestion from history, numbered alternatives, enter to accept. No history → skip suggestion line.

**Save rule**: After EVERY selection, append chosen value to relevant array (dedup, most recent first, max 10). Save prefs after Phase 0 and Phase 7.

**v2.1.0+ Recents update map** (which selection writes to which prefs path):

| Selection event | Prefs path | Shape | Cap |
|---|---|---|---|
| Project picked (Step 2) | `global.recentProjects` | `[{path, label, count, lastUsed}]` | 20 |
| Multi-repo group picked or saved (Step 2) | `global.recentGroups` | `[{label?, repos[], count, lastUsed}]` | 10 |
| Branch picked (Step 3) | `global.recentBranches[{projectKey}]` | `[{branch, lastUsed, count?}]` (TTL `settings.branchTtlDays`, default 15d) | 10 (TTL also prunes) |
| Service ping (any external API call) | `global.serviceStatus[{service}]` | `{ok, checkedAt, reason?}` (TTL `settings.serviceStatusCacheSeconds`, default 300s) | n/a |
| Git identity routed (Step 6a) | `projects[{name}].lastIdentity` | int (index into `global.identities`) | n/a |

All updates are O(1)  -  read-modify-write on the in-memory `prefs` object; single atomic write at the end of Phase 0 (and again at Phase 7).

#### Step 0.5 - Figma access pre-flight (BLOCKING when task carries a Figma reference)

When Step 1 input parsing surfaces a Figma reference (URL, node ID, or "from the design" free-text alongside a UI file target), Phase 0 MUST establish the Figma access tier before any other phase runs. Persist `state.figmaAccess.tier` (1, 2, or 3) into the run state so every downstream phase reads the same value.

Probe order:

1. **Tier 1 (Figma MCP)**: check the host serves `mcp__claude_ai_Figma__*` before probing. Absent → set `state.figmaAccess.tier1Unavailable = "host"` and fall through to Tier 2 with no probe, no re-auth retry, no MCP-token question. Present → probe `get_metadata(fileKey, nodeId)` on the first frame; on auth failure run `authenticate` + `complete_authentication` and retry once, and only a *second* failure raises the recreate-or-continue question. Success → `state.figmaAccess.tier = 1`.
2. **Tier 2 (Figma REST)**: when Tier 1 fails, resolve the PAT via `~/.claude/lib/credential-store.sh get <logical-key>` where `<logical-key>` = `prefs.global.keychainMapping.figma`. Probe `GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}` with header `X-Figma-Token: $TOKEN`. HTTP 200 → `state.figmaAccess.tier = 2`. Token missing / 401 / 403 → fall through.
3. **Tier 3 (User-attached screenshot)**: when Tiers 1 + 2 both fail, scan the task payload for inline screenshots or attachments. Present → `state.figmaAccess.tier = 3` and `state.figmaAccess.reviewBlocking = true` (Phase 4 enforces this).
4. **Halt**: all three tiers fail → emit a single AskUserQuestion asking the user how to proceed (provide PAT, paste a screenshot, abort). Never proceed with text-derived guesses.

Record the cause, not just the downshift. `tier1Unavailable = "host"` means the tier never existed here - routine on Copilot and Codex, where the installer registers only `multi-agent-toolkit`. `"auth"` means it existed and the credential failed, which on Claude Code points at a dead `figma_mcp` token worth surfacing in Phase 7. Conflating them costs two wasted MCP round trips and a question the user cannot act on. On those two hosts a mapped `figma` PAT is the primary path, not a fallback.

Log the resolved tier in the agent log:

```
→ figma access tier: <1|2|3>
```

Full chain definition, REST endpoints, URL parsing, canonical-component contract: `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain (BLOCKING, pipeline-wide)". Do not duplicate it here.

#### Step 0.6 - Update check (advisory by default, blocking on a required release)

Run `bash $HOME/.claude/scripts/update-check.sh` (cached per `updateCheck.ttlHours`, default 24h; 3s-bounded; every failure path silent; exit code always 0). Read stdout:

| stdout | Meaning | Branch |
|---|---|---|
| empty | current, ahead of the registry, or the check could not run | continue |
| `<local>\|<latest>` | a newer release exists | advisory |
| `<local>\|<latest>\|force` | installed version is below `dist-tags.required` | **required  -  the run halts** |

**Required branch (blocking).** Confirm with `bash $HOME/.claude/scripts/require-supported-version.sh` (exit 3 = halt; stdout `force|<local>|<latest>|<required>`, stderr the human block), then:

- Log `→ update required: v<local> < required v<required> (halting)`.
- Interactive and autopilot are identical  -  there is nothing to decide. Run the `/multi-agent:update` flow, then **stop the run** and ask the user, in `outputLanguage`, to re-issue the command. Do NOT continue into Phase 1: this run loaded its docs and scripts from the old version, which is the drift the floor exists to prevent. An update flow that itself fails halts too, never falls through.
- `MULTI_AGENT_ALLOW_OUTDATED=1` is the only override (`updateCheck.enabled: false` silences the advisory nag, not this); when set, log `→ supported-version gate overridden` and continue. Exemptions, fail-open rules and how a floor is published: `$HOME/.claude/multi-agent-refs/rules.md` "Supported Version Gate".

**Advisory branch (never blocks).**

- **Interactive**: `updateCheck.autoUpdate` defaults to `true`, so do not ask  -  run the `/multi-agent:update` flow, log `→ updated to v<latest>`, continue (docs already loaded finish this run on the old version; full effect next run). Only `autoUpdate: false` makes it a question: log `→ update available: v<local> -> v<latest>`, ask ONE AskUserQuestion  -  **Update now** (recommended) / **Continue without updating**; on *Continue*, no re-ask until the TTL expires.
- **Autopilot**: never ask (zero-interaction contract). On the default it updates first, logging `→ updated to v<latest>`; on `autoUpdate: false` it is log-only and continues.

Both branches must run BEFORE Step 6 (worktree creation) so an accepted or forced update cannot mutate `~/.claude` under a mid-phase run.

#### Step 0.7 - Token expiry pre-flight (runs before Step 1 fetch)

Validate the tokens THIS run will need before any of them is used mid-phase. Scope is derived, not exhaustive:

| Run signal | Tokens to probe | Cheap probe (1 call each) |
|---|---|---|
| Input is Jira-id / Jira-url | `jira` | `GET /rest/api/2/myself` |
| Input is GitHub issue / repo#N | `github` | `gh auth status` (or `GET /user`) |
| Remote is Bitbucket | `bitbucket_token` | `GET /rest/api/1.0/application-properties` |
| `reportChannels.confluence` true | `confluence` | `GET /rest/api/user/current` |
| Task carries a Figma reference | `figma_mcp` (Tier 1), `figma` (Tier 2) | MCP `initialize` POST to `mcp.figma.com/mcp`; `GET api.figma.com/v1/me` |

Results are cached in `global.serviceStatus` (existing TTL contract, default 300s)  -  a probe that ran in the last 5 minutes is not repeated.

**Valid** → continue silently (one log line: `→ token pre-flight: N ok`).

**Figma MCP expired / rejected  -  CRITICAL path.** The `figma_mcp` OAuth token (`figu_`) expires on a schedule (90 days), unlike the PATs. Because a dead MCP token silently degrades every Figma consumer to Tier 2/3 and has cost rebuild rounds, an expired `figma_mcp` is never deferred to mid-run:

1. **Silent renewal first**: run `~/.claude/lib/figma-mcp-refresh.sh` (refresh grant via `<key>_Refresh` + the `.figma-oauth.json` client credentials next to `prefs.global.tokenScripts.figma_mcp`). Exit 0 → re-probe, log `→ figma mcp token renewed silently`, continue. No question asked.
2. **Renewal impossible/rejected** (exit 1/2) → AskUserQuestion at init (`question`/`description` in `outputLanguage`): "Figma MCP token expired  -  update it now?"
   - **Regenerate now (script)**  -  shown only when `prefs.global.tokenScripts.figma_mcp` is set: run that script (browser OAuth flow), then re-probe and continue.
   - **Save a new token**  -  Token Save Flow from `setup.md` (clipboard path).
   - **Continue degraded**  -  proceed on Tier 2 (REST PAT) for this run; log the downgrade.
3. **Autopilot**: silent renewal only; if it fails, log `→ figma mcp token expired (autopilot: continuing on tier 2)` and continue degraded  -  never prompt.

**Other tokens expired / rejected** → run the Expired-token decision from `$HOME/.claude/multi-agent-refs/keychain.md` Rule 1 HERE at init (Regenerate / Use a different token / Skip and continue) instead of waiting for the first mid-run 401. A token that is structurally required for the input (e.g. the Jira PAT for a Jira-ID input) halts on skip, since Step 1 cannot fetch without it. Autopilot: skip-and-degrade semantics per Rule 1, halt only on structurally required tokens.

Probe failures caused by network (timeout, DNS) are NOT treated as expiry  -  log and continue; the mid-run 401 path still exists as the safety net.

**Record the inventory.** After the probes, persist what is reachable:

```bash
# --probe when the task references an external source (Jira ID, Crashlytics/Fortify URL,
# a remote to fetch): "the key is in the Keychain" and "the service answers" are
# different claims, and only the second one lets the run proceed.
bash "$HOME/.claude/lib/credential-inventory.sh" --json --probe > /tmp/cred-inventory-${TASK_ID}.json
```

Write `{usable, needsAttention, at}` into `agent-state.json.credentialInventory`. Later
phases read it instead of re-probing, and it is what makes `keychain.md` Rule 2
enforceable: any question that asks the user for data an external system holds must be
shaped by this file. A run that asks the user to paste a Crashlytics stack trace while
`firebase` is listed under `usable` is a Rule 2 violation, and the inventory is the
evidence.

#### Step 1  -  Parse Input

**Branch from input**: If the user provided a branch name after the issue reference (space-separated), store as `baseBranch` and skip Step 3. Otherwise Step 3 asks interactively.

Classify and fetch external data:

**GitHub Issue URL** (`https://github.com/org/repo/issues/N`):

1. Extract org, repo, issueNumber from URL
2. `gh issue view {N} -R {org}/{repo} --json title,body,labels,assignees`
3. Scan body for Jira ID (`{JIRA_KEY}-XXXXX`) → store as jiraId. Multiple → use FIRST. **No Jira ID** → apply the **GitHub-issue Jira auto-create** policy below; result is either a newly created Jira (`jiraId` stored + GitHub issue body updated with the link) or an explicit "no Jira" decision (branch uses `feature/GH{issueNo}-{kebab}`). See `$HOME/.claude/multi-agent-refs/issue-jira-triad.md` for the full policy, including autopilot behaviour and the `prefs.global.autoJiraFromGithubIssue` preference.
4. Scan body for Figma URL (`figma.com/design/...`) → store as figmaUrl
5. Auto-detect project: check `$HOME/{repo}` exists → skip Step 2

**GitHub Issue #** (`#316` or `316`):

1. Store issueNumber  -  repo determined after project selection (Step 2)
2. After selection: `git remote get-url origin` → extract org/repo → fetch as above

**Jira URL** (`https://{JIRA_HOST}/browse/{jiraId}`):

1. Extract ID, fetch via Jira API (resolve token via keychainMapping):
   ```bash
   JIRA_KEY=$(jq -r '.global.keychainMapping.jira // empty' "$PREFS_FILE")
   [ -z "$JIRA_KEY" ] && JIRA_KEY="${USER}_Jira_Access_Token"
   JIRA_TOKEN=$(~/.claude/lib/credential-store.sh get "$JIRA_KEY" 2>/dev/null)
   curl -s -H "Authorization: Bearer $JIRA_TOKEN" \
     "https://{JIRA_HOST}/rest/api/2/issue/{jiraId}?fields=summary,issuetype,status"
   ```
2. Extract: summary, issueType (Bug/Story/Task/Feature)

**Jira ID** (`PROJ-XXXXX`): Same as Jira URL but ID directly available.

**Free-text**:

0. **Intent guard (conceptual-vs-edit)**  -  gated by `prefs.global.intentGuard.enabled` (default `true`). Before any project selection, worktree, or Jira prompt, classify the input:
   ```bash
   INTENT=$(bash $HOME/.claude/lib/classify-intent.sh "$DESCRIPTION")
   ```
   - `question` -> the user asked something conceptual, not a task to implement. Do NOT create a branch/worktree/Jira. Surface a picker (picker-contract): **Answer here** (default) / **Treat as a task**. On "Answer here" (autopilot default for `question`), answer the question directly in chat and end the run cleanly  -  no dev chain, no commits. On "Treat as a task", fall through to step 1 below.
   - `ambiguous` or `task` -> proceed to step 1 (normal task flow). Ambiguous input is treated as a task; the guard never blocks an actionable request.

   This kills the most-cited daily annoyance (the agent starts editing when asked a question) without adding latency to real tasks  -  the check is a deterministic local classifier, no model call. Other input types (Jira id, issue URL/number, repo#N) are always explicit tasks and skip the guard.

1. Store as description. No external fetch.
2. After project selection (Step 2), ask with a native `AskUserQuestion` picker (never a typed y/n):
   - `question`: "Create a Jira issue for this task?" (rendered in `outputLanguage`)
   - `header`: "Jira" (English, <=12 chars)
   - `options`:
     - `{ label: "Create", description: "Open the interactive Jira issue creation flow" }`
     - `{ label: "Skip", description: "Continue without Jira; branch uses feature/{short-kebab} or bugfix/{short-kebab}" }`

**If Create → Interactive Jira Issue Creation** (resolve `JIRA_TOKEN` via keychainMapping):

Sequential prompts (standard UX pattern with Recent suggestion): Project Key → Issue Type → Summary → Target Version → Team → Component/s → Priority → Description. All selections saved to `prefs.projects[{project}]`. Create via `POST $JIRA_BASE/issue`; team uses custom field (discover via `/field` API, cache in `jiraTeamFieldId`).

**If Skip → continue without Jira.** Branch uses `feature/{short-kebab}` or `bugfix/{short-kebab}`.

**Token pre-check** (after parsing): Jira input → resolve key via `prefs.global.keychainMapping.jira`, verify token with a lightweight API call (e.g. `GET /myself`). GitHub input → verify `gh auth status`. On failure (missing key, 401, 403) → run the **Token Save Flow** from `setup.md` inline. This is the same clipboard-based flow used during setup  -  token never appears in terminal. If user skips and the token is critical for the input type (e.g. Jira token for Jira input), halt Phase 0.

**VPN connectivity check**: Test VPN-dependent services (Jira, Bitbucket, Confluence, Fortify, Graylog) with `curl --connect-timeout 3`. If unreachable, warn and offer to continue. Fallback: Jira → manual input, Bitbucket → `git push` only, Confluence → skip Phase 7, Fortify → skip scan, Graylog → skip log fetch (advisory only, never blocks). Cache in `agent-state.json` → `"vpnServices": {"jira": true, ...}`.

#### Step 1b  -  URL Enrichment (catalogue + targeted deep fetches)

**Runs only when Step 1 found at least one URL in the task input** (Jira/Confluence/Figma/Swagger/Crashlytics/Fortify/Graylog links). Otherwise skip straight to Step 2.

When it runs, load `$HOME/.claude/multi-agent-refs/features/url-enrichment.md` and follow it. It covers: 1b.0 extract + catalogue every link into `state.contextLinks[]` (always runs when this step runs), then the deep fetches that are each conditional on their link type being present  -  1b.1 Crashlytics -> `state.crashContext`, 1b.2 Fortify SSC -> `state.fortifyFinding`, 1b.3 Graylog, 1b.4 catalogue-only types (fetched at Phase 1). It also owns the two closing log lines and the Phase 1 / Phase 2 prepend contracts.

#### Step 2  -  Project Selection

Scan `$HOME` (maxdepth 2) for project markers (`.xcodeproj`, `Package.swift`, `build.gradle`, `package.json`, `requirements.txt`, `go.mod`, `Cargo.toml`, `pom.xml`), excluding `DerivedData`, `node_modules`, `.build`, `Pods`, `.worktrees`, `build`, `.gradle`.

**Skip if deterministic**: GitHub Issue URL → extract repo name → check `$HOME/{repo}` exists → auto-set.

**Otherwise**: Present discovered projects as numbered list (standard UX pattern). Stack tag (`[iOS]`, `[Android]`, etc.) auto-detected from project files. Set `PROJECT_ROOT` to selected directory  -  all subsequent commands use `git -C $PROJECT_ROOT`. Save to `prefs.global.recentProjects` (dedup, max 10). If bare `#N` was given, now fetch GitHub issue from project's remote.

**v2.1.0+ Multi-Repo Mode** (gated by `prefs.global.settings.multiRepoEnabled`):

- The picker accepts space-separated numbers (`1 3 4`) for multi-select.
- `prefs.global.recentGroups[]` (set in setup.md Step 6) surfaces at the top: pressing the group's number selects all its repos in one keystroke.
- Multi-select activates when ≥2 repos are chosen → populate `state.projects[]` array; the legacy scalar `state.project`/`projectRoot`/`worktreePath` fields mirror the **first** entry for backward-compat (single-repo phases that still read scalars don't break).
- After selection, if the chosen set matches an existing `recentGroups` entry, bump `count` + `lastUsed`. If it's a new combination of ≥2 repos, ask with a native `AskUserQuestion` picker (`question`: "Save this repo combo as a reusable multi-repo group?" in `outputLanguage`; `header`: "Save"; `options`: `{ label: "Save", ... }`, `{ label: "Skip", ... }`)  -  on **Save**, prepend to `recentGroups` (LRU cap 10).
- Single-repo selection (1 repo) → legacy single-repo path; `state.projects[]` is omitted, scalars are populated as before.

**Test policy (once per project).** Resolve `prefs.projects[{slug}].testPolicy` → `state.testPolicy`; missing → native picker "Should development write tests here?" (`header`: "Tests"): `tdd` (recommended) / `tests-after` / `none`; persist. Autopilot without a record: `tdd`, noted.

#### Step 3  -  Remote Detection + Branch Selection

1. **Check preferences first**: If `prefs.projects[{project}].remoteType` exists, use cached value.
2. Read remote: `git -C $PROJECT_ROOT remote get-url origin`
3. Detect type: `github.com` → github (`gh` CLI), `{BITBUCKET_HOST}` → bitbucket (API + `keychainMapping.bitbucket_token`), other → generic-git. Save to `prefs.projects[{project}].remoteType`.
4. **Skip if baseBranch already set** (from Step 1). Otherwise:
5. Fetch + list PR-targetable branches:
   ```bash
   git -C $PROJECT_ROOT fetch origin
   git -C $PROJECT_ROOT branch -r --sort=-committerdate \
     | grep -E '(develop|release|main|master)' \
     | grep -v -E '(feature/|bugfix/|fix/|hotfix/|chore/)'
   ```
6. Sort: `develop*` first, then `release/*`, then `main`/`master`. Surface through the
   **native picker** per `picker-contract.md` (`AskUserQuestion` on Claude Code,
   `ask_choice.sh` on Copilot CLI) - `question` + `description` in `outputLanguage`,
   `label` English, the recent branch first and marked `(Recommended)`. The ASCII sketch
   below is what the options carry, not a menu to print:

   ```
   header: "Base branch"
   options: origin/develop (Recommended, reused from last run) | origin/main | release/8.4.0 | Other
   ```
7. User picks → store as `baseBranch`, and append `{branch, lastUsed, count?}` to
   `prefs.global.recentBranches[{projectKey}]` (dedup by `branch`, cap 10) - what the TTL
   filter below reads. Key is `branch`, not `name`. Never the legacy
   `projects[{project}].branches`.

**MUST: this step is not skippable (BLOCKING).** The only legitimate skip is rule 4
above - `baseBranch` already supplied in the input. Everything else asks. A run once
took a Jira ID and implemented straight onto whatever the local checkout was pointing
at, with neither the project nor the branch picker ever shown; nothing failed, so
nothing surfaced it. `phase0-exit-gate.mjs` now refuses to close Phase 0 unless
`agent-state.json` carries `baseBranch` and `baseFetchStatus`, so a skipped picker is a
gate failure rather than a silent default.

This holds in every mode. A Short run skips the *LLM* phases (Analysis, Planning); it
does not skip Phase 0's pickers. Autopilot resolves them to their defaults without prompting,
which still writes the fields - it does not leave them unset.

**TTL filter for recent branches**:

- `prefs.global.recentBranches[{projectKey}][]` carries `{branch, lastUsed, count?}`. Filter to those whose `lastUsed` is within `settings.branchTtlDays` (default 15).
- Stale entries (>TTL) are pruned in-place during the read  -  keeps the picker uncluttered without a separate cleanup pass.
- The filtered "Recent" list precedes the fresh `git branch -r` list; cap at 5 visible recent entries.

**Fetch-fail handling** (replaces silent `git fetch origin` failure):

The legacy `git -C $PROJECT_ROOT fetch origin` step (line 110 above) MUST not silently fall back to a stale cached ref. On non-zero exit, surface the **native picker** (per `picker-contract.md`) with 4 options:

```
question:    "git fetch failed for {project} - the base ref may be stale. How should I proceed?"
description: "exit {code}, last successful fetch {ts}. Likely: VPN closed, host unreachable, auth expired."
header:      "Base ref"
options:
  Connect VPN and retry      (Recommended)  re-run the fetch, then continue with a fresh ref
  Use cached origin ref                     stale risk: base sha {sha}, fetched {since}
  Use local branch as base                  only offered when the local branch exists; commit {sha}
  Abort                                     no worktree, no branch, no state file
```

**Say which is which.** The question must name the corporate host when the remote points
at one - a `{BITBUCKET_HOST}` remote failing to resolve is almost always the VPN, and
telling the user that is the difference between a five-second fix and a run built on a
month-old ref. "Connect VPN and retry" re-runs the fetch and re-enters this picker if it
fails again; it is a real retry, not a label.

Persist user choice in `agent-state.json.baseFetchStatus` ∈ `"fresh" | "cached-stale" | "local-branch" | "aborted"`. On any non-fresh choice, log:
```
⚠️ Base ref stale (fetch fail @ {ts}, choice: {cached-stale|local-branch})
```
Phase 6 (commit/push) MUST re-attempt `git fetch origin` before push; if successful, prompt to rebase before pushing.

In multi-repo mode, the prompt fires per-repo. Choosing `[4] Abort` for any single repo aborts the entire task (atomic  -  no partial worktrees).

#### Step 4  -  Branch Naming (automatic)

Branch name is deterministic  -  no user confirmation needed.

**Type resolution** (first match wins):
- Jira `Bug|Hotfix|Defect` → `bugfix`
- Jira `Story|Task|Feature` → `feature`
- GitHub label `bug` → `bugfix`
- Free-text contains `bug`/`fix` → `bugfix`
- Else → `feature`

**Name construction**:
- Jira → `{type}/{jiraId}` (e.g. `bugfix/ABC-12345`)
- GitHub → `{type}/GH{issueNo}-{kebab}` (e.g. `feature/GH42-add-dark-mode`)
- Free-text → `{type}/{kebab}` (e.g. `bugfix/login-crash-fix`)

**Kebab rules** (for free-text + GitHub-issue titles):
1. Lowercase
2. Replace `[^a-z0-9]+` runs with single `-`
3. Trim leading/trailing `-`
4. Collapse adjacent `-` (no `--`)
5. Truncate to 50 chars, then trim trailing partial word at the last `-`
6. If empty after kebab (e.g. all-emoji title) → fall back to `task-{shortId}`

**Collision handling** (automatic  -  no prompt):
- Probe local + remote for existing branch. **Distinguish "no such ref" from "the
  probe failed"**: with `2>/dev/null` and an empty-output test they look identical,
  so an auth or network failure reads as "no collision" and the run creates a
  branch that already exists on the remote  -  surfacing as a rejected push at
  Phase 6, far from its cause.
  ```bash
  LOCAL_HIT=$(git -C "$root" rev-parse --verify --quiet "refs/heads/$branch")
  REMOTE_ERR=$(git -C "$root" ls-remote --exit-code --heads origin "$branch" 2>&1 >/dev/null)
  REMOTE_RC=$?
  # 0 = ref exists (collision) · 2 = no matching ref (authoritative "free")
  # anything else = the probe itself failed; REMOTE_ERR holds why
  ```
- `REMOTE_RC` is 0 or 2 → treat as authoritative
- `REMOTE_RC` is anything else → the remote answer is **unknown**, not "free". Log
  `Remote collision probe failed: <REMOTE_ERR>`, fall back to the local check only,
  and record `"remoteCollisionProbe": "failed"` in `agent-state.json` so Phase 6
  expects a possible non-fast-forward and re-checks before pushing.
- No collision → use as-is
- Collision found → append `-v2`, `-v3`, etc. until unique:
  `bugfix/ABC-12345` exists → `bugfix/ABC-12345-v2`
- Log: `Branch collision: {branch} exists, using {branch}-v2`

**Multi-Repo Mode**  -  branch name is **shared across all repos in the group**. Collision check runs per-repo; if any repo collides, the suffix applies to **all** repos (keeps cross-repo uniformity).

#### Step 5  -  Instruction Files (optional)

1. Check `$PROJECT_ROOT/.instructions/` exists → scan for `SKILL.md` files
2. If figmaUrl exists AND instructions include figma workflow → `instructionDriven: true`
3. Map instruction files to `"instructionFiles": { "start": "...", "validate": "...", "dev": "...", "commit": "..." }`
4. Instruction-driven → later phases read SKILL.md; no instructions → standard phases

#### Step 6  -  Branch + Workspace Setup

**6a. Resolve git identity** (automatic, no prompt):

```bash
ORIGIN=$(git -C "$root" config --get remote.origin.url)
CANON=$(echo "$ORIGIN" | sed -E 's|^(git@|https?://)||; s|:|/|; s|\.git$||')
```

Resolution order:
1. `platformIdentityRouting` match (longest-prefix wins) → use directly
2. `prefs.projects[{project}].lastIdentity` → use if exists
3. Single identity in `identities[]` → use it
4. No identities → run **Token Save Flow** from `setup.md` (creates identity with first token)
5. Multiple identities, no routing rule → ask once, save routing rule so it never asks again

In **multi-repo mode**, identity is resolved **per repo** independently.

Log: `Identity: {identity.name} <{identity.email}>`

**6b. Create branch + worktree**:

1. `git -C $PROJECT_ROOT fetch origin`

**If `--local` mode** (no worktree):

```bash
if [ -n "$(git -C $PROJECT_ROOT status --porcelain)" ]; then
  echo "Warning: Working directory has uncommitted changes. Stash or commit first."
fi
git -C $PROJECT_ROOT checkout -b {branch} origin/{baseBranch}
git -C $PROJECT_ROOT config user.name "{identity.name}"
git -C $PROJECT_ROOT config user.email "{identity.email}"
```

`worktreePath` = `$PROJECT_ROOT`, `localMode` = `true`.

**If normal mode** (worktree  -  default): 2. Worktree path: Jira → `.worktrees/{jiraId}/`, GitHub → `.worktrees/GH{issueNo}/`, free-text → `.worktrees/task-{shortId}/` 3. **Heal stale admin state first** (see "Worktree stale-lock heal" below) and **apply the residue guard** (see "Worktree residue guard" below), then `git -C $PROJECT_ROOT worktree add {path} -b {branch} origin/{baseBranch}` (if exists: enter, pull) 4. Set identity: `git -C {worktree-path} config user.name/email` 5. Create log dir + `agent-log.md` + `agent-state.json` at `$HOME/.claude/logs/multi-agent/{project}/{task-id}/`, never inside the worktree:

**Worktree stale-lock heal (required before every `worktree add`):** a run killed mid-`worktree add` (OOM, SIGTERM, disk full) leaves a locked or broken admin entry under `.git/worktrees/{id}/`, so the retry fails with `fatal: '<path>' already exists`. Always run the heal first  -  it is a no-op on a clean repo:

```bash
git -C "$proj" worktree prune 2>/dev/null || true            # drop entries whose dir is gone
if git -C "$proj" worktree list --porcelain | grep -qF "$WT_PATH"; then
  git -C "$proj" worktree unlock "$WT_PATH" 2>/dev/null || true   # clear a stale .locked marker
fi
```

If the path is still registered and healthy after the heal, enter + pull instead of re-adding (existing behavior). Only when `worktree add` still fails after the heal do the rollback / collision flow in the multi-repo block below.

**Worktree residue guard (required once per repo, idempotent):** every worktree dir holds a `.git` file, so a blanket `git add -A` in the parent tree records `.worktrees/{id}` as a gitlink that pollutes the branch. Keep `.worktrees/` out of the index via the clone-local exclude file (never committed, so no PR noise):

```bash
ex="$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
mkdir -p "$(dirname "$ex")"
grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex"
```

**Traversal-prune contract:** the exclude guard covers only the git INDEX, not
filesystem scans. Since each worktree is a full checkout, an unpruned tree walk
double-processes every file and can re-stage gitlinks. So `.worktrees` joins the
skip set (`node_modules`, `Pods`, `.build`, `DerivedData`, `.next`): every
`find`, walker, or `git add -A` MUST prune it. Already applied in the Step 2
scan, `shadow-git.sh` excludes, and the shared walkers (`extract-conventions.sh`,
`repo-cache.sh`, `repo-map.mjs`); add it to any new tree walk too.

**v2.1.0+ Multi-Repo Worktree Setup**:

When `state.projects[].length > 1`, repeat steps 2-4 **serially per repo** (worktrees are cheap; serial keeps git index sane and surfaces collisions one at a time):

```bash
for proj in "${PROJECTS[@]}"; do
  WT_PATH="$proj/.worktrees/$BRANCH_DIR/"
  git -C "$proj" worktree prune 2>/dev/null || true               # heal stale admin state
  git -C "$proj" worktree list --porcelain | grep -qF "$WT_PATH" \
    && git -C "$proj" worktree unlock "$WT_PATH" 2>/dev/null || true
  ex="$(git -C "$proj" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
  mkdir -p "$(dirname "$ex")"
  grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex"   # residue guard
  git -C "$proj" worktree add "$WT_PATH" -b "$BRANCH" "origin/$BASE_BRANCH"
  git -C "$WT_PATH" config user.name  "${proj_identity_name}"
  git -C "$WT_PATH" config user.email "${proj_identity_email}"
done
```

State file in multi-repo mode:
- Single shared `agent-state.json` lives at `$HOME/.claude/logs/multi-agent/{first-project}/{task-id}/agent-state.json` (anchored on the first repo for back-compat with `multi-agent log`/`status` commands)
- **One shared file + per-repo writers is exactly the race `write-state.mjs` exists for.** Every update to it (here and in every later phase) goes through `node $HOME/.claude/scripts/write-state.mjs` per the required mechanism in `operations.md` "Writing `agent-state.json`". A read-modify-write from two repos in the same loop drops one repo's `projects[]` entry.
- `state.projects[]` holds per-repo `{name, root, worktreePath, branch, baseBranch, identity, platform, baseFetchStatus, commit, pr, pushAttempts, buildStatus}`  -  see `agent-state.schema.json`
- Scalar fields (`project`, `projectRoot`, `worktreePath`, `branch`, `baseBranch`, `identity`) mirror `projects[0]` so legacy phases that read scalars keep working
- Atomicity: if any repo's worktree creation fails (collision aborted, fetch aborted, disk full), roll back already-created worktrees: `git -C $proj worktree remove --force $WT_PATH; git -C $proj branch -D $BRANCH`. Never leave a partial multi-repo state.

Single-repo mode (`projects.length === 1` or scalar-only) uses the legacy single-worktree path verbatim  -  no behavior change for existing tasks.

```json
{
  "taskId": "{jiraId}", "branch": "{branch}", "baseBranch": "{baseBranch}",
  "project": "{name}", "projectRoot": "{path}", "worktreePath": "{path}",
  "logPath": "$HOME/.claude/logs/multi-agent/{project}/{task-id}/",
  "remoteType": "github|bitbucket|generic-git|local",
  "offlineOnly": false,
  "baseFetchStatus": "fresh|cached-stale|local-branch|aborted",
  "inputType": "github-issue-url|github-issue-number|jira-url|jira-id|free-text",
  "jiraId": "PROJ-XXXXX|null", "figmaUrl": "...|null",
  "contextLinks": [
    { "type": "swagger|confluence|crashlytics|fortify|graylog|figma|generic-doc",
      "url": "<full-url|null>", "metadata": { } }
  ],
  "crashContext": null,
  "fortifyFinding": null,
  "graylogContext": null,
  "localMode": false, "instructionDriven": true|false, "instructionFiles": {},
  "identity": {"name": "...", "email": "...", "keychainKey": "..."},
  "currentPhase": 0, "status": "in_progress", "startedAt": "{ISO}", "shortId": "#N",
  "telemetry": { "mcpCalls": [] },
  "phases": { "0": {"status":"done","files":[]}, "1-7": {"status":"pending","files":[]} }
}
```

**Local-only flow**  -  when every entry in `state.projects[]` has `provider="local"`:
- `taskId` format: `LOCAL-{slug-of-freetext}-{yyyymmdd-HHMMSS}` (e.g. `LOCAL-purchase-flow-20260510-143200`). Slug = lowercase, non-alnum → `-`, trimmed, max 32 chars.
- `state.offlineOnly = true` (Phases 6/7 read this flag).
- `state.remoteType` per project = `"local"`.
- `state.baseBranch` = current branch of the local checkout (no `origin/{base}` fetch).
- `state.branch` = local-only feature branch on the same checkout; no upstream tracking is configured (`git checkout -b {branch}` without `-u`).
- No `gh`/`bb` API calls anywhere in Step 8. Worktree creation still applies if the user prefers worktree mode; collision handling stays the same.
- Multi-repo + mixed mode: any project with `provider != "local"` keeps its normal remote workflow; the `offlineOnly` flag is set only when **all** projects are local.

6. Log: "Phase 0: Init complete  -  {project} / {branch} / {identity.name}"

#### Step 7  -  Task Type Detection (deterministic, sets the contract for downstream phases)

Classify task _before_ Phase 1 so downstream phases branch on it. Persist to `agent-state.json.taskType`.

Priority order (first match wins):

1. Description/Jira summary contains Figma URL (`figma.com/design/...` or `figma.com/make/...`) → `component`
2. `instructionDriven == true` AND instruction path contains `figma` → `component`
3. Git diff shows new `Configuration.swift` AND `+Modifiers.swift` → `component`
4. Jira type matches `Bug|Hotfix|Defect` → `bugfix`
5. Branch starts with `bugfix/` or `hotfix/` → `bugfix`
6. Branch `feature/` AND description contains `refactor`|`cleanup`|`rewrite` → `refactor`
7. Branch `feature/` → `feature`
8. Description contains `chore`|`docs`|`ci`|`config` (no code keyword) → `chore`
9. Fallback → ask user (autopilot defaults to `feature`)

Persist: `"taskType": "component" | "bugfix" | "feature" | "refactor" | "chore"`

| Phase   | Behavior change                                                                                          |
| ------- | -------------------------------------------------------------------------------------------------------- |
| Phase 3 | `component` → dispatch to the enabled `ai-<platform>-toolkit` plugin's `create-component` skill (fallback `create-ui-component`); else standard TDD |
| Phase 4 | `bugfix` → test coverage; `component` → accessibility+tokens; `refactor` → behavior preservation         |
| Phase 6 | `bugfix`/`hotfix` → `fix(...)` prefix; `feature`/`component` → `feat(...)`; `refactor` → `refactor(...)` |
| Phase 7 | `component` → includes SubPhase breakdown                                                                |

Log: `Phase 0 Step 7: taskType = {component|bugfix|feature|refactor|chore}`

#### Step 7.5  -  Pipeline depth (Full / Short)

Ask the depth question from `$HOME/.claude/multi-agent-refs/phases/modes.md` "Pipeline depth" - it carries the wording, the per-`taskType` recommendation and the mode tables. Here because the recommendation needs `taskType` (Step 7), which needs the fetched issue (Step 1) and the branch (Step 3).

**Who is asked.** `/multi-agent` and `/multi-agent:local` only. Both autopilot entries and analysis mode skip it; autopilot always runs Full.

When the intake carried an analysis document or a Figma reference, say so **inside** the question: Short skips the only two phases that would turn that document into a task breakdown, and the user should learn that before choosing, not after.

**Pass the default explicitly.** `ask-choice.sh` picks the FIRST option on a non-TTY, so relying on option order breaks the first time someone reorders them for readability, on the host where nobody is watching:

```bash
ASK_CHOICE_DEFAULT="$DEPTH_RECOMMENDATION" \
  $HOME/.claude/lib/ask-choice.sh "Which pipeline for this task?" "Full" "Short"
```

**Persist.** Short sets `state.onlyDevelop = true`; Full leaves it `false`. The key is unchanged - only who sets it changed - so every downstream reader keeps working. Short also flips the Phase 1 and Phase 2 tiles to `skipped` (tracker-contract.md, "Late skip"); pre-marking is forbidden.

Log: `Phase 0 Step 7.5: depth = {full|short} (recommended {full|short}, source {user|autopilot|default})`

#### Step 7.6  -  Test baseline (opt-in, `prefs.global.testBaseline.enabled`, default `false`)

Phase 4 Gate 3 cannot tell an inherited red suite from one this run broke, so it blocks on someone else's bug or the agent "fixes" tests it never touched. Runs after Step 6, only when the stack has a test command; skipped in analysis mode.

```bash
BASELINE_LOG="$WORKTREE/.baseline-test.log"
timeout "${prefs_testBaseline_timeoutSeconds:-600}" <same-test-command-as-Phase-4-Gate-3> 2>&1 | tee "$BASELINE_LOG"
```

Persist `state.baseline.tests` with `command`, `capturedAt`, `logPath` and exactly one status: `green` (passed), `red` + `failing[]` (failed, names parsed), `red` + empty `failing[]` (failed, names unparseable), `unknown` (no test command, `timeout` fired, or flag off). Folding `unknown` into `green` would let a skipped baseline read as a clean tree, which is the failure this record exists to prevent.

Log: `Phase 0 Step 7.6: test baseline = {green|red|unknown} ({N} pre-existing failures)`

#### Step 8  -  Clarification (opt-in, runs AFTER maturity, BEFORE Phase 1)

**Gated by `prefs.global.clarifyAmbiguous.enabled`** (default: `false`). When enabled and `state.maturity.status != "blocker"`:

1. Dispatch `agents/task-clarifier.md` (Haiku by default) with the task title + body + acceptance + maturity warnings already on `agent-state`.
2. The agent returns JSON conforming to `$HOME/.claude/schemas/clarify-output.schema.json`  -  `clarityScore` (0-10), `questions[]`, `stopAndAsk`.
3. If `clarityScore >= prefs.clarifyAmbiguous.minScoreToProceed` (default 6) or `stopAndAsk == false` → write `state.clarification` (score + rationale, no questions), proceed to Phase 1 silently.
4. If `stopAndAsk == true`:

   - **Interactive runs:** render the questions via `AskUserQuestion` (label + header per `$HOME/.claude/multi-agent-refs/rules.md` Language Application matrix; `outputLanguage` for `question` + `option.description`, English for `label` + `header`). Up to `maxQuestions` questions, each with the recommended option flagged. Persist the user's answers under `state.clarification.userAnswers`.
   - **Autopilot runs:** follow `clarifyAmbiguous.autopilotMode`:

     | Mode  | Behavior |
     |---|---|
     | `skip`  | Discard questions, proceed to Phase 1 with no extra context. Logs `clarify.skipped` |
     | `log`   | Append questions to `agent-log.md` for human review, proceed. Default  -  keeps the signal |
     | `abort` | Pause Phase 0 (`state.status = "clarify-pending"`); user resumes via `multi-agent:resume #N`. Then Step 8 re-dispatches AskUserQuestion |

5. Phase 1 Analysis reads `state.clarification.userAnswers` (when present) as additional context  -  fold answers into the Explore prompt so downstream phases inherit the resolution.

**Cost:** ~$0.0025 per Haiku call. The pipeline's other expensive phases (Phase 4 reviewers, Phase 3 Sonnet codegen) far outweigh this  -  the value is avoiding the ~30 min wasted when Phase 3 builds the wrong thing because Phase 0 didn't ask.

**Reference:** see `$HOME/.claude/agents/task-clarifier.md` for the full scoring rubric and question-quality rules.

**Why this fits Phase 0 (not a new phase):** clarification doesn't change what code gets written  -  it changes what gets understood before code is written. Phase 0 already collects identity / project / branch / maturity; ambiguity scoring fits naturally as the last contextual gate.

#### Telemetry

After each clarifier call:

```bash
LOG_METRIC_FORWARD_TO_TRACKER=1 $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 0 clarify.call \
  model=haiku score=$SCORE questions=$Q stop_and_ask=$STOP autopilot_mode=$AP \
  duration_ms=$D tokens_in=$TI tokens_out=$TO
```

Phase 7 cost rollup carries this as a `phase 0` line item so the user sees ambiguity-scoring cost separately from Phase 1 Analysis.

<!-- progress-contract: applied -->

**Progress (per `$HOME/.claude/multi-agent-refs/progress-contract.md`):** emit one `→ <verb> <object>` line for each of: `→ parsing input`, `→ checking token <service>`, `→ scanning project <root>`, `→ creating worktree <repo>`, `→ binding identity <name>`, `→ writing state`. When `clarifyAmbiguous.enabled`, also emit `→ scoring task ambiguity` before Step 8 and `→ asking clarifying questions <N>` when `stopAndAsk` fires.

**Save preferences**: Write updated prefs to `$HOME/.claude/multi-agent-preferences.json` with all Phase 0 selections.

---

#### Phase 0 exit gate (BLOCKING  -  run before marking the phase completed)

Phase 0 owns `agent-state.json`. Do not call
`phase-tracker.sh update 0 completed` until this gate passes:

```bash
node "$HOME/.claude/scripts/phase0-exit-gate.mjs" "$TASK_ID" --input "$ORIGINAL_INPUT"
```

It asserts three things, each of which has failed silently in a real run:

1. **`agent-state.json` exists.** A run once reported Phase 0 `completed` with only
   `tracker-state.json` on disk. Every later phase then reasons from fields that are
   not there.
2. **`taskType` is set.** Phase 3 branches on it (Step 7). Absent, a Figma-driven
   screen is dispatched as generic development, skipping the stack plugin's
   token-compliance check, Code Connect publish and component review. That run
   guessed `16` where the frame said `Spacing/12`, and half its commits were rework.
3. **A Figma reference forces `taskType: "component"`, and `figmaAccess.tier` is
   recorded.** Without the tier, a later phase cannot tell "the design was confirmed"
   from "the design was never fetched"  -  which is exactly when spacing gets guessed.

A failure is a halt, not a warning. Fix the state and re-run the gate; the phase
stays `in_progress` until it passes. **Never** mark Phase 0 completed on the grounds
that its steps ran  -  the gate checks the output, and the output is what Phase 3
consumes.
