You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.jsonmigration_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.