Research: CLI Startup Readiness Coordinator Skeleton
This mission has no novel technology research surface; all technical choices are forced by the existing codebase. This document captures the design-rationale decisions, alternatives considered, and references.
Decision 1: Coordinator owns the _render_nag_if_needed call site (inside vs. parallel)
Decision: The coordinator's evaluate_readiness function calls _render_nag_if_needed(ctx) directly. The inline call to _render_nag_if_needed from callback() in helpers.py is removed and replaced by a single call to evaluate_readiness(ctx).
Rationale:
- Single source of ownership: there is exactly one place that decides when the nag fires (the coordinator) and exactly one function that renders it (
_render_nag_if_needed). - Future WS3 work (snooze cadence, "Always keep me up to date", "Not now", "Never ask again") plugs in by extending the coordinator's call to
_render_nag_if_needed, NOT by adding a parallel render path. - Avoids duplicate side effects (the nag cache update). If the coordinator shadowed the nag, the next mission would have to reason about two paths that both call into NagCache.
Alternatives considered:
- Parallel render: coordinator runs its own decision logic and emits its own rendering. Rejected — it would duplicate the suppression logic and the cache update.
- Coordinator gates the nag: coordinator checks suppression first, then calls
_render_nag_if_neededonly if not suppressed. Rejected — duplicates the gate that_render_nag_if_neededalready does internally; any drift between the two checks becomes a silent bug. By calling unconditionally, we keep_render_nag_if_neededas the sole authority on whether the nag actually fires.
Decision 2: OutputPolicy is a 3-bucket StrEnum (vs. a single boolean)
Decision: OutputPolicy has three values: INTERACTIVE, NON_INTERACTIVE, MACHINE_OUTPUT.
Rationale:
- WS2 (auth readiness) needs to distinguish "non-TTY but human is at the keyboard reading the script output later" from "JSON pipe — never put any human text on stdout". Today both are "suppressed" booleans; WS2 will render to stderr in the former case and emit nothing in the latter.
- WS3 (upgrade UX) needs the same distinction for the "Always keep me up to date" / "Not now" prompts: prompts are valid in interactive mode, optional in non-interactive mode (stderr line only), forbidden in machine-output mode.
- Storing the bucket on
ReadinessResultonce per invocation lets every subcommand consult one source of truth.
Alternatives considered:
- Boolean
suppressed: same as today, but every downstream consumer would re-derive the 3-bucket distinction. Rejected — defeats the purpose of having a coordinator. - 2-bucket (interactive vs. not): rejected for the same WS3 reason.
- 4-bucket (
INTERACTIVE,NON_INTERACTIVE_TTY,NON_INTERACTIVE_PIPE,MACHINE_OUTPUT): rejected — over-engineered for this mission; can be widened later under the WS2/WS3 evolution.
Decision 3: _auth_recovery import is module-scope on coordinator.py
Decision: from specify_cli.cli.commands._auth_recovery import detect_logged_out_with_connected_teamspace # noqa: F401 lives at module scope of coordinator.py.
Rationale:
- mypy --strict can then verify the symbol still exists and has the expected signature. If a future refactor renames it, the type-checker breaks the coordinator import immediately.
- Provides a grep target: future WS2 implementer searches for
# WS2: auth probe wiringand finds the exact import + the exact call-site location. - Import cost is acceptable:
_auth_recoveryonly does stdlib imports at top level. (Heavy imports —TokenManager, sync routing,_auth_login— are alllazyinside_auth_recovery's own functions.) Coordinator overhead bounded by NFR-001 (≤1ms p50 when hosted mode is disabled).
Alternatives considered:
- Function-scope import: defers import cost but loses mypy verification of the symbol's existence. Rejected — type-check loss outweighs the marginal startup-cost win.
- Don't import at all: rejected — WS2 hand-off becomes less obvious; the seam isn't typed.
Decision 4: _render_nag_if_needed stays in specify_cli.cli.helpers
Decision: Do NOT move _render_nag_if_needed into the readiness package. It remains exported from specify_cli.cli.helpers (its current __all__).
Rationale:
- C-005: external dependents (including out-of-tree code we cannot see) may import from
specify_cli.cli.helpers. Moving the function would break them. - Coordinator imports
_render_nag_if_neededlazily inside_invoke_nagto avoid a circular import (coordinator ← helpers ← coordinator would form a cycle if coordinator is imported by helpers' callback).
Alternatives considered:
- Move to
readiness/nag.pyand re-export fromhelpers: doable but increases diff scope. Rejected — out of scope per C-009.
Decision 5: ctx.obj keying is the dict key "readiness"
Decision: When ctx.obj is a dict (the existing convention), the coordinator stores its result under the key "readiness". Sibling key: "compat_plan_result" written by _render_nag_if_needed.
Rationale:
- Existing convention in
_render_nag_if_neededalready usesctx.objas a dict. Following the same pattern minimizes the cognitive load on reviewers. - Distinct key prevents accidental overwrites between the two writers.
- A short, descriptive key keeps the public surface clear.
Alternatives considered:
- Attribute on
ctx: typer'sContextdoesn't formally support arbitrary attributes; relying on attribute storage would be fragile. - Module-level cache keyed by
id(ctx): rejected — leaks across CLI invocations in the same process (which happens in tests).
Decision 6: Exception handling — coordinator never raises
Decision: Any exception inside _evaluate_uncached is caught in evaluate_readiness and replaced with _NOOP_DISABLED. The CLI cannot crash because of readiness logic.
Rationale:
- Defense in depth. The existing
_render_nag_if_neededalready swallows its own exceptions; preserving that posture at the coordinator level means a single broken inner function cannot ever break the CLI. - Subcommands consuming
get_readiness(ctx)always see a validReadinessResult. Notry/exceptboilerplate at every call site.
Alternatives considered:
- Let exceptions propagate: rejected — would degrade
--help/--versionUX if the readiness logic broke.
References
- Existing helpers:
/Users/robert/spec-kitty-dev/teamspace-readiness-plan-20260522-115958-YQygKW/spec-kitty/src/specify_cli/cli/helpers.py - Existing rollout gate:
/Users/robert/spec-kitty-dev/teamspace-readiness-plan-20260522-115958-YQygKW/spec-kitty/src/specify_cli/saas/rollout.py - Existing auth recovery:
/Users/robert/spec-kitty-dev/teamspace-readiness-plan-20260522-115958-YQygKW/spec-kitty/src/specify_cli/cli/commands/_auth_recovery.py - Existing CI-determinism tests:
/Users/robert/spec-kitty-dev/teamspace-readiness-plan-20260522-115958-YQygKW/spec-kitty/tests/cli_gate/test_ci_determinism.py - Tracking issue: Priivacy-ai/spec-kitty#1093
- Program plan:
/Users/robert/spec-kitty-dev/teamspace-readiness-plan-20260522-115958-YQygKW/start-me-start-here.md