v3.2.5

actix/actix-webv3.2.5Jul 8, 2026by github-actions[bot]

AI Summary

This release introduces coordination locks for placement-partition, environment-variable indirection for pack paths, review-time regression gates, and fixes for stale assertions and review prompt files.

Key Highlights

  • Coord/primary placement-partition lock formalized as the single authority.
  • Doctrine/charter pack paths support environment-variable indirection.
  • Review-time regression gate at `move-task --to for_review`.
  • Post-merge stale-assertion analyzer fixes false-positive storms.

New Features

  • Placement-partition coordination locks
  • Environment-variable indirection for pack paths
  • Review-time regression gates

Full Release Notes

### ✨ 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](../guides/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](../migrations/mission-id-canonical-identity.md).