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 byspec-kitty-orchestratorcan now pass
spec-kitty accept(two false-positive blockers removed),move-task --to for_reviewrecovers 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 dirtygit statusor block
accept.map-requirementsalso 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 repairspec-kitty upgraderuns) 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.yamlgate 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-inno longer implies remote
enablement it didn't perform, andsync status --check --jsonreports real
(or honestlyunknown) remote/import state instead of staying silent. - New
orchestrator-apicapability. A read-onlyresolve-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 canonicalspec-kitty nextruntime/control-loop package; antecedent #612)specify_cli.glossary→glossary(antecedent #613)specify_cli.charter_lint→specify_cli.charter_runtime.lintspecify_cli.charter_freshness→specify_cli.charter_runtime.freshnessspecify_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 byspec-kitty doctor shim-registry) is drained toshims: [], 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 nextandspec-kitty charter lint/preflight/freshnessbehave identically. No version bump accompanies this entry:src/specify_cli/__init__.pyis 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.mdstill published the retired legacy charter pathmemory/charter.md, reddingfast-tests-docson every open PR — replaced with the canonical.kittify/charter/charter.md; (b) thearch-adversarialjob (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 asfast-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
MissionCreatedpayload builder (#2270, PR #2398). Three independent code paths derivedMissionCreateddefaults and had drifted apart:core/mission_creation.py, a verbatim-duplicate helper insync/emitter.py, and a third, divergent inline fallback instatus/lifecycle_events.py(raw slug vs. titleizedfriendly_name, absent vs. nowcreated_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 SaaSEventEmitterroute through it, preserving the CORE↛INTEGRATION boundary (no sync import in core). Behavioral note: the local mission log's defaultfriendly_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). Theci-quality.ymlPR 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 compositedorny/paths-filtergroups (auth_audit_git,lifecycle,agent_surface,closeout,governance,platform), each registered atomically across all five surfaces (filters block,changes.outputs.*, theunmatchedenumeration, and theJOB_GROUPSneeds-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_dispatchrun_all/run_extended, the nightlyschedulecron, theunmatchedfail-closed catch-all) and the nightlyrun_allover-cover of every worklist dir remain intact — no new blind spot (FR-009). - Shard-side split (#2378).
fast-tests-core-miscis 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% ofsrc/, and is de-serialized fromfast-tests-core-misc(itsneedsedge is dropped) so it no longer sits on the core-misc critical path — the path collapses fromsumtomax, structurally under the ≤13.6-min next-lane ceiling (NFR-001; livemeasured_source_run_idbackfilled 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.needsand critical-path emitters ⊆diff-coverage.needs; in the process the pre-existing productionmission-loader-coveragecoverage-drop is fixed — it emits--cov=src/specify_cli/mission_loaderyet was absent fromsonarcloud.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_count0;total_testsrises with the added invariants;duplicate_test_countfalls with the same-tier consolidation).
- Group-side shrink (#1933). The previously-unmapped
- Root
CHANGELOG.mdis now a symlink to the canonicaldocs/changelog/CHANGELOG.md— the generated mirror is retired. The two-file model (canonical + frontmatter-stripped root copy kept in sync byscripts/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 --checkguards the symlink (and--writerestores 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-apicontract 1.1.0 → 1.2.0: new read-onlyresolve-workspacecommand (#2337).resolve-workspace --mission --wpreturns a work package's laneworkspace_path/prompt_path/lane_branchfor its existing lane, resolved via the canonical naming seams without allocating, creating, validating-clean, or transitioning. It's the read-only companion ofstart-implementation(which does aplanned→claimed→in_progresscomposite transition): an external orchestrator resuming a WP already past implementation (e.g. one parked infor_reviewafter 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 tasksgod-command is decomposed into pure decision cores behind injected ports (missiontasks-py-degod-01KWF08S, #2116 under #2173). Behavior-preserving — the fullagent tasksCLI contract (all subcommands, flags, exit codes,--jsonenvelopes, 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 injectedTasksPortsseam (FsReadercoord-READ authority + a two-capabilityCoordCommitRoutercoord-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-filetasks.pyshim relocation are deferred to a follow-up mission (seedocs/plans/tasks-py-degod-followup-mission-debrief.md).
🐛 Fixed
-
Bulk-edit occurrence-map gate now also enforced on the legacy
agent tasks finalize-taskscommand (#2345, PR #2386). The PR that gatesoccurrence_map.yamlat finalize-tasks (rather than at firstimplement WP##) only wired the check intoagent mission finalize-tasks; the older, still-liveagent tasks finalize-taskscommand 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_readymirrors 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 intobulk_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_syncstatus fields (#2264 slice, PR #2396).sync opt-inprinted✓ Enabled SaaS sync for this checkouteven 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 --jsongains a typedremote_syncblock (remote_project_state/materialized_at/historical_import_state/last_blocker_sample), honestlyunknown/null until the import engine (#2262) populates it —oksemantics are unchanged, so existing consumers are unaffected and new consumers read remote state fromremote_sync, notok. Also folded in:opt-innow 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_subtasksentered 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- [ ] T0xxrows as the earlier WP's blockers — spuriously blocking that WP's lane transition. A heading now belongs to the WP named by its firstWPxxtoken, not any mention. grep_absencenegative invariants accept an optional path-scope (#1834). The acceptance gate rangrep -r <pattern> .over the whole repo, so a negative-invariant pattern that a mission's own spec/plan/WP prose mentioned false-positived asstill_present.NegativeInvariantnow carries an optionalscope(whitespace-separated repo-relative search roots); when set, the grep runs only under those paths. Default (unscoped) preserves the whole-repo search, andscopeis 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-invariantverification_commandlive (so a hand-setoverall_verdictdoes not stick), and that a mission whose invariants assert the merged post-state must runspec-kitty merge(local) beforespec-kitty accept.
- Subtask guard no longer misattributes a later WP's checkboxes (#2346, also closes #2324).
-
A mission fully implemented by
spec-kitty-orchestratorcan 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 requiredshell_pidon every WP, including
terminal ones — butshell_pidis an interactive-spec-kitty nextartifact 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 materializewrites 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'sgit_dirtycheck — the
derived/views are now a registeredIGNOREDstate surface (so fresh
spec-kitty initgitignores it), and a dedicated backfill migration
(3.2.4_derived_mission_views_gitignore_backfill) adds.kittify/derived/
to.gitignoreonspec-kitty upgradefor 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.jsonlof a healthy
mission (#2376). The repair (run byspec-kitty upgradevia the TeamSpace
mission-state gate, and bydoctor mission-state --fix) quarantined every
event_typerow except retrospective ones — including the canonical lifecycle
events (MissionCreated,SpecifyStarted,WPCreated, …) that
status/lifecycle_events.pywrites 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 (thetypeenvelope),
and theretrospective.*(event_nameenvelope) 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 aretrospective.completedrow 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_DRIFTactually
converges and stale coordination worktrees are left untouched (#2320).
doctor mission-state --fixalready re-materializesstatus.jsonfrom
status.events.jsonl, but when invoked from inside a worktree the
invocation root pointed at that worktree, so the materialize landed there:
the primarystatus.jsonstayed frozen (a re---auditreported the
sameSNAPSHOT_DRIFTblocker) 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--auditpath is now anchored on the same authority at the
run_mission_statedispatch seam, so audit and fix resolve to the identical
canonical root from any cwd (a worktree-invoked--auditreads the primary,
matching--fix) instead of diverging onto a stale worktree.
- Repair now anchors on the primary checkout so
-
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 ingit statusindefinitely (and could be
accidentally committed). It is now registered as anIGNOREDLOCAL_RUNTIME
surface in the state contract (op_invocation_index), which flows into fresh
spec-kitty initprotection automatically. Existing projects are repaired by
the runtime git-hygiene migration, which alsogit rm --cacheds a
previously-committed index. Durable per-Op audit records
(kitty-ops/<op_id>.jsonl, the newop_invocation_recordsurface) 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 dashboardregistered 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.jsonis 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.jsonnow also carriesmission_id. No
change for merged/idle missions. -
move-task --to for_reviewrecovers 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_reviewfailed with a message demanding a manualgit add/git commit
inside the lane worktree — violating the "spec-kitty drives commits" rule. On
thefor_reviewtransition, when the auto-commit policy is enabled (the
default), spec-kitty now commits the finished lane deliverables via the tool
(safe_commiton the lane branch) before the readiness guard runs, so recovery
completes without touching lane git by hand. Scoped tofor_reviewonly
(approved/donedeliverables are already committed);--forcestill
bypasses, and--no-auto-commitdefers to the existing guard. -
mission close/spec-kitty mergenow 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-capturedretrospective.yamland appended a
RetrospectiveCapturedevent tostatus.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, somergeandmission 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 --helpno longer describes the non---discardpath as a pure "no-op". -
map-requirementsnow explains why a WPrequirement_refsentry is stale
instead of looking like data corruption (#2066). When the stale/invalid-refs
gate trips, the--jsonpayload (and console output) now surface the FR-ID set
parsed fromspec.md(parsed_spec_ids), classify each offending ref per WP into
malformed(violates theFR-NNN/NFR-NNN/C-NNNformat — e.g. a
letter-suffixedFR-003aor 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.