Contracts

adversarial-evidence-contract.md

Contract: Adversarial Evidence

Every contested finding from an adversarial-squad pass carries a recorded disposition — accepted, changed, or deferred_with_rationale. No contested finding may be silently dropped.

  • Post-spec squad (reviewer-renata / architect-alphonso / planner-priti): dispositions recorded in ../research.md §Adversarial Evidence. All HIGH/MEDIUM findings changed; two deferred_with_rationale (extracted-pack layout → #3022; Windows-CI matrix → out of scope, covered by parametrized unit tests).
  • Post-plan squad: dispositions to be appended to research.md before /spec-kitty.tasks.
  • No new dependency is introduced (hand-rolled parser), so the supply-chain adversarial pass reduces to confirming the null-change + the D6 pin-exact-rc mitigation.

env-expander.md

Contract: Kernel env-expansion seam

src/kernel/env_expand.py — one primitive, two policies. Consumed by charter (below specify_cli) and specify_cli.

API

def expand_env_template(raw: str, *, inject_defaults: bool, environ: Mapping[str,str] | None = None) -> str
class UnresolvedEnvTokenError(ValueError): ...
# kernel/paths.py
def get_packs_root_default() -> Path   # == get_built_in_pack_root().parent

Behavioral guarantees (tests)

  • C-EXP-1: expand_env_template("${SPEC_KITTY_PACKS_ROOT}/built-in/x", inject_defaults=True) with the var UNSET → resolves to str(get_packs_root_default()) + "/built-in/x" (no literal token, no raise).
  • C-EXP-2: same call with inject_defaults=False and var unset → raises UnresolvedEnvTokenError naming the token.
  • C-EXP-3: get_packs_root_default() == get_built_in_pack_root().parent (token names …/packs; resolver returns …/packs/built-in; no double-join).
  • C-EXP-4: org_pack_config._expand_path_template delegates with inject_defaults=False; an org-pack local_path with an unset ${VAR} still raises the existing fail-loud error (behavior byte-preserved).
  • C-EXP-5: kernel module imports nothing from specify_cli/doctrine (arch-gated by test_kernel_no_doctrine_import/test_layer_rules).

kitty-env-loader.md

Contract: .kitty.env pre-import loader

src/specify_cli/bootstrap/env_file.py — invoked before any spec-kitty import.

Behavioral guarantees (tests)

  • C-LDR-1 (precedence): var set in real env, per-repo .kittify/.kitty.env, and home-tier ${SPEC_KITTY_HOME}/.kitty.env → resolved value is the real-env one; with real-env unset, the per-repo one; with both unset, the home one. (Tiers merged {home, repo}, then one os.environ.setdefault.)
  • C-LDR-2 (pre-import): SPEC_KITTY_SYNC_MINIMAL_IMPORT set only in .kitty.env → the import-time-gated behavior (sync/__init__.py:455) observes it; proves seed precedes import specify_cli (__init__.py:36).
  • C-LDR-3 (fail policy): absent file → warn+continue (exit 0); present-but-unreadable env_file → non-zero exit naming the file; malformed line → skipped + debug log, bootstrap survives.
  • C-LDR-4 (locator recursion): a SPEC_KITTY_HOME= line inside .kitty.env → ignored with a warning.
  • C-LDR-5 (single pointer): config.yaml env_file resolved once at bootstrap; a raw config reader never receives an unexpanded ${…} token; the key is outside extra="forbid" blocks.
  • C-LDR-6 (budget): loader is stdlib-only; the completion benchmark shows no regression beyond its noise floor.
  • C-LDR-7 (cross-platform, state-root home): home-tier resolves to the STATE root — %LOCALAPPDATA%\spec-kitty on Windows, ~/.spec-kitty on POSIX (NOT ~/.kittify). The shim reuses ONE stdlib-safe kernel-floor state-root primitive (no 4th resolver); a test asserts the shim's unset-SPEC_KITTY_HOME default equals get_runtime_root().base on both platforms.

provenance-and-channel.md

Contract: Provenance tokens, secret redaction, release channel

Provenance (both carriers, one normalizer)

  • C-PRV-1: fresh charter compile → every built-in catalog source_path is ${SPEC_KITTY_PACKS_ROOT}/built-in/...; 0 absolute paths. Same for agent_profiles_manifest.json.
  • C-PRV-2 (re-bake gate): with SPEC_KITTY_PACKS_ROOT=/some/abs exported, emitted artifacts are byte-identical to the unset case (token stored, never the resolved path).
  • C-PRV-3 (invariance): charter.yaml byte-identical across editable checkout and installed wheel.
  • C-PRV-4 (heal): an existing absolute source_path → rewritten to a token by the heal migration; re-run = 0 changes.
  • C-PRV-5 (leak-check): spec-kitty doctor (dedicated _provenance_doctor.py sibling under cli/commands/doctor.py, NOT runtime/doctor.py) flags any committed absolute built-in path with a heal hint.
  • C-PRV-6 (surgical normalizer, 3 classes): the shared path→token normalizer emits (a) token for built-in-pack paths, (b) repo-relative for in-tree project/org paths, (c) absolute for out-of-tree non-pack paths — replacing ONLY compiler.py:1424/1447 + projection.py:56; a regression asserts mission-template callers (compiler.py:1482/1494) and manifest output_path (manifest.py:112) are byte-unchanged.

Secret redaction (fail-closed allowlist)

  • C-SEC-1: a var NOT on the printable-var allowlist (e.g. SPEC_KITTY_SAAS_TOKEN) never appears by value in doctor/sync status/logs — only its presence.
  • C-SEC-2: .kitty.env matches an ignore rule in both .gitignore and .claudeignore (asserted by an architectural test).

Release channel (consumer slice; interface with #3047)

  • C-CHN-1 (default off): with SPEC_KITTY_PRERELEASE unset and a newer rc on the index, upgrade --agent-check reports the latest stable; no rc advisory.
  • C-CHN-2 (opt-in): with it truthy, the newest PEP 440 pre-release on the same PyPI index the CLI already probes is surfaced; upgrade_command is spec-kitty-cli==<rc> (pinned, no --pre).
  • C-CHN-3 (doctor): doctor reports the active channel.
  • Interface with #3047: producer must publish rc's as PEP 440 pre-releases on that index/scheme; this mission owns only the consumer read.

Migration idempotency

  • C-MIG-1: re-running the heal migration and the provision migration each yields 0 changes.
  • C-MIG-2 (no PACKS_ROOT seed): the provision scaffold never writes SPEC_KITTY_PACKS_ROOT; a regression asserts SPEC_KITTY_TEMPLATE_ROOT still governs asset resolution when the scaffold is present.