## Token Save Flow (reusable)

This flow is used in TWO contexts:
- **Setup Step 4**  -  for missing tokens during first-time onboarding
- **Phase 0 token pre-check**  -  when a token is missing or expired (401/403) at runtime

**Trigger**: token missing (`keychainMapping` is `null`) OR token expired (API returned 401/403).

**Step A  -  Ask** (options depend on whether the token is missing or expired):

Missing token (first-time / unmapped):
```
{Service} token missing.
  Where to get it: {source URL/instructions}

  [1] Add now  -  I'll generate the token and copy it
  [2] Skip  -  I'll set it up later

  Select:
```

Expired / rejected token (401 / 403 mid-run)  -  the Expired-token decision from `$HOME/.claude/multi-agent-refs/keychain.md` Rule 1:
```
{Service} token expired (the service returned 401/403).
  Where to get a fresh one: {source URL/instructions}

  [1] Regenerate  -  replace {KEY_NAME} in place, then retry
  [2] Use a different token  -  map another Keychain entry, then retry
  [3] Skip and continue  -  drop this source (halts only if the token is required for the input)

  Select:
```

Render `question` + `label` + `description` in `outputLanguage`; `header` English. Names below are option semantics, not strings to print. The picked option is a choice, never the secret: the value enters via Step B clipboard.

- **Skip** (missing `[2]` / expired `[3]`) → skip semantics. During setup: `keychainMapping` stays `null`. During Phase 0 / mid-run: abort that service's contribution, warn and continue if non-critical, halt if critical (e.g. Jira token for Jira input).
- **Add now** / **Regenerate** → proceed to Step B (clipboard save). Regenerate reuses the same `{KEY_NAME}`.
- **Use a different token** → proceed to Step B, then re-run Step C identity binding for the new entry.

**Step B  -  Clipboard save** (token NEVER appears in terminal):

For PAT / API key / username:
```
  Copy the token to your clipboard, then press Enter.
  (Token will be read from clipboard and saved to Keychain automatically)

  Press Enter when ready...
```

Pipeline runs silently  -  value is piped via stdin (`-` sentinel) so it never lands on argv / shell history:
```bash
pbpaste | ~/.claude/lib/credential-store.sh set "<KEY_NAME>" -
pbcopy < /dev/null
```

(The shell driver auto-delegates to `~/.claude/scripts/keychain.py`, which writes both `-l` (label) and `-s` (service) attributes so callers using either convention find the same entry.)

For JSON file (Firebase)  -  same clipboard flow, JSON content never lands in argv or shell history:
```
  Open the Service Account JSON, copy its FULL contents to your clipboard,
  then press Enter.
  (Contents are saved to the Keychain exactly as issued)

  Press Enter when ready...
```

The pipeline then runs the same `pbpaste | ... set "<KEY_NAME>" -` and `pbcopy < /dev/null` as above. A key that arrives as a file (the App Store Connect `.p8`, the Google Play JSON) skips the clipboard: `set <key> - < <file>`.

**Firebase repeats per project.** Teams own several Firebase projects (legacy plus redesign, staging plus prod), each with its own key, and a crash URL from the second one used to fail the `project_id` check as if it were misconfigured. After each saved key, read `project_id` from the stored JSON (never ask) and offer another round: `Saved: <project_id>. Add another Firebase project? [y/N]`.

- Every key appends `{projectId, keychainKey, label?}` to `prefs.global.firebase.accounts`; the first one also fills `keychainMapping.firebase`, so a one-project setup is unchanged.
- Keys after the first are named `${USER}_Firebase_Access_Json_<projectId>` so they cannot collide.
- At run time `fetch-crashlytics.sh` picks the account matching the console URL's projectId, falling back to `keychainMapping.firebase`.

For GitHub (special case):
```
  ! gh auth login
```

**Step C  -  Identity binding** (after token saved):

```
  ✓ Saved to Keychain as <KEY_NAME>
  ✓ Clipboard cleared

  Which git identity uses this token?

    Existing identities:
      1. Ada Lovelace <ada@example.com>
      2. Ada Lovelace <ada@personal.com>
      n. Create new identity

    Select:
```

- Existing identity selected → add `{service}: <KEY_NAME>` to that identity's `servicePatMap`
- `n` (create new) → ask name + email → create identity → add to `prefs.global.identities[]` → map token
- First token ever (no identities exist) → skip picker, go straight to name + email
- `bitbucket_user` is excluded from identity binding (it's a username, not a PAT)

**Step D  -  Confirm + update**:

```
  ✓ Token mapped to identity: Ada Lovelace <ada@example.com>
```

Update `prefs.global.keychainMapping.{service}` with the key name.
Update `identity.servicePatMap.{service}` with the key name.
Update `serviceStatus.{service} = { ok: true, checkedAt: <now> }`.

**→ Next: if `{service}` is `jira` / `confluence` / `bitbucket` / `fortify` / `graylog` AND the corresponding
`prefs.global.hosts.{service}` is unset, run Step 3.5 (Host Prompt) inline before
the Auto-routing rule below. required  -  the pipeline cannot build API URLs without
the host. Firebase is exempt (see Step 3.5).**

**Auto-routing rule** (optional, on first token per platform):

If `platformIdentityRouting` has no rule for this platform yet, auto-suggest one:
```
  Auto-add routing rule?
    bitbucket.example.com/* → Ada Lovelace <ada@example.com>
    [enter=yes / n=skip]
```

This builds `platformIdentityRouting` incrementally  -  no separate Step 7 needed for most users.

## Step 3.5  -  Host Prompt (embedded in Token Save Flow)

**Not a standalone step**  -  runs inline at the end of the Token Save Flow whenever the saved token belongs to a **hosted service** (Jira, Confluence, Bitbucket, Fortify, Graylog) AND the host is not yet in `prefs.global.hosts`. Firebase tokens skip this step  -  Crashlytics is always on Google's fixed domains and `project_id` is embedded in the service-account JSON.

Right after Step D (identity mapping confirmed), ask:

```
Service host  -  needed to build API URLs for this token.

  {Service} host (e.g. jira.example.com): ___
```

Save to `prefs.global.hosts.{service}`. For Fortify, also ask `Fortify project version ids (optional, comma-separated - e.g. 1234,5678)` → `prefs.global.fortify.versionIds`. A ticket naming only an instance id carries no version, so the lookup silently no-ops without these; URL-referenced findings resolve either way.

For Jira, **discover the project keys instead of asking the user to recall them.** The token is already saved and the host is already known, so ask Jira which projects this person actually works in - a corporate instance has thousands of projects, and a typed key is a typo waiting to route branches and new issues at the wrong board:

```bash
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
  "https://{JIRA_HOST}/rest/api/2/search?jql=assignee%3DcurrentUser()%20OR%20reporter%3DcurrentUser()%20ORDER%20BY%20updated%20DESC&fields=project&maxResults=100" \
  | jq -r '[.issues[].fields.project | "\(.key)\t\(.name)"] | unique | .[]'
```

Present the distinct keys, most-recently-worked first, as a picker; fall back to the free-text prompt `Default Jira project key (e.g. PROJ)` when the call fails or returns nothing (no VPN, fresh account). The picked key → `prefs.global.defaultJiraKey`. From the Jira host, derive `corpDomain` (`jira.example.com` → `example.com`) and pre-fill it so the Confluence / Bitbucket prompts only need the subdomain.

`defaultJiraKey` is a single global fallback, but one Jira host usually serves several repos with DIFFERENT keys, so the pass continues with a per-repo mapping. Show the repos, and let the key come from the discovered list rather than being typed again:

```
Per-repo Jira keys (optional)  -  repos whose project key differs from {defaultJiraKey}:

  [ ] 1. my-ios-app        (current: PROJA)
  [ ] 2. my-other-app      (current:  - )

  Toggle repos, then pick a project for each  -  discovered: PROJA, PROJB, PROJC. Enter to skip.
```

- The repo list comes from `prefs.projects` + `recentProjects` (when Step 5 already ran); a free-text repo path is accepted for repos not discovered yet, and a free-text key for a project the search did not surface.
- Each answer is PREPENDED to `prefs.projects[{slug}].jiraProjectKeys` (deduped, max 10 per schema). Example: `my-ios-app` → `PROJA`, `my-other-app` → `PROJB`  -  two repos on the same Jira host, two different project keys.
- Resolution order everywhere a Jira key is needed (placeholder replacement, branch names, new-issue creation): `prefs.projects[{slug}].jiraProjectKeys[0]` first, `global.defaultJiraKey` as fallback.
- **Re-run / update**: `/multi-agent:setup jira-keys` re-opens only this mapping without touching tokens or hosts.

Resulting shape:

```json
{
  "global": {
    "hosts": {
      "jira":       "jira.example.com",
      "confluence": "confluence.example.com",
      "bitbucket":  "bitbucket.example.com",
      "fortify":    "ssc.example.com",
      "graylog":    "graylog.example.com",
      "graylogTest": "graylog-test.example.com",
      "corpDomain": "example.com"
    },
    "defaultJiraKey": "PROJ"
  }
}
```

Placeholders `{JIRA_HOST}`, `{CONFLUENCE_HOST}`, `{BITBUCKET_HOST}`, `{FORTIFY_HOST}`, `{GRAYLOG_HOST}`, `{GRAYLOG_TEST_HOST}`, `{CORP_DOMAIN}`, `{JIRA_KEY}` in skills/commands are resolved from this block at runtime. If a host is ever missing when a phase needs it, Phase 0 prompts the same Host Prompt inline.

Graylog uses a **PAT** whose API auth is HTTP Basic with the token as username and the literal `token` as password  -  no separate username entry. It is optional by the null-mapping convention: `keychainMapping.graylog` or `hosts.graylog` left `null` degrades the log fetch to empty and never blocks a run.

Graylog is asked **twice**  -  test and production are separate instances, and a tester-minted trx id does not exist in production. After the production host:

```
Graylog test host (optional  -  Enter to skip): ___
Does the test instance use a different token? [y/N]
```

Test host → `hosts.graylogTest` (`{GRAYLOG_TEST_HOST}`); skipped means production-only search, which degrades nothing. Yes to the token question runs one more Token Save Flow pass for `graylog_test`; no (default) leaves it null and the fetcher reuses the production key. Run-time order is `--env auto`: production first, test on empty or unreachable, and the payload names which answered.

**Re-run / update**: `/multi-agent:setup hosts` re-opens the prompt to edit values without touching tokens.

Team-sharing note: Hosts are the only corporate metadata the pipeline needs. Tokens live in macOS Keychain (encrypted, never on disk inside the repo). Preferences file lives in `~/.claude/`  -  fully outside the repo clone. A teammate cloning the repo runs `/multi-agent:setup` once; host + token prompts run naturally together.
