Research: OSS Fork Packaging Hooks

Decision: Shared specify_cli.distribution package owns resolvers

Rationale: Session-presence, version_utils, compat planner/remediation, and the version banner all need the same package-name and provider resolution. Owning resolvers under session_presence/ (as sibling WIP does) creates an inverted dependency (compat → session_presence) and duplicates homes when Phase 2 lands.

Alternatives considered:

  • Keep resolvers in session_presence/upgrade_provider.py — rejected (layering).
  • Put resolvers in compat/ — rejected (version_utils / banner would import compat for identity).

Decision: Reuse LatestVersionProvider protocol; do not invent a second one

Rationale: Spec FR assumes zero-arg constructible providers with non-raising get_latest. Existing protocol + LatestVersionResult already encode this; stock PyPIProvider remains the fallback.

Alternatives considered:

  • New async provider API — rejected (call sites are sync).
  • Callable-only entry points without protocol — rejected (harder to test/type).

Decision: LatestVersionResult.source gains "simple_index" (or equivalent)

Rationale: Mapping private-index success to "pypi" would mislabel telemetry/debug. Extending the Literal is a narrow, testable change. Error tokens stay the fixed vocabulary.

Alternatives considered:

  • Keep source="pypi" for all successful HTTP lookups — rejected (dishonest for private index).
  • Separate result type for simple index — rejected (call-site churn).

Decision: Multi-provider selection — env disambiguation, else deterministic first

Rationale: Align with sibling WIP: SPEC_KITTY_UPGRADE_PROVIDER=<name> when multiple registered; else sorted-by-name first; never invent unregistered names; load failures fall back to PyPIProvider.

Alternatives considered:

  • Fail hard if multiple and env unset — rejected (breaks accidental dual registration in editable+wheel).
  • Require exactly one always — rejected (FR-004 allows multi + env).

Decision: CHK028 length → 512 (shared constant)

Rationale: Index URL flags push remediation strings past 128. Spec allows 512 or dynamic length; fixed 512 is simpler and keeps character class unchanged ([A-Za-z0-9 .\-+_/=:]).

Alternatives considered:

  • Dynamic max(len(composed), 128) — more complex, little security gain.
  • Allow ?&= in character class — broader attack surface for display strings; avoid unless required.

Decision: Supersede sibling mission pluggable-upgrade-check-provider-01KXZDMC for this scope

Rationale: Coord worktree already contains a working resolve_upgrade_provider prototype. Port the resolution algorithm into distribution/upgrade_provider.py, wire call sites here, and treat the sibling as abandoned/superseded rather than landing two overlapping features.

Alternatives considered:

  • Finish sibling first then add profile mission — rejected (user asked for all three phases in one mission).
  • Duplicate code temporarily — rejected (canonical-source principle).

Decision: Profile resolution precedence

Rationale: 1. If spec_kitty.distribution_profile registers a profile object → use it. 2. Else synthesize a minimal profile from cli_package + upgrade_provider resolvers (Phase 1 aliases). 3. Else stock defaults (package_name="spec-kitty-cli", public PyPI provider, notifier enabled, no index URLs, default freshness).

Alternatives considered:

  • Profile-only (drop Phase 1 groups) — rejected (FR-017).
  • Merge all groups into one required profile — rejected (raises packager bar for session-presence-only forks).

Open questions resolved by operator signal

User requested automatic progression with confirmed Intent Summary covering all three phases and feature-branch PR path — no remaining product NEEDS CLARIFICATION markers.