github Priivacy-ai/spec-kitty v3.2.4

3 hours ago

Spec Kitty 3.2.4 is a reliability-and-trust release. It fixes a batch of
everyday mission-lifecycle friction points, closes a real gap in the bulk-edit
safety net, makes SaaS sync reporting honest, and — behind the scenes —
reshapes the CI pipeline and decomposes a large internal command module for
faster feedback and lower regression risk going forward.

  • Smoother day-to-day mission lifecycle. A batch of guard/gate fixes
    removes friction from the implement-review-accept loop: a subtask guard no
    longer misattributes a later work package's unchecked boxes to an earlier
    one, a mission fully implemented by spec-kitty-orchestrator can now pass
    spec-kitty accept (two false-positive blockers removed), move-task --to for_review recovers a killed implementer's uncommitted work instead of
    dead-ending, closing a mission now reliably commits its auto-captured
    retrospective, the dashboard no longer hides an in-flight coordination
    mission behind a synthetic "orphan" entry, and a stale .kittify/derived/
    view or Op-index cache file can no longer dirty git status or block
    accept. map-requirements also now explains exactly why a requirement
    reference is stale instead of looking like data corruption.
  • Mission-state repair no longer risks data loss. doctor mission-state --fix (and the automatic repair spec-kitty upgrade runs) previously could
    empty a healthy mission's event log of its canonical lifecycle history; it
    now preserves every reader-canonical event class and anchors on the primary
    checkout so repairs actually take effect.
  • Bulk-edit safety net closed. The occurrence_map.yaml gate that blocks
    an incomplete bulk-edit classification at finalize-tasks now covers both
    finalize-tasks command surfaces, not just one — closing a path where a
    bulk-edit mission could slip through with an inadmissible occurrence map.
  • Honest SaaS sync reporting. sync opt-in no longer implies remote
    enablement it didn't perform, and sync status --check --json reports real
    (or honestly unknown) remote/import state instead of staying silent.
  • New orchestrator-api capability. A read-only resolve-workspace
    command lets an external orchestrator recover a work package's lane
    workspace without accidentally re-triggering a lifecycle transition.

Behind the scenes: the CI pipeline was reshaped — path-filtered job groups, a
split core-misc shard, and an always-on, de-serialized architectural-adversarial
pole — so most PRs get faster, more targeted feedback without losing any
coverage; and the sprawling agent tasks command module was decomposed into
small, independently-tested, behavior-preserving pieces. Neither changes any
user-visible behavior, but both reduce the odds of the next regression and
speed up how fast we catch one.

💥 Breaking Changes

  • Deprecated compatibility shim packages removed (mission unshim-wave2-01KWMCAX, #2291 / #2290 / #2326 under #1797). The following re-export shim import paths are deleted — code that imported them must switch to the canonical path:

    • specify_cli.nextruntime.next (the canonical spec-kitty next runtime/control-loop package; antecedent #612)
    • specify_cli.glossaryglossary (antecedent #613)
    • specify_cli.charter_lintspecify_cli.charter_runtime.lint
    • specify_cli.charter_freshnessspecify_cli.charter_runtime.freshness
    • specify_cli.charter_preflightspecify_cli.charter_runtime.preflight

    All in-tree callers were re-pointed to the canonical modules before deletion. The shim registry (docs/migrations/shim-registry.yaml, the file read by spec-kitty doctor shim-registry) is drained to shims: [], and the ownership manifest (docs/architecture/05_ownership_manifest.yaml) mirrors the drain for these slices. The user-facing CLI surface is unchanged — spec-kitty next and spec-kitty charter lint/preflight/freshness behave identically. No version bump accompanies this entry: src/specify_cli/__init__.py is untouched by the mission (verified against the mission lane history), so the public CLI package version is unaffected; only internal deprecated import paths are removed.

♻️ Changed

  • CI health: charter-path doc hotfix + arch-adversarial matrix shard (mission ci-health-charter-path-and-arch-shard-01KWRTB2, closes #2397). Two independent CI-health fixes bundled by operator decision: (a) docs/guides/contributing.md still published the retired legacy charter path memory/charter.md, redding fast-tests-docs on every open PR — replaced with the canonical .kittify/charter/charter.md; (b) the arch-adversarial job (already de-serialized from the core-misc critical path by the CI-topology-shrink mission above) remained a single unsharded ~14.4-min bottleneck — matrix-sharded into always-on, group-less shards (same pattern as fast-tests-core-misc), still running on 100% of source changes with no test dropped or double-counted across shards, dropping the slowest shard below the ~13.6-min sub-target.
  • One canonical MissionCreated payload builder (#2270, PR #2398). Three independent code paths derived MissionCreated defaults and had drifted apart: core/mission_creation.py, a verbatim-duplicate helper in sync/emitter.py, and a third, divergent inline fallback in status/lifecycle_events.py (raw slug vs. titleized friendly_name, absent vs. now created_at, differing None-field wire shape). A new pure CORE module, core/mission_payload.py::build_mission_created_payload, is now the single source; both the local lifecycle-event path and the SaaS EventEmitter route through it, preserving the CORE↛INTEGRATION boundary (no sync import in core). Behavioral note: the local mission log's default friendly_name (when not passed explicitly) now titleizes the slug instead of using it raw, matching what the SaaS emitter already produced.
  • CI topology shrink + shard split + always-on architectural pole (mission ci-topology-shrink-01KWQAVX, #2378 / #1933 / #2383 under #1931). The ci-quality.yml PR pipeline is reshaped so a single-area PR runs only its focused shard(s) plus the always-on gates, not a full-matrix run:
    • Group-side shrink (#1933). The previously-unmapped src/specify_cli/* directories are folded into six named composite dorny/paths-filter groups (auth_audit_git, lifecycle, agent_surface, closeout, governance, platform), each registered atomically across all five surfaces (filters block, changes.outputs.*, the unmatched enumeration, and the JOB_GROUPS needs-lists). All 32 worklist dirs are now routed. This is the shrink interpretation of #1933 (fast, targeted PR CI), not the literal nightly-scheduled full suite (deferred per C-006); the escape hatches (workflow_dispatch run_all/run_extended, the nightly schedule cron, the unmatched fail-closed catch-all) and the nightly run_all over-cover of every worklist dir remain intact — no new blind spot (FR-009).
    • Shard-side split (#2378). fast-tests-core-misc is subdivided into two disjoint, non-empty matrix shards (ignore-mirror kept consistent), so a core-misc change no longer drags the whole misc bucket.
    • Architectural un-blind, de-serialized (#2383, NFR-002). The architectural + adversarial guard job (arch-adversarial) runs always-on (if: always(), group-less) over 100% of src/, and is de-serialized from fast-tests-core-misc (its needs edge is dropped) so it no longer sits on the core-misc critical path — the path collapses from sum to max, structurally under the ≤13.6-min next-lane ceiling (NFR-001; live measured_source_run_id backfilled by the operator from the PR's first post-shrink CI run).
    • Coverage-consumer integrity by construction (C-005). New architectural invariants assert coverage-emitting jobs ⊆ sonarcloud.needs and critical-path emitters ⊆ diff-coverage.needs; in the process the pre-existing production mission-loader-coverage coverage-drop is fixed — it emits --cov=src/specify_cli/mission_loader yet was absent from sonarcloud.needs, so its coverage XML was silently dropped from the Sonar gate. The full 8-invariant #2368 substrate suite plus the new NFR-002/003/005 and C-005 relations stay green (NFR-007), and the gate-coverage ratchet baseline is refreshed (orphan_test_count 0; total_tests rises with the added invariants; duplicate_test_count falls with the same-tier consolidation).
  • Root CHANGELOG.md is now a symlink to the canonical docs/changelog/CHANGELOG.md — the generated mirror is retired. The two-file model (canonical + frontmatter-stripped root copy kept in sync by scripts/docs/sync_changelog.py) made contributors edit the root copy and trip docs-freshness on every external PR. There is now exactly one changelog file; sync_changelog.py --check guards the symlink (and --write restores it), and release tooling (extract_changelog.py, validate_release.py) reads the canonical text through the link — both scan ## [...] headings and tolerate the YAML frontmatter.
  • orchestrator-api contract 1.1.0 → 1.2.0: new read-only resolve-workspace command (#2337). resolve-workspace --mission --wp returns a work package's lane workspace_path / prompt_path / lane_branch for its existing lane, resolved via the canonical naming seams without allocating, creating, validating-clean, or transitioning. It's the read-only companion of start-implementation (which does a planned→claimed→in_progress composite transition): an external orchestrator resuming a WP already past implementation (e.g. one parked in for_review after an interrupted run) can obtain its workspace to dispatch a reviewer without mis-transitioning it. Purely additive — existing commands and payloads are unchanged.
  • Internal: the agent tasks god-command is decomposed into pure decision cores behind injected ports (mission tasks-py-degod-01KWF08S, #2116 under #2173). Behavior-preserving — the full agent tasks CLI contract (all subcommands, flags, exit codes, --json envelopes, including the coord skip-exit-0 arm and the refuse-exit-1 arms) is byte-identical, frozen by a golden characterization harness. The decision/aggregation logic of the five fat command bodies (move_task, map_requirements, status, mark_status, finalize_tasks) now lives in pure, independently-tested sibling modules (tasks_transition_core, tasks_mapping_core, tasks_status_view) behind an injected TasksPorts seam (FsReader coord-READ authority + a two-capability CoordCommitRouter coord-WRITE authority); each command body is a ≤150-LOC thin orchestrator. Also folds the pre-3.0 coord read-authority split-brain onto the kind-aware authority (guard-only sites) and drains the resolution-authority census (shrink-only). No user-facing behavior change. The Render-seam unification and the whole-file tasks.py shim relocation are deferred to a follow-up mission (see docs/plans/tasks-py-degod-followup-mission-debrief.md).

🐛 Fixed

  • Bulk-edit occurrence-map gate now also enforced on the legacy agent tasks finalize-tasks command (#2345, PR #2386). The PR that gates occurrence_map.yaml at finalize-tasks (rather than at first implement WP##) only wired the check into agent mission finalize-tasks; the older, still-live agent tasks finalize-tasks command family could complete finalize-tasks for a bulk-edit mission with a missing, schema-invalid, or inadmissible occurrence map with zero gate friction — found by the pre-merge adversarial squad during landing. _ft_validate_occurrence_map_ready mirrors the mission-command gate and runs first in _ft_validate (fail-fast, before dependency parsing); the shared error message and JSON payload are de-duplicated into bulk_edit/gate.py (finalize_tasks_gate_error_payload) so the two command surfaces can't drift apart again.

  • Honest SaaS sync opt-in reporting + typed remote_sync status fields (#2264 slice, PR #2396). sync opt-in printed ✓ Enabled SaaS sync for this checkout even though it only writes local routing flags — implying remote enablement that never happened; the message now states only that a local preference was recorded. sync status --check --json gains a typed remote_sync block (remote_project_state / materialized_at / historical_import_state / last_blocker_sample), honestly unknown/null until the import engine (#2262) populates it — ok semantics are unchanged, so existing consumers are unaffected and new consumers read remote state from remote_sync, not ok. Also folded in: opt-in now exits non-zero (was a dim exit-0 message) when the SaaS-sync rollout flag is disabled, since opt-in cannot take effect with the flag off.

  • Guard/gate friction hotfixes (#2346 / #2324, #1834).

    • Subtask guard no longer misattributes a later WP's checkboxes (#2346, also closes #2324). _check_unchecked_subtasks entered a WP's section on any heading that merely mentioned its id, so a dependent heading like ### WP03 — … (depends: WP01, WP02) re-entered WP01/WP02's section and harvested WP03's unchecked - [ ] T0xx rows as the earlier WP's blockers — spuriously blocking that WP's lane transition. A heading now belongs to the WP named by its first WPxx token, not any mention.
    • grep_absence negative invariants accept an optional path-scope (#1834). The acceptance gate ran grep -r <pattern> . over the whole repo, so a negative-invariant pattern that a mission's own spec/plan/WP prose mentioned false-positived as still_present. NegativeInvariant now carries an optional scope (whitespace-separated repo-relative search roots); when set, the grep runs only under those paths. Default (unscoped) preserves the whole-repo search, and scope is omitted from serialization when unset so existing matrices are untouched.
    • Documented merge-before-accept for merged-post-state invariants (#1834). The accept runbook (docs/guides/accept-and-merge.md) now records that the accept gate re-runs each negative-invariant verification_command live (so a hand-set overall_verdict does not stick), and that a mission whose invariants assert the merged post-state must run spec-kitty merge (local) before spec-kitty accept.
  • A mission fully implemented by spec-kitty-orchestrator can now pass
    spec-kitty accept — two mechanical false-positives removed (#2369).
    The
    accept gate is the mission-level readiness check (all-done, subtasks,
    clarifications, artifacts, clean tree, paths), but two checks always failed on
    orchestrator-completed missions, forcing operators to bypass accept entirely:
    (a) the strict-metadata check required shell_pid on every WP, including
    terminal ones — but shell_pid is an interactive-spec-kitty next artifact the
    orchestrator never stamps; it is now lane-gated to active lanes exactly like
    assignee
    , so a done/approved WP no longer needs it (an active WP still does).
    (b) spec-kitty materialize writes regenerable views to .kittify/derived/,
    which was not in the runtime gitignore set (unlike sibling .kittify/
    paths), so it dirtied the tree and failed accept's git_dirty check — the
    derived/ views are now a registered IGNORED state surface (so fresh
    spec-kitty init gitignores it), and a dedicated backfill migration
    (3.2.4_derived_mission_views_gitignore_backfill) adds .kittify/derived/
    to .gitignore on spec-kitty upgrade for already-initialised projects
    (the runtime-hygiene migrations previously only knew a hardcoded subset of
    entries). The six meaningful accept checks still gate.

  • Mission-state repair no longer empties status.events.jsonl of a healthy
    mission (#2376).
    The repair (run by spec-kitty upgrade via the TeamSpace
    mission-state gate, and by doctor mission-state --fix) quarantined every
    event_type row except retrospective ones — including the canonical lifecycle
    events (MissionCreated, SpecifyStarted, WPCreated, …) that
    status/lifecycle_events.py writes and whose only per-mission home is
    status.events.jsonl. A completed mission whose log was all lifecycle events
    was emptied to 0 bytes. Repair now preserves every reader-canonical non-lane
    class
    in place — canonical lifecycle events (those in
    LIFECYCLE_EVENT_TYPES), retrospective lifecycle rows (the type envelope),
    and the retrospective.* (event_name envelope) stream written by
    emit_retrospective_event
    — so the repair predicate matches the durable
    reader (status/store.py::is_non_lane_event) exactly. Before this, a mixed log
    containing a retrospective.completed row still silently stripped it (the same
    #2376 data-loss class in a different event format). A backstop also refuses to
    write a 0-byte log when the source was non-empty. Decision-Moment
    (DecisionPoint*) rows are still pruned: their canonical store is
    decisions/index.json / DM-*.md, so the copy here is a mirror (unchanged,
    tested behavior). Rows preserved verbatim in the quarantine dir remain
    recoverable.

    • Repair now anchors on the primary checkout so SNAPSHOT_DRIFT actually
      converges and stale coordination worktrees are left untouched (#2320).

      doctor mission-state --fix already re-materializes status.json from
      status.events.jsonl, but when invoked from inside a worktree the
      invocation root pointed at that worktree, so the materialize landed there:
      the primary status.json stayed frozen (a re---audit reported the
      same SNAPSHOT_DRIFT blocker) and uncommitted repairs were written into a
      possibly-stale coordination worktree. Repair now re-anchors to the canonical
      primary main-checkout via the single worktree-pointer parser
      (resolve_canonical_root), so it always targets the primary
      kitty-specs/<slug> (drift converges) and never dirties .worktrees/. The
      read-only --audit path is now anchored on the same authority at the
      run_mission_state dispatch seam, so audit and fix resolve to the identical
      canonical root from any cwd (a worktree-invoked --audit reads the primary,
      matching --fix) instead of diverging onto a stale worktree.
  • The Op-index performance cache is now gitignored (#2341).
    kitty-ops/ops-index.jsonl — the machine-local reverse-scan cache that powers
    spec-kitty invocations list — was never added to .gitignore, so a
    freshly-generated index showed up in git status indefinitely (and could be
    accidentally committed). It is now registered as an IGNORED LOCAL_RUNTIME
    surface in the state contract (op_invocation_index), which flows into fresh
    spec-kitty init protection automatically. Existing projects are repaired by
    the runtime git-hygiene migration, which also git rm --cacheds a
    previously-committed index. Durable per-Op audit records
    (kitty-ops/<op_id>.jsonl, the new op_invocation_record surface) stay
    tracked — only the index is ignored.

  • The dashboard no longer orphans a valid in-flight (mid-orchestration)
    mission (#2331).
    While a coordination-topology mission had live worktrees
    checked out, spec-kitty dashboard registered it under a synthetic
    orphan:<slug> key — hiding it from the mission dropdown — because the
    registry read mission identity (meta.json) from the coordination worktree,
    which lacks it (meta.json is a PRIMARY-partition artifact that lives on the
    primary checkout). Identity now resolves through the kind-aware
    resolve_planning_read_dir(..., PRIMARY_METADATA) seam, so a mission mid-run
    shows under its real title and canonical ULID; status/board display still uses
    the coordination surface. lifecycle.json now also carries mission_id. No
    change for merged/idle missions.

  • move-task --to for_review recovers a killed implementer's uncommitted lane
    deliverables instead of dead-ending (#2335).
    When an implementer finished its
    files but was interrupted before committing, moving the work package to
    for_review failed with a message demanding a manual git add/git commit
    inside the lane worktree — violating the "spec-kitty drives commits" rule. On
    the for_review transition, when the auto-commit policy is enabled (the
    default), spec-kitty now commits the finished lane deliverables via the tool
    (safe_commit on the lane branch) before the readiness guard runs, so recovery
    completes without touching lane git by hand. Scoped to for_review only
    (approved/done deliverables are already committed); --force still
    bypasses, and --no-auto-commit defers to the existing guard.

  • mission close / spec-kitty merge now commit the retrospective they
    auto-generate, instead of leaving the durable event log dirty.
    Closing a
    mission that was merged via the legacy plain-git/GitHub path (so merge-time
    teardown never ran) auto-captured retrospective.yaml and appended a
    RetrospectiveCaptured event to status.events.jsonl, but left both
    uncommitted with no notice — violating the atomic-event-log discipline
    (FR-016), since an uncommitted append can be lost. The shared post-merge
    retrospective postcondition now commits the captured record + its event-log
    append via the merge-bookkeeping commit path, so merge and mission close
    behave identically and the working tree is left clean. If the commit cannot be
    made (detached HEAD / not a worktree), it fails open but reports the
    uncommitted artifacts and the exact command to commit them. mission close --help no longer describes the non---discard path as a pure "no-op".

  • map-requirements now explains why a WP requirement_refs entry is stale
    instead of looking like data corruption (#2066).
    When the stale/invalid-refs
    gate trips, the --json payload (and console output) now surface the FR-ID set
    parsed from spec.md (parsed_spec_ids), classify each offending ref per WP into
    malformed (violates the FR-NNN / NFR-NNN / C-NNN format — e.g. a
    letter-suffixed FR-003a or an unfilled <FR-XXX> placeholder) vs
    unknown_spec_id (well-formed but not declared in the spec), and the hint names
    the format rule. A one-character ID-format mismatch is now obvious rather than
    reading like invented/orphaned IDs.

Don't miss a new spec-kitty release

NewReleases is sending notifications on new releases.