# Setup Step 5  -  Repo Discovery (full contract)

> Split out of `setup/SKILL.md` (token budget). The SKILL keeps the opt-in prompt and a summary; this file carries the scan commands, merge rules, picker, prefs shapes and skip / degrade rules. Read it when the user opts into repo discovery.

Pre-populates the Phase 0 project picker. Without this, every task types the repo path by hand; with this, the picker shows a curated list of repos the user has been active in.

Three sources merge into one flat list. Each source is independent  -  any one failing does NOT block the others.

**Prompt:**
```
Step 5  -  Repo Discovery (optional)
Scan for recent repos now to pre-populate the Phase 0 picker?

  y  -  all three (local + Bitbucket + GitHub, ~10-30s)
  l  -  local scan only (fastest, no network)
  n  -  skip (Phase 0 will prompt for repo path manually)
```

**7a. Local scan**  -  `$HOME` depth-3, repo markers

```bash
find "$HOME" -maxdepth 3 -type d -name ".git" 2>/dev/null | while read gitdir; do
  repo_dir="$(dirname "$gitdir")"
  origin="$(git -C "$repo_dir" config --get remote.origin.url 2>/dev/null)"
  last="$(git -C "$repo_dir" log -1 --format=%cI 2>/dev/null)"
  printf '%s\t%s\t%s\n' "$repo_dir" "$origin" "$last"
done
```

Auxiliary markers (recorded but not required): `*.xcworkspace`, `*.xcodeproj`, `Podfile`, `Package.swift`, `package.json`, `pom.xml`, `build.gradle`. Their presence tags the repo as iOS/macOS, Cocoapods, SPM, Node, Maven, Gradle in the picker.

**7b. Bitbucket scan**  -  last 90 days active

Runs only if `keychainMapping.bitbucket_token` is non-null AND `serviceStatus.bitbucket.ok === true` (or cache older than `settings.serviceStatusCacheSeconds`, re-ping first).

Bitbucket Data Center (self-hosted)  -  the host comes from `prefs.global.bitbucketHost` (or prompts once if unset; NEVER hardcoded):
```bash
BB_HOST="$(jq -r '.global.bitbucketHost // empty' "$PREFS_FILE")"
BB_USER="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_user' "$PREFS_FILE")" 2>/dev/null)"
BB_TOK="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_token' "$PREFS_FILE")" 2>/dev/null)"
CUTOFF_MS=$(( ( $(date +%s) - 7776000 ) * 1000 ))   # 90 days
curl -sfS -u "$BB_USER:$BB_TOK" \
  "https://${BB_HOST}/rest/api/1.0/repos?permission=REPO_WRITE&limit=200" \
  | jq -r --argjson cutoff "$CUTOFF_MS" '
      .values[] | select((.updatedDate // 0) >= $cutoff) |
      { slug: "\(.project.key)/\(.slug)", clone: (.links.clone[] | select(.name=="http") | .href) }'
```

Bitbucket Cloud  -  swap to `https://api.bitbucket.org/2.0/repositories/{workspace}?role=member&sort=-updated_on`.

**7c. GitHub scan**  -  owner + collaborator, push in last 90 days

Runs only if `keychainMapping.github` is non-null AND `serviceStatus.github.ok === true`.

```bash
GH_TOK="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.github' "$PREFS_FILE")" 2>/dev/null)"
CUTOFF_ISO="$(date -u -v-90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ)"
# gh CLI path (preferred when available)
gh api --paginate 'user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100' \
  -q ".[] | select(.pushed_at > \"$CUTOFF_ISO\") | {full: .full_name, clone: .clone_url}"
# Fallback without gh:
curl -sfS -H "Authorization: Bearer $GH_TOK" \
  'https://api.github.com/user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100'
```

**7d. Merge by origin URL**

Normalize clone URLs (strip `git@`, `https://`, trailing `.git`) to a canonical `host/owner/repo` key. Deduplicate:
- Local + remote match on canonical key → single entry with `path = <local>`, `platform = <remote source>`.
- Remote-only match → entry with `path: null` (user clones on first use from Phase 0).
- Local-only (no remote clone URL matches) → kept as-is, `platform: "local"`.

**7e. Multi-select picker**

```
Discovered 24 active repos (local + remote ≤ 90 days):

  [ ]  1. Booking               ~/dev/Booking              bitbucket   iOS · SPM
  [ ]  2. UIComponents          ~/dev/UIComponents         bitbucket   iOS · SPM
  [ ]  3. multi-agent-pipeline  ~/multi-agent-pipeline     github      Node
  [ ]  4. dotfiles              (not cloned)               github       - 
  [ ]  5. backend-services      ~/work/backend-services    bitbucket   Maven
  ...

  Toggle with numbers (e.g. "1 3 4"), 'all', 'none', or Enter to skip.
  Selected repos become defaults in the Phase 0 project picker.
```

**7f. Save to preferences**

```json
{
  "global": {
    "recentProjects": [
      {"path": "~/dev/Booking",            "label": "Booking",            "lastUsed": "2026-04-15T21:08:00Z"},
      {"path": "~/dev/UIComponents",       "label": "UIComponents",       "lastUsed": "2026-04-15T21:08:00Z"},
      {"path": "~/multi-agent-pipeline",   "label": "multi-agent-pipeline","lastUsed": "2026-04-15T21:08:00Z"}
    ],
    "serviceStatus": {
      "bitbucket": {"ok": true, "checkedAt": "2026-04-15T21:08:00Z"},
      "github":    {"ok": true, "checkedAt": "2026-04-15T21:08:00Z"}
    }
  }
}
```

Entries are objects (not bare strings). The schema `recentProjects.items` is a `oneOf`  -  both shapes validate, but v2.1.0 writes objects so `lastUsed` + `label` are first-class.

**7g. Skip / degrade rules**

- Source failure (network, VPN, 401, rate-limit) skips that source; the others still run. Failure is recorded in `serviceStatus.{service} = { ok: false, checkedAt: <now>, reason: <short> }` so Phase 0 can also read that cache.
- **Report the skip, do not swallow it.** Setup is where the user is actively configuring things, so a source that could not be reached is exactly what they need to hear about, and the reason decides their next move: a 401 means refresh that token, no response at all on a corporate host means connect the VPN, and a missing `global.hosts.<service>` means the token has nowhere to point. Print one line per skipped source with its classification (see `refs/keychain.md` Rule 2) before moving on. A discovery pass that quietly returned three repos out of forty reads as "you only have three repos".
- If all three sources yield zero hits, Step 5 is a no-op  -  the picker in Phase 0 falls back to manual path entry.
- Discovery is **never destructive**: it never modifies git state, never writes outside `PREFS_FILE`.
- Step 5 is idempotent. Re-running it refreshes `recentProjects` and `serviceStatus` but preserves user-curated entries unless explicitly deselected.

