type: meta

slug: claude-account-parity

topic: Claude Code account lifecycle, authentication health, usage limits, and tray parity with Codex via run-plan-codex

exec_mode: run-plan-codex

scheduler: dag-parallel

land_mode: pr

base_branch: main

gate0_cmd: python3 -m pytest tests/ -q && ruff check . && mypy .

codex_impl_effort: high

codex_review_effort: high

codex_review_fallback_effort: high

status_enum: BLOCKED PENDING WORKING WRITTEN REVIEWED COMMITTED

truth_rule: this file is ground truth for intent and task state; git is ground truth for done-ness; reconcile statuses against git

refresh_how: native Read of this JSONL before each wave and after compaction

refresh_what: meta, goals, methodology, intent, directions, session memory, and all non-COMMITTED tasks

refresh_when:
wave boundary
task status change
after compaction
before review

update_rule: rewrite the affected task line on status change; append session_memory records for new durable facts

report_rule: report only done, blocked, deferred, or canceled

executor_notes: Invoke: node /home/user/.claude/workflows/run-plan-codex.js --slug claude-account-parity --repo /home/user/Projects/systray-ai --concurrency 3. The executor reads base_branch, gate0_cmd, Codex model/effort fields, and per-task wave/files/blocked_by/seat. Wave order is authoritative. Manual live-account smoke is post-run because every JSONL task must produce a commit.


type: goal

text: Give Claude Code accounts the same tray add, default, rename, remove, re-authenticate, authentication-health, and usage-limit functionality as Codex accounts without consuming model quota.


type: methodology

text: Contract-first TDD. Implementers read the repository and derive bodies. Tool-qualified provider services isolate Codex and Claude while preserving shared UI/storage seams. Fake credentials, subprocesses, and HTTP in automated tests; no live account mutation.


type: intent

text: Close the current partial-parity gap: Claude registry/routing exists, but lifecycle UI is Codex-bound, Claude health has no producer, and Claude limits are never fetched.


type: direction

verbatim: claude code doesnt have any account management, like rename/delete/reauth, like we got for codex. also no health, or account limits. i want the functionality of claude code to be the same.


note: Verified locally with Claude Code 2.1.199: auth login/logout/status exist; auth status is JSON; Claude Code internally fetches GET https://api.anthropic.com/api/oauth/usage; a live schema-only probe returned five_hour, seven_day, optional limits, extra_usage, and spend. Exact OAuth client ID and token endpoint are pinned in w1.t3.


note: No human gates: design choices, base branch, and PR landing policy are resolved. Destructive remove behavior is tested only in temporary registries. Live GTK/account smoke remains post-run and is not a task because run-plan-codex halts shipping on non-committed tasks.


type: task

desc: Provider contracts + Codex adapter. Create provider_services.py and tests/test_provider_services.py. Pin: frozen LifecycleEvent(kind,account,message,error,prompt,session,collision,existing_accounts); AccountLifecycle.add(alias, *, login_hint=None)->Iterator[LifecycleEvent]; reauthenticate(account)->Iterator[LifecycleEvent]; AccountHealthProvider.fetch(account, timeout_secs=10.0)->AccountSnapshot; frozen ProviderServices(tool:AccountRegistryKind,registry,lifecycle,health); ProviderServiceMap.for_tool(AccountRegistryKind|str) and for_account(Account), normalizing strings and rejecting duplicate/missing providers. CodexLifecycleAdapter must translate every current DeviceAuthOperation event field unchanged; ignore login_hint. No GTK/I/O in service map. Tests cover translation and same-slug tool-qualified dispatch.

status: COMMITTED

seat: codex


desc: Transactional registry operations. Modify account_registry.py and tests/test_account_registry.py. Pin AccountRegistry.stage_add(alias)->StagedAccount; StagedAccount exposes slug/account_home and single-use commit()->Account/rollback()->None, with second terminal call raising RuntimeError('staged account already closed'). Commit under account-root lock, reject concurrent final destination, atomic rename + atomic registry write, no incomplete account visible. Claude list reads account_identity.json schema {email,org_id,subscription_type} before legacy claude.json. remove(slug) quarantines directory, atomically updates registry/default/active links, restores on state-write failure; removing default selects first remaining; final removal clears default/active links. Preserve add_dir/Codex migration. Temp-home tests cover concurrency, rollback, reassignment, final removal, and failure restoration.


desc: Claude OAuth usage session. Create claude_oauth.py and tests/test_claude_oauth.py. Pin 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. ClaudeOAuthSession(credentials_path, transport=None, timeout_secs=5.0).get_usage()->dict performs authenticated GET and at most one refresh/retry after 401; no retry for 403/404/schema. Refresh under adjacent lock; re-read after lock and prefer concurrently replaced token. Atomic merge preserves mode, mcpOAuth, unknown keys. Typed credential/auth/protocol/timeout/transport failures. Never expose tokens, headers, identity payloads, or raw request objects. Fake-transport tests only.


desc: Claude lifecycle. Create claude_auth_operation.py and tests/test_claude_auth_operation.py. ClaudeAuthOperation(registry, runner=None) implements AccountLifecycle. Add runs `claude auth login --claudeai`, appending paired `--email <login_hint>` only when non-empty, with target CLAUDE_CONFIG_DIR; require exit 0, credentials file, and bounded `claude auth status --json` loggedIn true. Normalize identity lowercase email+orgId; reject duplicates; atomically persist account_identity.json email/org_id/subscription_type; commit staged account. Reauth locks home, backs up credentials/identity/Claude metadata, requires known identity unchanged, restores all on cancel/failure/mismatch/collision; legacy missing identity accepts unique verified baseline. Emit started/browser-waiting/success/collision/failure/rollback. Never call logout or leak subprocess identity/credentials.


desc: Claude health + limits. Create claude_health_client.py/tests and extend health_client.py, health_store.py, tests/test_health_client.py, tests/test_health_store.py. Pin ClaudeHealthClient.fetch(Account,timeout_secs=10)->AccountSnapshot. Run auth status JSON in account CLAUDE_CONFIG_DIR: loggedIn true=>HealthStatus.OK, explicit false/auth rejection=>BROKEN, launch/timeout/malformed/transient=>UNKNOWN. Add frozen NamedLimit(kind,group,percent,resets_at,active); extend AccountSnapshot backward-compatibly with checked_at, redacted detail, named_limits, typed extra-usage/spend summaries. Map five_hour to primary and seven_day to secondary, clamp 0..100, parse ISO resets to epoch, retain active limits, tolerate null/unknown keys. Store reads old entries and round-trips new. Usage failure after verified auth remains OK with unavailable limits/redacted detail. No model call or terminal /usage scraping.


desc: Provider-aware tray/popup. Modify indicator.py, account_card.py, tests/test_indicator.py, tests/test_account_card.py. Indicator receives ProviderServiceMap; every action resolves by account.tool/tray_key. Render provider Add Account plus per-account Set Default, Reload, Re-authenticate, Rename, Remove for Codex and Claude. Key cards/buttons/snapshots/in-flight state by '<tool>:<slug>'; callbacks carry Account, never bare slug. Refresh All/popup-open schedules all providers and writes separate caches. Merge usage-only failures with prior successful limits/resets while showing current auth/stale timestamp. Disable only active account/action. Rebuild after lifecycle/default changes. Preserve Codex copy, lazy refresh, auto-close. Tests cover same-slug independence and all dispatches.


desc: Redacted live-schema smoke probe. Create scripts/verify_claude_health.py and tests/test_verify_claude_health.py. CLI requires --account-home and never defaults to ~/.claude. Reuse ClaudeHealthClient; print exactly one JSON object with keys status,five_hour_available,seven_day_available,five_hour_reset_valid,seven_day_reset_valid,named_limit_count. Exit 0 only for OK with both limits+resets, 1 otherwise, 2 for usage/arguments. Never output identity, IDs, tokens, paths, payloads, or headers. Tests use fakes and assert schema, exits, explicit-home requirement, redaction; no live HTTP in automated gate.


desc: Composition-root/scheduler/router integration. Modify systray_codex_switcher.py, scheduler.py, tests/test_systray_codex_switcher.py, tests/test_scheduler.py, tests/test_command_router.py. Startup creates Codex ProviderServices (existing registry/health/device-auth adapter) and Claude ProviderServices (Claude registry/auth/health), after both migrations, and injects complete ProviderServiceMap into Indicator. Scheduler keys work by tool-qualified account, stays off GTK. Claude snapshots write claude_health_cache.json and cld consumes it independently from Codex health_cache.json. Preserve lock, rule seeding, fallbacks; unsupported Claude config reports explicit error without corrupting Codex state. Integration tests prove both providers wired/scheduled/cached/routed independently.