# Claude Code Account Parity Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Give Claude Code accounts the same add, default, rename, remove, re-authenticate, health, and limit functionality as Codex accounts.

**Architecture:** Route account actions through provider service records instead of Codex-specific `Indicator` fields. Add transactional Claude login, a concurrency-safe OAuth session, and a Claude health provider that keeps authentication health independent from the internal usage endpoint.

**Tech Stack:** Python 3.13, GTK 3 / AyatanaAppIndicator, `subprocess`, `urllib.request`, `fcntl`, pytest, Ruff, mypy.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|---|---|---|---|
| 1 | Tasks 1, 2, 3 | `provider_services.py`, `account_registry.py`, `claude_oauth.py`, separate tests | Yes; no file overlap |
| 2 | Tasks 4, 5 | `claude_auth_operation.py`, `claude_health_client.py`, separate tests | Yes; both consume Wave 1 contracts only |
| 3 | Tasks 6, 7 | `indicator.py` and UI tests; manual smoke script and its test | Yes; no file overlap |
| 4 | Task 8 | startup wiring, startup/integration tests | Single task; depends on UI and providers |
| 5 | Task 9 | no source changes expected | Single verification gate |

## Task 1: Provider Contracts And Codex Adapter

**Wave:** 1
**Blocks:** Tasks 4, 5, 6, 8
**Blocked by:** -

**Files:**
- Create: `provider_services.py`
- Create: `tests/test_provider_services.py`
- Modify: `device_auth_operation.py:27-381` only if a public event type annotation is required

- [ ] **Step 1: Write failing contract and Codex adapter tests**

```python
def test_codex_lifecycle_adapter_translates_repair_events():
    operation = FakeDeviceAuthOperation([FakeDeviceEvent(kind="success")])
    adapter = CodexLifecycleAdapter(operation)

    events = list(adapter.reauthenticate(fake_account("codex")))

    assert [event.kind for event in events] == ["success"]


def test_provider_services_are_selected_by_tool():
    codex = fake_services("codex")
    claude = fake_services("claude")
    services = ProviderServiceMap([codex, claude])

    assert services.for_tool("codex") is codex
    assert services.for_tool("claude") is claude
```

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_provider_services.py -q`
Expected: FAIL because `provider_services` does not exist.

- [ ] **Step 3: Implement provider-neutral contracts**

Create immutable `LifecycleEvent` and `ProviderServices` records, `AccountLifecycle` and `AccountHealthProvider` protocols, a duplicate-rejecting `ProviderServiceMap`, and `CodexLifecycleAdapter`. Translate every existing Codex event field used by `Indicator` (`kind`, `account`, `message`, `error`, `prompt`, `session`, `collision`, `existing_accounts`) without changing Codex behavior.

```python
@dataclass(frozen=True)
class ProviderServices:
    tool: AccountRegistryKind
    registry: AccountRegistry
    lifecycle: AccountLifecycle
    health: AccountHealthProvider


class ProviderServiceMap:
    def __init__(self, providers: Iterable[ProviderServices]) -> None:
        provider_list = tuple(providers)
        self._providers = {provider.tool: provider for provider in provider_list}
        if len(self._providers) != len(provider_list):
            raise ValueError("duplicate provider services")

    def for_account(self, account: Account) -> ProviderServices:
        return self.for_tool(AccountRegistryKind(account.tool))
```

- [ ] **Step 4: Run focused tests and verify GREEN**

Run: `python3 -m pytest tests/test_provider_services.py tests/test_device_auth_operation.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add provider_services.py tests/test_provider_services.py device_auth_operation.py
git commit -m "feat: add provider service contracts"
```

## Task 2: Transactional Account Registry Operations

**Wave:** 1
**Blocks:** Tasks 4, 6
**Blocked by:** -

**Files:**
- Modify: `account_registry.py:128-420`
- Modify: `tests/test_account_registry.py:349-457`

- [ ] **Step 1: Write failing staged-add and remove transaction tests**

Cover: staged account invisible before commit; commit registers and atomically renames; rollback removes staging; concurrent destination causes commit failure without overwrite; default removal selects the first remaining account; final removal clears default and active Claude links; registry-write failure restores quarantined account data.

```python
def test_stage_add_is_invisible_until_commit(claude_registry):
    staged = claude_registry.stage_add("Rafa")
    assert claude_registry.list() == []
    (staged.account_home / ".credentials.json").write_text("{}")

    account = staged.commit()

    assert claude_registry.list() == [account]
    assert account.account_home.joinpath(".credentials.json").exists()
```

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_account_registry.py -q`
Expected: FAIL with missing `stage_add` and transactional remove behavior.

- [ ] **Step 3: Implement registry-owned transactions**

Add an account-root lock, `StagedAccount`, `stage_add(alias)`, `_commit_staged`, and quarantine-based `remove`. Keep `add_dir` for current Codex callers. Use atomic registry/default writes already present. Never expose `.staging-*` or `.removing-*` directories from `list()`.

```python
@dataclass
class StagedAccount:
    registry: AccountRegistry
    slug: str
    alias: str
    root: Path
    account_home: Path
    _closed: bool = False

    def commit(self) -> Account:
        if self._closed:
            raise RuntimeError("staged account already closed")
        account = self.registry._commit_staged(self)
        self._closed = True
        return account

    def rollback(self) -> None:
        if not self._closed:
            shutil.rmtree(self.root, ignore_errors=True)
            self._closed = True
```

- [ ] **Step 4: Run registry and router tests**

Run: `python3 -m pytest tests/test_account_registry.py tests/test_command_router.py -q`
Expected: PASS; existing Codex and Claude routing behavior unchanged.

- [ ] **Step 5: Commit**

```bash
git add account_registry.py tests/test_account_registry.py
git commit -m "feat: make account changes transactional"
```

## Task 3: Claude OAuth Session

**Wave:** 1
**Blocks:** Task 5
**Blocked by:** -

**Files:**
- Create: `claude_oauth.py`
- Create: `tests/test_claude_oauth.py`

- [ ] **Step 1: Write failing request, refresh, race, merge, and redaction tests**

Use a fake transport; never read real home credentials. Assert one refresh retry on 401, no retry on 403/404, token replacement under lock wins over stale state, unknown top-level fields and `mcpOAuth` survive the atomic write, and exception strings contain no tokens.

```python
def test_401_refreshes_once_and_preserves_unknown_credentials(tmp_path):
    credentials = tmp_path / ".credentials.json"
    write_credentials(credentials, access="old", refresh="refresh", extra={"future": 1})
    transport = FakeTransport([response(401), refresh_response("new"), usage_response()])

    payload = ClaudeOAuthSession(credentials, transport=transport).get_json(USAGE_URL)

    assert payload["five_hour"]["utilization"] == 12
    assert read_credentials(credentials)["future"] == 1
    assert read_credentials(credentials)["claudeAiOauth"]["accessToken"] == "new"
```

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_claude_oauth.py -q`
Expected: FAIL because `claude_oauth` does not exist.

- [ ] **Step 3: Implement the deep OAuth helper**

Pin:

```python
USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
TOKEN_URL = "https://platform.claude.com/v1/oauth/token"
CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
```

Implement typed `ClaudeOAuthError` subclasses, bounded JSON HTTP, one locked refresh retry, re-read-after-lock race handling, and mode-preserving atomic credential replacement. Redact bearer/access/refresh token values from all raised messages.

- [ ] **Step 4: Run focused tests and verify GREEN**

Run: `python3 -m pytest tests/test_claude_oauth.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add claude_oauth.py tests/test_claude_oauth.py
git commit -m "feat: add Claude OAuth usage session"
```

## Task 4: Claude Account Lifecycle

**Wave:** 2
**Blocks:** Tasks 6, 8
**Blocked by:** Tasks 1, 2

**Files:**
- Create: `claude_auth_operation.py`
- Create: `tests/test_claude_auth_operation.py`

- [ ] **Step 1: Write failing add and re-authentication tests**

Cover add success/cancel/CLI failure/malformed status/duplicate identity; repair success/cancel/wrong known identity/legacy identity baseline/collision/backup restoration. Fake process spawning and status reads.

```python
def test_add_commits_only_verified_unique_identity(claude_registry):
    operation = ClaudeAuthOperation(
        claude_registry,
        runner=FakeClaudeRunner(login_code=0, status=healthy_status("a@example.com", "org-a")),
    )

    events = list(operation.add("A", email="a@example.com"))

    assert events[-1].kind == "success"
    account = claude_registry.list()[0]
    assert read_identity(account) == {"email": "a@example.com", "org_id": "org-a"}
```

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_claude_auth_operation.py -q`
Expected: FAIL because `ClaudeAuthOperation` does not exist.

- [ ] **Step 3: Implement Claude login orchestration**

Run `claude auth login --claudeai` with optional paired `--email <email>`, set only the target `CLAUDE_CONFIG_DIR`, emit provider-neutral lifecycle events, and verify with bounded `claude auth status --json`. Store `account_identity.json` atomically. Repair under an account lock and restore credentials/identity metadata on every non-success path. Never call `claude auth logout` during local removal.

- [ ] **Step 4: Run lifecycle and registry tests**

Run: `python3 -m pytest tests/test_claude_auth_operation.py tests/test_account_registry.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add claude_auth_operation.py tests/test_claude_auth_operation.py
git commit -m "feat: add Claude account lifecycle"
```

## Task 5: Claude Health And Limits Provider

**Wave:** 2
**Blocks:** Tasks 6, 7, 8
**Blocked by:** Tasks 1, 3

**Files:**
- Create: `claude_health_client.py`
- Create: `tests/test_claude_health_client.py`
- Modify: `health_client.py:14-32` only for provider-neutral snapshot fields required by parsed limits
- Modify: `tests/test_health_client.py` for snapshot compatibility

- [ ] **Step 1: Write failing auth and usage independence tests**

Cover healthy/broken/unknown status mapping; five-hour/seven-day percentages and resets; optional/null/unknown fields; scoped limits; usage 401/403/404/timeout/schema failure retaining healthy auth; stale successful values merged by existing store behavior.

```python
def test_usage_failure_does_not_mark_authenticated_account_broken(account):
    client = ClaudeHealthClient(
        status_runner=FakeStatusRunner(healthy_status("a@example.com", "org-a")),
        oauth_factory=raising_oauth(ClaudeUsageUnavailable("schema changed")),
    )

    snapshot = client.fetch(account)

    assert snapshot.status is HealthStatus.HEALTHY
    assert snapshot.primary_used_pct is None
    assert snapshot.detail == "Claude usage unavailable"
```

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_claude_health_client.py -q`
Expected: FAIL because `ClaudeHealthClient` does not exist.

- [ ] **Step 3: Implement auth status and usage parsing**

Execute `claude auth status --json` with the account config dir and timeout. Parse identity/plan independently. Fetch usage through `ClaudeOAuthSession`, mapping `five_hour` to primary and `seven_day` to secondary. Normalize utilization to `0..100`, parse ISO reset timestamps, retain optional active limit rows, set `checked_at`, and turn usage-only failures into redacted detail without changing healthy auth.

- [ ] **Step 4: Run health/store/router tests**

Run: `python3 -m pytest tests/test_claude_health_client.py tests/test_health_client.py tests/test_health_store.py tests/test_routing_resolver.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add claude_health_client.py health_client.py tests/test_claude_health_client.py tests/test_health_client.py
git commit -m "feat: add Claude health and limits"
```

## Task 6: Provider-Aware Tray And Popup Actions

**Wave:** 3
**Blocks:** Task 8
**Blocked by:** Tasks 1, 2, 4, 5

**Files:**
- Modify: `indicator.py:99-967`
- Modify: `account_card.py:75-153`
- Modify: `tests/test_indicator.py:668-2417`
- Modify: `tests/test_account_card.py`

- [ ] **Step 1: Write failing Claude parity UI tests**

Assert both provider sections render Add Account. Every card dispatches set-default/reload/re-authenticate/rename/remove to its own provider. Same-slug Codex/Claude cards never collide. Running one action disables only that account. Reload All and popup-open refresh both providers. Usage failure retains stale bars and timestamp while showing healthy auth.

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_indicator.py tests/test_account_card.py -q`
Expected: FAIL on Claude lifecycle dispatch and provider service constructor expectations.

- [ ] **Step 3: Refactor `Indicator` to provider services**

Replace direct registry/device-auth/health selection with `ProviderServiceMap.for_account`. Keep UI callbacks provider-neutral. Add provider-scoped Add Account controls and per-card Rename/Remove controls using existing GTK helpers. Use `(tool, slug)` keys for cards, buttons, snapshots, and in-flight state. Preserve existing Codex copy and behavior.

- [ ] **Step 4: Run focused UI tests and verify GREEN**

Run: `python3 -m pytest tests/test_indicator.py tests/test_account_card.py tests/test_popup_window.py tests/test_tray_model.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add indicator.py account_card.py tests/test_indicator.py tests/test_account_card.py
git commit -m "feat: add Claude account actions to tray"
```

## Task 7: Opt-In Claude Health Smoke Script

**Wave:** 3
**Blocks:** Task 9
**Blocked by:** Task 5

**Files:**
- Create: `scripts/verify_claude_health.py`
- Create: `tests/test_verify_claude_health.py`

- [ ] **Step 1: Write a failing redaction/output-shape test**

Require output to contain only status, boolean availability, field names, and reset parse success. Assert supplied email, organization ID, access token, and refresh token never appear.

- [ ] **Step 2: Run test and verify RED**

Run: `python3 -m pytest tests/test_verify_claude_health.py -q`
Expected: FAIL because the script does not exist.

- [ ] **Step 3: Implement an explicit-account smoke command**

Require `--account-home`; do not default to `~/.claude`. Reuse `ClaudeHealthClient`, print redacted JSON summary, and return nonzero for broken/unknown auth or unavailable required five-hour/seven-day fields.

- [ ] **Step 4: Run test and verify GREEN**

Run: `python3 -m pytest tests/test_verify_claude_health.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add scripts/verify_claude_health.py tests/test_verify_claude_health.py
git commit -m "test: add Claude health smoke probe"
```

## Task 8: Startup, Scheduler, And Router Integration

**Wave:** 4
**Blocks:** Task 9
**Blocked by:** Tasks 1, 4, 5, 6

**Files:**
- Modify: `systray_codex_switcher.py:117-144`
- Modify: `scheduler.py:39-126` only if provider concurrency requires scheduler changes
- Modify: `tests/test_systray_codex_switcher.py:101-257`
- Modify: `tests/test_scheduler.py`
- Modify: `tests/test_command_router.py` for Claude cache integration

- [ ] **Step 1: Write failing startup and dual-provider refresh tests**

Assert startup constructs two `ProviderServices`, wires `ClaudeAuthOperation` and `ClaudeHealthClient`, preserves both migrations, writes separate caches, schedules both provider account sets, and lets `cld` consume fresh Claude snapshots without affecting `cdx`.

- [ ] **Step 2: Run tests and verify RED**

Run: `python3 -m pytest tests/test_systray_codex_switcher.py tests/test_scheduler.py tests/test_command_router.py -q`
Expected: FAIL on the old `Indicator(registry, health_client, claude_registry=...)` wiring.

- [ ] **Step 3: Wire provider services at composition root**

Create Codex and Claude lifecycle/health instances in `main()`, pass a `ProviderServiceMap` into `Indicator`, and keep provider-specific construction out of UI modules. Ensure scheduler work remains off GTK and snapshots stay tool-qualified.

- [ ] **Step 4: Run integration tests and verify GREEN**

Run: `python3 -m pytest tests/test_systray_codex_switcher.py tests/test_scheduler.py tests/test_command_router.py tests/test_indicator.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add systray_codex_switcher.py scheduler.py tests/test_systray_codex_switcher.py tests/test_scheduler.py tests/test_command_router.py
git commit -m "feat: wire Claude account parity"
```

## Task 9: Full Verification And Manual GTK Gate

**Wave:** 5
**Blocks:** -
**Blocked by:** Tasks 7, 8

**Files:**
- Verify only; fix failures in their owning files and commit each fix separately

- [ ] **Step 1: Run the complete automated gate**

Run:

```bash
python3 -m pytest tests/ -q
ruff check .
mypy .
```

Expected: all commands exit 0 with zero failures/errors.

- [ ] **Step 2: Run the opt-in local Claude health smoke**

Run: `python3 scripts/verify_claude_health.py --account-home ~/.systray-ai/claude-accounts/<verified-slug>/CLAUDE_HOME`
Expected: exit 0; redacted JSON reports healthy auth and available five-hour/seven-day limits. Replace `<verified-slug>` with a slug from the local Claude registry before execution; do not commit it.

- [ ] **Step 3: Run manual GTK lifecycle smoke**

Verify, in order: add Claude account, set default, rename, reload, re-authenticate same identity, cancel a second re-auth attempt and confirm rollback, remove a non-default account, remove a default account and confirm deterministic reassignment. Confirm Codex actions still work after every Claude action.

- [ ] **Step 4: Inspect final diff and security properties**

Run:

```bash
git diff --check
git status --short
rg -n "accessToken|refreshToken|Authorization" --glob '*.py' --glob '!tests/**'
```

Expected: no whitespace errors; only intended files changed; token strings appear only as schema keys/header construction and never in logging or UI paths.

- [ ] **Step 5: Commit verification fixes, if any**

Stage only exact files changed by a verified fix. Use a narrow commit message describing that fix. Do not create an empty verification commit.

## Self-Review

- Completeness: all spec behaviors map to a task and test surface; no placeholder implementation decisions remain.
- Dependency order: provider contracts, registry transactions, and OAuth are independent Wave 1 foundations; auth and health can then proceed in parallel; UI precedes composition-root wiring.
- File conflicts: no same-wave task modifies the same file.
- TDD: every implementation task begins with a focused failing test and explicit RED/GREEN commands.
- Security: tests use fake credentials/transports; the only live smoke is opt-in and redacted.
- Scope: no remote token revocation, Console/API account setup, terminal `/usage` scraping, or spend UI.
