v3.2.4
QwenLM/Qwen3-ASRv3.2.4Jul 5, 2026by github-actions[bot]
AI Summary
This reliability-focused release improves mission lifecycle friction, enhances bulk-edit safety, and fixes SaaS sync reporting. It also includes internal architectural changes to the CI pipeline and decomposes the `agent tasks` command module.
Key Highlights
- Smoother mission lifecycle with fixes for `spec-kitty accept` and `move-task` recovery.
- Mission-state repair now preserves event logs to prevent data loss.
- Bulk-edit safety net closed to prevent incomplete classifications from slipping through.
- Honest SaaS sync reporting that accurately reflects remote state.
- New `resolve-workspace` capability for external orchestrators.
Breaking Changes
- Deprecated compatibility shim packages removed (e.g., `specify_cli.next`, `specify_cli.glossary`).
New Features
- New read-only `resolve-workspace` command capability.
- Canonical `MissionCreated` payload builder to standardize event generation.
- CI topology shrink and shard split for faster, more targeted feedback.
- Decomposition of the `agent tasks` command module for lower regression risk.
Full Release Notes
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.next` → **`runtime.next`** (the canonical `spec-kitty next` runtime/control-loop package; antecedent #612)
- `specify_cli.glossary` → **`glossary`** (antecedent #613)
- `specify_cli.charter_lint` → **`specify_cli.charter_runtime.lint`**
- `specify_cli.charter_freshness` → **`specify_cli.charter_runtime.freshness`**
- `specify_cli.charter_preflight` → **`specify_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 --cached`s 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.