This document is the canonical parity contract for nils-codex-cli and nils-gemini-cli after core-crate consolidation into CLI adapters
plus nils-common::provider_runtime.
Both binaries must expose the same top-level command topology:
agentauthdiagconfigprompt-segmentcompletion
Shared help behavior:
--help/helpexit code remains0.- Unknown groups/subcommands return deterministic usage errors (
64) with lane-specific binary prefixes.
Non-canonical command invocations must preserve equal exit semantics between lanes:
- Unknown top-level groups
- Unknown subcommands under canonical groups
Parity requirement: for equivalent invocation shapes above, codex-cli and gemini-cli must return identical exit code classes.
Structure parity is required while schema ids remain provider-specific.
- Auth command family:
- Codex schema id prefix:
codex-cli.auth.v1 - Gemini schema id prefix:
gemini-cli.auth.v1
- Codex schema id prefix:
- Diag command family:
- Codex schema id prefix:
codex-cli.diag.rate-limits.v1 - Gemini schema id prefix:
gemini-cli.diag.rate-limits.v1
- Codex schema id prefix:
Compatibility rules:
- Required fields and envelope shape remain aligned across both lanes.
- Provider labels and schema ids remain lane-specific.
- No secret-bearing fields are emitted in error envelopes.
- Provider-specific env key names, default model values, path precedence, and dangerous-exec command shape are configured via provider profiles.
- Provider-specific exec flags may diverge when the upstream provider CLI capabilities differ (for example Codex supports optional
--ephemeral; Gemini currently has no equivalent). - Shared runtime primitives (
auth/json/jwt/error/path/config/execlogic) stay innils-common::provider_runtime. - Human output text and exit semantics stay stable for existing commands.
--refreshremains the only blocking refresh path in both lanes. The default prompt path must keep printing the cached line first and keep appending the lane-specific stale suffix when the cache is stale.- Current gap:
gemini-clistill callsrefresh_blocking()from the default stale/missing-cache path, whilecodex-clienqueues a best-effort background refresh and returns immediately. - Current Gemini missing-cache behavior is part of the same gap: no cache entry falls through to the same inline
refresh_blocking()path instead of a detached enqueue. - When porting Codex semantics to Gemini, preserve these guardrails:
- Best-effort background spawn via the current executable;
current_exeor spawn failures stay silent no-ops. - Minimum refresh interval throttling via
*_PROMPT_SEGMENT_REFRESH_MIN_SECONDS. - Last-attempt markers are written before
spawn()so prompt storms still throttle after child-launch failure. - Lock contention stays a no-op for prompt rendering, with stale-lock recovery via
*_PROMPT_SEGMENT_LOCK_STALE_SECONDS. - Lock and throttle files stay cache-adjacent (
<cache-stem>.refresh.lockand<cache-stem>.refresh.at). - Cache format, stale suffix rendering, and exit codes stay unchanged.
- Best-effort background spawn via the current executable;
- Existing Gemini tests that must change when the default path stops refreshing inline:
crates/gemini-cli/tests/prompt_segment_refresh.rs:prompt_segment_stale_cached_entry_refreshes_on_runcrates/gemini-cli/tests/prompt_segment_cached.rs:prompt_segment_stale_cache_with_failed_refresh_returns_0crates/gemini-cli/tests/prompt_segment_cached.rs:prompt_segment_missing_cache_root_is_treated_as_no_cache
- Coverage still missing after the port:
- A default missing-cache test that asserts background enqueue instead of inline fetch.
- Lock/min-interval/stale-lock recovery coverage that matches the existing
codex-cliprompt-segment contract.
crates/gemini-cli/tests/prompt_segment_refresh.rs:prompt_segment_refresh_updates_cacheremains the blocking-path anchor and should stay unchanged by the background-refresh rewrite.
- Shared async invariants:
--asyncstill rejects--one-lineand positional secret args with exit64.--async --cached -cremains invalid; clear-cache failures remain exit1.- Async JSON keeps
schema_version,command="diag rate-limits",mode="async", top-levelok, and a fullresultsarray. - Collection results stay deterministically sorted by
name, never by completion order. - Command-level secret discovery failures remain top-level
error.code="secret-discovery-failed". - Per-secret failures in async JSON keep the full
resultsarray and return exit1. --jobsremains non-fatal on zero/invalid input; later concurrency work must not introduce a newinvalid --jobsusage error.- Missing-access-token fallback remains explicit: successful fallback emits
source="cache-fallback"withok=true; otherwise the result stays an error.
- JSON error-path invariants:
codex-clicommand failures emiterror { code, message, details? }; per-result failures use the same nestederrorenvelope and may carrydetails.gemini-clicommand failures emit the same top-levelerror { code, message, details? }, but per-result failures currently serialize a nestederror { code, message }object without per-resultdetails.- Stable per-secret async JSON error codes are
missing-access-token,request-failed,invalid-usage-payload, andcache-read-failed.
- Crate-specific differences to preserve during concurrency refactors:
codex-clitext async already uses bounded worker threads and honors--jobs, defaulting zero/invalid values to5; its watch mode is codex-only and still requires--async.codex-cliasync JSON is still sequential today; Sprint 4 should change only the execution strategy, not envelopes, ordering, fallback, or return codes.codex-cliasync JSON already honors--cached.gemini-clitext async is sequential today because it delegates torun_all_mode(...);jobsexists inRateLimitsOptionsbut is unused.gemini-cliasync JSON is sequential and keeps amissing-access-token -> cache-fallbackspecial case.gemini-clitext async honors--cached, but async JSON currently does not branch onargs.cachedand still fetches from the network. Treat that difference as part of the current observable contract until an intentional contract update says otherwise.
cargo test -p nils-gemini-cli --test prompt_segment_cached --test prompt_segment_refreshcargo test -p nils-gemini-cli --test rate_limits_async --test rate_limits_networkcargo test -p nils-codex-cli --test prompt_segment_cached --test prompt_segment_refreshcargo test -p nils-codex-cli --test rate_limits_async
cargo test -p nils-codex-cli --test parity_oraclecargo test -p nils-gemini-cli --test parity_oraclecargo test -p nils-codex-cli --test runtime_auth_contract --test runtime_error_contract --test runtime_exec_contract --test runtime_paths_config_contractcargo test -p nils-gemini-cli --test runtime_auth_contract --test runtime_error_contract --test runtime_exec_contract --test runtime_paths_config_contract