github Priivacy-ai/spec-kitty v3.2.5

3 hours ago

✨ Added

  • Coord/primary placement-partition lock — one topology-aware seam owns where
    every mission artifact is stored and read (#1716, #1878/G2).
    Formalizes
    PlacementSeam.write_target(kind) / read_dir(kind) as the single thin
    authority over the existing resolve_action_context SSOT and routes every
    remaining write site through it — fail-closed via PlacementResolutionRequired
    (a real resolution failure raises rather than silently committing to the
    operator's checkout). An architectural ratchet forbids the
    CommitTarget(ref=<checkout>) grammar (a self-test proves it bites), and an
    end-to-end characterization test locks the behaviour. Canonical partition:
    coord branch = lifecycle (status/notes/trace/issue-matrix/move-task); primary
    = stable planning (spec/plan/WP outlines); no-coordination topology → all
    primary. RETROSPECTIVE delegates to the existing resolve_retrospective_home
    (no second authority). Also fixes #2091 (an empty mid8 no longer builds a
    malformed kitty/mission-<slug>- coordination branch → git worktree add
    exit-128; the CoordinationWorkspace composition seam fail-loud-guards it) and
    #2250 (a never-coordinated mission no longer reports
    COORDINATION_BRANCH_DELETED). The read side is now locked too (#1716
    closeout):
    every remaining coordination-surface read — the kind-blind
    resolve_feature_dir_for_mission sweep (#2453) and the inline meta.json
    reads (#2100) — routes through the same topology-aware seam kind-correctly, so
    spec-kitty accept no longer reads a stale -coord acceptance matrix (#2404)
    and planning artifacts always resolve to their canonical surface regardless of
    the working directory. A non-vacuous architectural ratchet keeps the routed
    read classes from silently regrowing. Closes epic #1716 and #2453 / #2100
    / #2088 / #2404.
  • Doctrine/charter pack paths support environment-variable indirection
    (#2437).
    Pack-location fields (e.g. an org-pack's local_path) previously
    had to be hardcoded absolute paths, so a shared, committed .kittify config
    resolved only on the machine of the developer who wrote it and broke for every
    teammate and CI runner. Pack-path templates now expand ${VAR}/$VAR and ~
    at read time (via os.path.expandvars + expanduser), so a portable form
    like ${SPEC_KITTY_PACK_HOME}/acme-doctrine can be committed once and each
    environment resolves it against its own base. Expansion is fail-closed: an
    unset/empty env token raises rather than silently collapsing to a wrong path
    (e.g. ${UNSET}/acme → repo root). Purely additive — literal absolute paths
    keep working.
  • Review-time regression gate at move-task --to for_review (#572). When a
    work package moves to for_review, Spec Kitty now auto-scopes the CI shards
    that cover the WP's changed files and re-runs them, so a WP that broke a
    shared contract pinned by a test outside its owned_files is caught at
    review time instead of only at merge. Warn-only by default; opt in to a hard
    block with review.fail_on_pre_review_regression (enforced only when
    review.test_command is set; move-task --force overrides), and override the
    scope per-WP via frontmatter pre_review_test_scope. See
    review-gates.md.
  • spec-kitty review --check-residual + environment-parity preflight
    (#2283).
    The new --check-residual flag runs CI's always-on
    unit-contract-residual -m selection over tests/ locally — the -m
    expression is read live from .github/workflows/ci-quality.yml, so a
    previously CI-only marker-orphan failure can be reproduced before pushing. The
    spec-kitty review preflight also detects local-vs-CI environment skew
    (installed package versions diverging from uv.lock, plus typer/click
    lock-parity), warning by default and failing closed under
    SPEC_KITTY_ENV_SKEW_FAIL_CLOSED.

🐛 Fixed

  • Post-merge stale-assertion analyzer stops false-positive storms on
    behavior-preserving refactors (#2031, #2343).
    A relocation/re-export (a
    WP05 extraction produced 180 false findings and tripped the drift ceiling)
    and generic-literal noise both flagged as stale assertions. The analyzer now
    (a) suppresses a removed identifier only when the origin file's module-level
    head
    still re-exports/imports it (keyed on head-importability — not
    bare-name-anywhere, so a genuine deletion of a common name like run/main
    is still flagged; a nested import parse does not mask a real def parse()
    deletion), and (b) suppresses literal-only noise by genuineness (a pinned
    generic-token set / all-punctuation), never by length (a short literal like
    "E001" can be assert-critical). Both paths suppress rather than
    info-downgrade, so render surfaces and the FP ceiling are unchanged.
  • Review-prompt files no longer accumulate unbounded; coverage-allowlist
    repointed off a removed module (#2439, #2443).

    write_review_prompt_with_metadata() now prunes review-prompt files to a
    newest-preserving cap after each write — fail-safe, and never pruning the
    current invocation's own file — closing the LC-7 retention residual of #1842.
    Separately, the diff-coverage critical-path --include allowlist still
    referenced the stale src/specify_cli/core/mission_detection.py (removed in a
    rename); it is repointed to src/specify_cli/lanes/branch_naming.py in both
    the CI workflow and its test authority in lockstep, with a glob-aware existence
    guard so a phantom allowlist entry reds instead of silently covering nothing.
  • spec-kitty agent mission finalize-tasks accepts glob-only owned_files
    (#2446).
    A work package whose owned_files used a filename glob (e.g.
    src/foo/*.py) with no explicit authoritative_surface was rejected because
    infer_authoritative_surface produced an invalid src/foo/*.py/ path. It now
    reduces a glob-bearing final path segment to its directory, so glob-only
    ownership infers a valid authoritative surface. (Fix landing via PR #2454.)
  • A retired skill's projected files are now drained by surface repair instead
    of orphaned forever (#2409).
    When a skill was retired from the registry,
    spec-kitty upgrade --project could not reconcile its leftover projections:
    the repair path warned Cannot repair … skill not found in registry on every
    subsequent upgrade and left the files in place (committed, where the repo
    tracks skill surfaces; a dangling symlink, where the global root had dropped
    the skill). Repair now reconciles retirements manifest-driven: entries in
    .kittify/skills-manifest.json whose skill no longer exists in a live
    registry have their projected files removed — symlinks unlinked, hash-clean
    copies deleted, user-modified copies archived to
    .kittify/.migration-backup/agent-skills/ instead of deleted — emptied skill
    dirs pruned, and the manifest entries dropped, silencing the repeat warning.
    Only ledger-recorded paths are ever touched (a user-authored skill sharing
    the projection root is never scanned), and an empty registry never
    retires anything (a broken canonical source must not mass-drain the
    manifest).
  • Test-session state leaks closed: per-mission prompt-tmp namespace +
    workspace-context tombstone (#1842, #2032).
    Runtime prompt files are now
    written under a per-repo/per-mission namespaced temp directory
    (<tmp>/spec-kitty-prompts/<repo-id>/…) instead of a shared flat /tmp
    path, and a lane's workspace-context sidecar is now tombstoned when its
    worktree is torn down at merge (or when the lane is canceled) — clearing the
    stale-context and prompt-litter residue flagged by the #1931 /tmp-hygiene
    audit. A session-scoped pytest reaper (controller-gated, run-uid-scoped, and
    never touching the real ~/.spec-kitty) now fails the suite if a test leaks
    repo-root residue, so new leaks are caught at their source.
  • Dashboard: PR-bound missions planned on a feature branch are visible again
    (#2430).
    The dashboard scanner resolved ONE directory per mission,
    coord-worktree-first — but under coordination topology the two partitions
    live on different surfaces: spec-commit lands planning artifacts
    (spec.md/plan.md/tasks.md/tasks//meta.json) on the primary
    surface, while the live event log lives on the coordination branch. The
    scanned -coord copy therefore held only status writes, so the legacy
    existence filter (numeric prefix or tasks/ present) silently dropped any
    in-flight post-083 mission from /api/features, and artifact viewers/kanban
    read the wrong surface. The scanner now splits the read per partition —
    planning artifacts primary-first through the same kind-aware read seam the
    #2331 identity fix uses (resolve_planning_read_dir), live lane state from
    the gather-resolved coord surface — so an in-flight coordination mission
    lists with its real spec/plan/tasks AND live kanban lanes. Same
    coord-shadows-primary class as #2331; the accept-side sibling is #2404.
  • Session banner no longer recommends a downgrade (#2413). The
    session-presence health check decided "upgrade-available" with a bare
    inequality (avail != current), so any machine whose installed CLI was
    newer than the cached PyPI latest — a fresh release not yet in the 1-hour
    cache, or an rc/dev install — got "⚠ Upgrade available: 3.2.2" while running
    3.2.4. Both health branches now use packaging.Version ordering
    (avail > current); unparseable versions are treated as no-upgrade so
    session start never breaks on a weird cache value.
  • The shared Agent-Skills projection root and the per-machine skill install
    ledger are now gitignored (#2412).
    The skills installer projects global
    canonical skills into .agents/skills/ (codex/vibe/pi/letta) preferring
    absolute symlinks into the user-global root — machine-local content by
    construction — but unlike .claude/ and the other agent dirs (gitignored
    wholesale at init), bare .agents/ was never covered by any init path,
    registry entry, or migration, so the /Users/<name>/... symlink blobs were
    committable (and the upgrade auto-commit would land them automatically).
    .kittify/skills-manifest.json (per-machine timestamps/hashes/delivery
    modes) had the same gap. Both are now registered IGNORED state surfaces
    (fresh spec-kitty init gitignores them, and spec-kitty doctor's
    present-but-not-gitignored warning covers them), and a sibling backfill
    migration (3.2.5_agents_skills_gitignore_backfill) adds the entries on
    spec-kitty upgrade for already-initialised projects. The symlink-vs-copy
    delivery question (ask 3 of #2412) is tracked separately on the issue.
  • spec-kitty upgrade now commits worktree churn on every checkout, not just
    main (#2392, closes #2385/#1873, pins #2105).
    The upgrade auto-commit
    captured a porcelain baseline for the main checkout only, so migration writes
    it made in sibling worktrees were left uncommitted — dirtying them and
    blocking coordination-topology merges (NFR-002). The porcelain-baseline commit
    routine is extracted into a canonical per-checkout seam
    (upgrade/autocommit.py::commit_touched_checkout); the migration runner now
    snapshots each worktree before its writes and commits only that new churn on
    the worktree's own branch (in-flight WP edits are never swept in;
    manual_review_required migrations skip the commit with a warning;
    detached-HEAD skips instead of guessing a ref; --dry-run stays a strict
    no-op). #1873's self-healing path is restored (metadata synthesized from
    None is marked dirty so it's saved even when detected version == target).
  • Legacy-topology warning no longer over-fires on intentional
    coordination-less missions (#2351).
    The once-per-mission warning keyed on
    coordination_branch absence, so it fired for genuinely deliberate
    single_branch/lanes shapes (#2218) as well as truly pre-SSOT missions. A
    new warning-only classifier gates just the emit on the canonical stored
    MissionTopology + flattened flag (warn iff coordination branch absent AND
    stored topology null AND not flattened; malformed still warns); the shared
    _is_legacy_mission predicate that drives worktree routing and write-contract
    selection is left byte-for-byte unchanged.
  • .kittify/migrations/ and .kittify/logs/ are gitignored and backfilled
    (#2384, completes the #2369 sweep).
    Two more generated .kittify/ subtrees
    (mission-state repair manifests + quarantine backups; orchestrator per-WP
    logs) were neither IGNORED state surfaces nor backfilled, so they dirtied the
    tree and failed accept's git-dirty check. Both are now registered IGNORED
    local-runtime surfaces (fresh init gitignores them) with a sibling
    backfill migration for already-initialised projects on upgrade.
  • saas_client no longer defaults to a hardcoded api.spec-kitty.io; it
    fails closed (#2248).
    load_auth_context fell back to a baked-in SaaS URL
    when neither SPEC_KITTY_SAAS_URL nor .kittify/saas-auth.json supplied one,
    risking silently pointing the client at the wrong server. With the #2146
    target-authority decision in place, the default is removed and a no-URL
    resolution now raises SaasAuthError (caught where the no-token path already
    was).
  • Sync runtime callers route through the canonical resolved target authority
    (#2146).
    The tracker client, sharing client, and background sync read the
    raw SyncConfig.get_server_url() (config-only, hardcoded default) instead of
    resolve_runtime_target().resolved_server_url, so with SPEC_KITTY_SAAS_URL
    set, auth/readiness hit the env target while these surfaces silently posted to
    the config target (the SC-008 split-brain). All three now resolve the same
    target as auth/readiness, and sync status/sync doctor report the resolved
    URL. Separately, sync server <url> now accepts loopback HTTP
    (http://localhost/127.0.0.1/::1) for the documented local-Docker-SaaS
    dev workflow while still requiring HTTPS for remote hosts.
  • compat-planner contract checks are revived (#2419). Two test surfaces
    validated upgrade/render_json payloads against the committed
    compat-planner.json contract but silently no-op'd in CI since 2026-04-27
    (each resolved the contract via a maintainer-worktree-only path, so it was
    None and validation was skipped). Both now anchor on the repo root, load
    unconditionally, and fail hard on a missing/unreadable contract — which caught
    one real drift (a migration description trimmed to satisfy maxLength).
  • migration_id contract pattern widened to admit the dotted convention
    (#2339).
    The compat-planner.json migration_id pattern
    (^[a-z0-9_]{1,128}$) rejected 83 of 89 real dotted ids (e.g.
    3.2.0rc45_...) — a contract-authoring bug that let #2339-class drift slip
    through unnoticed. Widened to ^[a-z0-9_.]{1,128}$ (a backward-compatible
    widening; no shipped migration_id renamed, so persisted per-project ledgers
    keep validating).

♻️ Changed

  • Internal: version-comparison primitives consolidated into one canonical module (#2417, landing via PR #2414).
    Landing the #2413 fix surfaced three independent "is version A newer than
    version B" implementations that had drifted apart in edge-case handling:
    cli/commands/upgrade.py::_version_is_newer (caught the broad Exception
    instead of the specific parse failure), core/upgrade_probe.py::_classify's
    inline comparison, and the _upgrade_is_available helper #2413 had just
    added to session_presence/manager.py. All three now delegate to a new
    pure module, specify_cli.core.version_compare
    (try_parse_version / is_version_newer), with no CLI/click/typer
    imports so it is safe to import from any layer; the two duplicate boolean
    helpers are deleted outright, no aliases left behind. No user-facing
    behavior change.
  • Internal: CI-topology census freshness gate is now LOC-insensitive
    (#2416).
    The gate compared each worklist entry's exact line count and an
    LOC-derived sort order, so any PR that shifted the line count of a worklist
    directory went red regardless of touching CI routing — a maintenance tax two
    PRs paid inside 24h. It now checks membership + committed routing plan only
    (an order/LOC-insensitive index at the shared derivation), fixing both the
    pytest gate and the --verify-census CLI by construction. Zero src changes.
  • Internal: test-suite /tmp hygiene sweep (#1842, epic #1931). 97
    grandfathered test files were converted off literal /tmp paths to
    tmp_path/sentinels, and the empty-/tmp ratchet was flipped to a
    self-consistent hard gate so new literal-/tmp leaks fail fast. Also fixed a
    test-only --output= parser leak that could write an invalid Windows filename
    (breaking the Windows critical CI job), with a new arch guard blocking
    Windows-illegal names and shell-expansion-leak telltales across tracked files
    (#2169).

💥 Breaking Changes

  • Pre-3.2.x legacy (meta-less) missions are no longer supported for
    coordination operations (#2091 / #2462).
    A mission with no resolvable
    mission identity — no mission_id/mid8 in meta.json, the pre-3.2.x
    mission-identity model — can no longer drive coordination operations (status
    transitions, move-task, review/merge coordination writes). Such a mission
    now fails loud with a customer-actionable error instead of silently
    composing a malformed kitty/mission-<slug>- ref (the old #2091 defect that
    surfaced only as an opaque git worktree add exit-128). This intentionally
    drops the dual-era legacy-bridge fallback: legacy missions are dwindling and
    the dual-support path was a persistent source of split-brain routing bugs.
    Migration: run spec-kitty migrate backfill-identity to mint a ULID
    mission_id (and derive its mid8) for any affected mission — audit first
    with spec-kitty doctor identity. See
    mission-id-canonical-identity.md.

Don't miss a new spec-kitty release

NewReleases is sending notifications on new releases.