v3.2.6rc1
Pre-releaseWarning
3.2.6rc1 is a Release Candidate — an internal, delta build published for validation, NOT an official 3.2.6 release. It ships as a GitHub prerelease and to PyPI as a PEP 440 prerelease, so ordinary installers skip it unless you explicitly opt in with --pre. Do not use it for production or general rollout. The last official release remains v3.2.5; when 3.2.6 is finalized its changelog section supersedes this one.
Install for testing (opt-in only):
pipx install --pre spec-kitty-cli==3.2.6rc1
# or, inside an existing environment:
pip install --pre spec-kitty-cli==3.2.6rc1Highlights since v3.2.5 — the operator-facing changes worth exercising in this candidate:
- Breaking — built-in doctrine content relocated to
packs/built-in/with no compatibility shim (missionrelocate-builtin-doctrine-packs). Repoint any reference that still targets the oldsrc/doctrine/<kind>/built-in/path. - Breaking — local
beads/fptracker sync now requires a recorded egress decision (missiontracker-egress-refusal-3108). A binding that never recorded hosted-sync consent stops syncing on upgrade until you recordtracker.egress: permittedorsync.enabled: true. - Breaking — the
rtk-search-toolingtoolguide is removed, and the3.2.6_retire_rtk_search_toolingupgrade migration strips it from projects that had it activated (it runs automatically onspec-kitty upgradeand is safe to re-run). - Breaking — org packs with an unrecognised agent-profile or DRG key now fail to load (mission
doctrine-silence-guards). Runspec-kitty doctor doctrine --jsonand checkskipped_profilesbefore you upgrade. charter synthesizeis now non-destructive — it preserves backed governance content by default, with--pruneas the explicit opt-in and--dry-runto preview (missioncharter-synthesize-reconciliation;#3270P0, folds#2777/#3052). Theimplement/nextboundary no longer hard-blocks until you resynthesize.- Approving a work package after a rejection now sticks with no override flag required (mission
review-verdict-write-integrity;#3044) — the reject → fix → approve cycle no longer forces--skip-review-artifact-check. - Timestamps Spec Kitty writes into your project are now correct aware-UTC instead of local time mislabelled as UTC (mission
kernel-clock-single-door;#3305, closes#3289). - CLI UX: shell autocompletion, a
-hshort-help alias, and alphabetical command listing (#2232,#2234,#2235) — additive, with no behavior change to existing commands.
The complete, factual list of changes for this candidate follows in the entries below.
✨ Added
-
Multi-Agent Parallel Development and Orchestrator Quickstart tutorials now open
with illustrated Mission Kitty splashes (#3331). Each page adds a decorative hero and
a one-line caption that points to the authoritative workflow steps in the prose
below, matching the pattern introduced in#3276. -
Governed Charter Workflow, Claude Code Integration, and Claude Code Workflow
tutorials now open with illustrated Mission Kitty splashes, plus TIP cross-links
and hubrelated:edges (#3333). Each page adds a decorative hero and a one-line
caption; the Charter tour points at the governance how-tos, and the two Claude
Code tutorials link each other for slash-command vs CLI/dashboard paths. -
Three #3276 Mission Kitty heroes are regenerated as decorative-only splashes
(missions overview, your first mission, when-to-use-modes alternate art) (#3341).
Replaces caption-mitigated board labels with abstract art; each PNG compressed
to ≤500 KB. Memphis, four-paths, and corporate-comics assets on when-to-use-modes
are unchanged. -
The DRG now carries a whole-graph, action-only reachability companion guard, and six genuine residual
orphans are wired into the graph (missiondrg-reachability-metric-wiring-01KZS5VR;#3009point 3,
#1923).TestReachabilityCompanionGuardintests/doctrine/drg/test_reachability.pyasserts every
activatable-kind node reachable from neither the action channel nor the profile channel's
{requires, specializes_from, suggests}web is correctly partitioned into "both-channel dead" vs
"profile-delivered" — live totality/disjointness assertions against the graph measured at test time, not
an exact-membership pin (the ever-growing frozenset-literal pin was softened out during PR #3342 landing,
consistent with missionassertive-test-suite-sanitation-01KZME3P's "test plausible graph behavior, not
exact ever-growing membership"). A fixed anti-gaming gate (TestActionUnreachableShippedLedgerCoverage)
independently proves the thirteen URNs this mission wires are genuinely action-reachable and named in a
wiring-table ledger row, so a node with outbound edges but no inbound path still cannot pass silently. Six
traced inbound edges (procedure:refactoring → DISCIPLINED_REFACTORING,DIRECTIVE_024/DIRECTIVE_025→
RECONCILE_CHANGE_SCOPE_TENSIONS,DIRECTIVE_030→USE_MUTATION_TESTING_TO_VALIDATE_TEST_QUALITY,
researcher-robbie → spike-timebox-policy,lexical-larry → glossary-maintenance-workflow,
minutes-maker-mahad → meeting-minutes-pipeline) move the action-only whole-graph residual 88 → 75 and
the both-channel-dead subset 38 → 34 (descriptive measurements at time of wiring, not pinned
assertions). The#1923DRG orphan-residual record
(kitty-specs/mission-lifecycle-dispatch-drg-closeout-01KV0S99/drg-orphan-residual.md) is truth-upped
against the wired graph: every one of the 75 residual members now carries an individual or group
disposition,toolguide:rtk-search-toolingis retired (already removed from disk), and only the genuinely
action-reachable members of the prior "6 promoted" claim are recorded as promoted. -
Documentation pages can now declare who they are for: a canonical
audience:
frontmatter field, plus a Common Docs styleguide and gates, ship in the built-in
doctrine pack (missioncommon-docs-convergence). Each page states its
intended reader and Divio type, and projects that activate the pack inherit the
same Common Docs conventions — the single-root structure and audience/type
discipline — out of the box. -
charter synthesizeis now non-destructive — it preserves backed governance
content by default (missioncharter-synthesize-reconciliation;#3270P0,
folds#2777/#3052). Previously an authoring-only charter edit forced
operators to runcharter synthesize, which silently deleted doctrine graph
nodes and edges whose backing artifacts still existed on disk — andimplement
/nexthard-blocked until you ran it. Now synthesize reconciles against
the on-disk graph: backed content is retained and the command reports what it
kept (exit 0);--pruneis the explicit opt-in that removes divergent content
and lists every deletion;--dry-runpreviews exactly what--prunewould
remove and writes nothing; and a non-zero refusal is reserved for genuinely
unpreservable states (orphaned removal without--prune, or an unparseable
overlay). Theimplement/nextboundary auto-refresh now self-heals
non-destructively and clears the stale signal so you are never trapped, and
charter activate/deactivatego through the same preserve path. Consumer-pack
synthesis also emits charter-relevant edges from declared interview evidence
(#3052) — no more orphaned just-generated directives, and no fabricated edges. -
spec-kitty accept --jsonnow surfaces stranded-verdict advisories in a
top-leveladvisoriesarray (missionverdict-seam-boundary-hardening;
#3255). When a mission carries a review verdict that no longer has a home
(an SC-008 "stranded verdict"),accept --jsonreports it as a structured,
non-error advisory alongside the normal payload instead of staying silent — so
operators and automation can see and act on it without scraping human-readable
output. -
Spec Kitty's own charter now activates the writing-comms & diagramming
doctrine set - this repository only; no downstream impact (builds on#2918/
#3225).
Spec Kitty now dogfoods "The Magnificent 7" writing-comms doctrine in its own
.kittify/charter/: audience-oriented writing (DIRECTIVE_047+
writing-audience-catalog+ theplain-languageand
professional-communicationsstyleguides), the documentation-structure
styleguides (divio-type-discipline,docs-accessibility,docs-freshness-sla,
publication-authority), C4 diagramming (USE_C4_MODEL_TECHNIQUES, alongside the
already-active Mermaid/PlantUML toolguides), the communication-governance
directives (048version-governance,049agent self-introduction,050
credential-handling), theglossary-maintenance-workflowprocedure and
research-citation-disciplinestyleguide, and the seven writing-comms agent
profiles (comms-cleo,diagram-daisy,analyst-annie,lexical-larry,
minutes-maker-mahad,scribe-sally,synthesizer-sam). It also activates
RECONCILE_CHANGE_SCOPE_TENSIONSto bridge the smallest-viable-diff / Boy Scout /
locality-of-change tension, explained in a newcharter.mdsection.
Scope: this changes Spec Kitty's own project governance only. The doctrine
artifacts already ship inpacks/built-in; projects created or upgraded by the
CLI are unaffected and keep their own charter activations. -
A new "When to use Spec Kitty modes" guide helps you pick the lightest path
for the work at hand (#3238).
docs/guides/when-to-use-modes.mdlays out
four entry points - skip Spec Kitty for a trivial edit, dispatch an ad-hoc
specialist for a short task, run a bounded adversarial squad at an SDD
checkpoint, or drive a full mission when acceptance criteria and structured
review matter - with an illustrated overview, so you match effort to the
change instead of paying full-mission overhead on every edit. -
spec-kitty charter pack apply --compilenow applies a pack and compiles
its bundle in one step (missioncharter-pack-usage-journey;#3105).
Previouslyapplyonly merged activations intoconfig.yamland left you to
discover that a separatecharter generatewas still needed; the default
applyoutput now names that exact next command, and--compiledoes both at
once (inheritinggenerate's git-worktree requirement).charter context --include section:terminology-canon/section:code-review-checklistalso no
longer dead-end when the section is unauthored - they resolve to an honest
placeholder pointing you at the file to edit (#3095,#3094,#2552). -
Empty/unconfigured charter now dispatches to a warned generic agent instead
of silently applying every built-in doctrine artefact (mission
charter-delivery-finish-context-degod;#3064). Previously, running a
dispatch in a repository with no charter activated silently fell back to
"all built-ins" - the entire shipped doctrine catalogue was applied in both
routing and the governance context injected into the prompt. Now, when
nothing is activated (a composite check across every charter-activatable
dimension), auto-routing resolves to a generic agent using no charter
doctrine - verified across both routing and the governance block, so no
directive canon leaks into the prompt - and the user gets a clear warning
to activate a charter first. Thesoftware-devmission type stays
always-available, and explicitly passing--profile <name>still resolves the
specialist even under an empty charter (only the no-hint auto-route path
changes; the shared activation gate is untouched). -
A minimal starter charter now ships as a first-class, on-demand charter
pack (missioncharter-delivery-finish-context-degod;#3064). It ships
assrc/charter/packs/minimal.yaml, alongsidedefault.yaml. Run
spec-kitty charter pack listto see the shipped built-in packs,
spec-kitty charter pack path minimalto resolve the file, or
spec-kitty charter pack apply minimalto merge its small, curated
activation set into.kittify/config.yamlinstead of authoring one from
scratch - it is additive by default and never silently overwrites an
existing activation (--forceto overwrite explicitly). -
Profile-channel
suggests-edge delivery is now live - the inert#3063
doctrine topology actually delivers (missiondoctrine-delivery-activation,
fast-follow todoctrine-delivery-reachability). The profile-channel
reachability walk now followssuggestsedges (in addition torequires/
specializes_from) and surfaces each edge'swhenclause as the delivered
doctrine's applicability condition - aswhen-labelled links, not eager
bodies (respecting the context-bloat budget). Architect and implementer
profiles now receive their linked paradigms, tactics, techniques, and
canvases: e.g.architect-alphonsoreachesdomain-driven-design, the C4
model techniques, and the REASONS-canvas guidance; implementer profiles reach
theDISCIPLINED_REFACTORINGrefactoring tactics. Companion authoring: a C4
template:instantiatesedge fromaction:documentation/design, and
refactoring code-smellanti_patternnodes wired viaREJECTS(grounded in
each tactic's attestedproblem/when). The reachability pins, the delivery
wiring table, and the forward-API dead-symbol allowlist are reconciled to the
now-live topology, each moved count carrying a composition-ledger entry. (#3063) -
The
assetdoctrine kind is now reachable end to end - resolve, deliver,
and author it (missiondoctrine-delivery-reachability). Previously a
shipped asset (executable logic or any blob a pack hands to a downstream repo)
had no operator surface and no followable how-to, so the documented remedy
"ship it as an asset" was not actually followable (#3037). Now:spec-kitty doctrine asset listenumerates every resolvable asset with
its source tier (built-in / org / project), andspec-kitty doctrine asset path <id>resolves one identifier to a filesystem path - exit0on
success, non-zero with the id named on an unknown id or a containment
refusal. Nothing is installed into the consumer repo; assets resolve from
packaged data plus the project/org overlays (no auto-install). The one
built-in asset,common-docs-structural-lint, resolves from any
installation.spec-kitty doctrine new --kind asset <name>scaffolds an asset with
the same parity asvalidate, writing into the directory the resolver reads
(.kittify/doctrine/assets/).- The action doctrine bundle now delivers every resolved kind, including
procedures and assets. The delivery gate is a total function over kinds:
activation-gated kinds deliveractivated ∩ reachable, while assets are
delivered-but-not-activation-gated (gate = ALL) - a reachable source
pulls them in without an activation list. This closes the defect where
asset_ids = []was the silently-conforming outcome forever. - Docs: Create a doctrine artifact
gains an executable asset how-to (author a manifest, place the blob, resolve
it), and Doctrine artifact kinds documents
the shipped built-in asset and the three delivery categories.
-
New checks that catch a change which looks like it worked and did nothing
(missiondoctrine-silence-guards). Four additions, all aimed at the same
failure mode - a declaration that loads, validates, reports success, and then
has no effect:- A zero-producer lint fails the build when a schema field is declared but
no code path ever writes it. Three such fields had shipped in this
repository, one of them inert for months behind passing tests. The findings
that already exist are frozen as a shrink-only baseline; the list can only
get smaller, every entry names an owner and the fix it is waiting on, and a
test refuses to let an entry outlive the work that was meant to remove it. scripts/generate_schemas.py --checknow runs in CI, in the always-on
lintjob rather than behind a path filter, so a model change that leaves
its generated YAML schema behind fails immediately. Seven schemas were
already stale and are reconciled here.spec-kitty doctor doctrinenow reports org-pack edges that point at
nothing (see Breaking Changes).- Occurrence maps can protect a single field inside a file that is otherwise
migrating. Thedo_not_changeclassification used to accept only
whole-file path globs, so a file containing both governed keys and keys that
should be renamed could not be described. It now accepts a YAML field path.
- A zero-producer lint fails the build when a schema field is declared but
-
CLI UX: shell autocompletion, a
-hshort-help alias, and alphabetical
command listing (#2232, #2234, #2235). Three additive quality-of-life
improvements to thespec-kittycommand surface, with no behavior change to
existing commands:- Shell autocompletion is now enabled (
--install-completion/
--show-completion), with a latency-safe fast path: every TAB press serves
command/subcommand candidates from a small committed manifest
(_completion_manifest.json) instead of importing the whole command tree,
cutting completion invocation latency from roughly 812 ms to about 76 ms. A
drift-guard test keeps the manifest in sync with the live command tree. -his accepted as an alias for--helpacross the root command and
every subcommand.- The root
--helpcommand list is now sorted alphabetically, and a bare
spec-kittyinvocation renders the same ordered help.
- Shell autocompletion is now enabled (
-
spec-kitty sync import-historymaterializes existing local mission history
into the SaaS projection (#2262). A first sync registers a remote
project/build but leaves it with zero materialized missions - the SaaS
materializer refuses to fabricate a work package from a status event with no
prior create. The new command synthesizes the missing
MissionCreated -> WPCreated[] -> WPStatusChanged[]prefix (INV-3) from local
history so historical work populates the projection.--dry-run(default)
runs the whole read-only pipeline and previews the stream;--applyattaches
a sha256 provenance manifest, runs the offline envelope contract gate, then
server-preflights the entire stream before uploading anything (fail-closed -
a rejection leaves the projection untouched) and uploads in chunks.
Deterministic event ids make re-runs idempotent (the server dedups on
event_id). Buildable slices Y1-Y5 ship here; Y6/Y7/Y8 remain gated. -
Doctrine packs can now ship supporting files to consumer repos - the first
is a docs structural-lint that keeps a project's documentation organized
(#2302, #2864-#2867). A doctrine pack can now carry an arbitrary
addressable file (a script, a blob, a.docx) that travels with the pack and
resolves in a consumer repo with no access to our source tree - the new
built-inassetdoctrine kind, shipping its first member. That inaugural
asset,common-docs-structural-lint, checks that docs land in the right
section, that redirect stubs and frontmatter follow the house contract, and
that retired shadow doc-trees don't reappear; it is wired into the
docs-freshnessCI workflow as a blocking gate and is pulled in automatically
whenever a common-docs artifact is activated. Alongside it, a one-time
structural-sanity sweep redistributed nine misfiledarchitecture/notes,
retired theplans/notes/1.x shadow tree, and repointed every referrer with
zero dead links; the common-docs styleguide gains a machine-parseable
structural_lint_configblock (with aredirect_stub_description_prefix
exemption key) as the lint's single source of truth; and the docs
reference-rewrite helper no longer doubles a destination path (.../x.md/x.md)
on a file-level move. No CLI product code (src/specify_cli) changed. -
Fork packaging hooks (entry-point plugins +
DistributionProfile).
Renamed / private-index forks can customize CLI package identity, upgrade
providers (including built-in PEP 503SimpleIndexProvider), remediation
index URLs, data-freshness TTL, and the public-PyPI notifier via
spec_kitty.cli_package,spec_kitty.upgrade_provider, and
spec_kitty.distribution_profile- without overlayingsrc/specify_cli/**.
Stock public-PyPI /spec-kitty-cliinstalls with no hooks are unchanged.
Packager guide:docs/guides/fork-packaging-hooks.md. -
Doctrine-controlled transition gates - the
for_reviewpre-review gate is now
declared by the repo's active doctrine, not hardcoded to Spec Kitty's own repo
shape (#2595, #2596, #2598; epic #2535 half A). Scope resolution moves behind a
layout-agnosticScopeSourceport (GateCoverageScopeSourcepreserves the
incumbent pytest/JUnit path byte-identically;DeclaredCommandScopeSourceruns a
consumer's declaredreview.test_commandwith baseline-relative verdicts). A
namedGATE_REGISTRYregisters the pre-review engine as the first handler; a
versionedgatesbinding on themission_step_contractreview contract (with an
inerthandler_kindseam for future executable-asset gates) is resolved through
charter activation and dispatched by the inverted_mt_run_transition_gates
hook with deterministic aggregation and per-handler fail-open (two hard-stops
preserved). A new built-inmission_step_contract.graph.yamlDRG fragment makes
the gate fire on the source repo. Behaviour parity is enforced by a golden
captured from the pre-refactor base. Closes the pre-review facet of #2534 (the
always-ontests.architectural._gate_coverageconsumer-repo import is removed -
a consumer never reaches it, even under erroneous activation) and #2330 (the
pytest-layout papercut). Half B (executable gate assets, #2599) is out of scope. -
The pre-review test gate now works correctly for projects that use a custom
test command, not just pytest (#2873, follow-up to #2535 half A). When a work
package moves tofor_review, Spec Kitty runs the project's tests and flags any
failure the change newly introduced - comparing against a baseline captured
before the work started. Previously the baseline side and the review side could
run those tests two different ways, so a project configured with its own
review.test_command(anything other than pytest) got unreliable results: it
could be wrongly blocked on failures that were already there, or - worse - a
genuinely new test failure could slip through unflagged on a clean baseline. Now
both sides run and interpret the tests through one shared path, so the
comparison is apples-to-apples and a new failure is reliably caught. When the
two sides genuinely can't be compared (for example, the test command was
changed in between), the gate now says so with a clear, non-blocking warning
(shown asSOURCE_MISMATCH) instead of guessing - it never silently passes and
never hard-blocks on that case. The captured baseline is now always saved, so
review no longer gets stuck on an uncommitted baseline file. Internally, ~450
lines of dead, duplicated test-scoping code left over from an earlier design
were removed. -
WP runtime-state evicted into the append-only event log (#2684, #2093).
Runtime-mutable work-package state -shell_pid, subtask completion,
## Activity Lognotes,tracker_refs,agent/assignee, and review-cycle
fields - is folded through a single off-axisInnerStateChangedevent and read
from the reduced status snapshot, sotasks/WP##.mdstops mutating on runtime
events (byte-stable dossier content hash, AC-5). Shipped as dual-write behind
the phase-1 flag (_phase1_dual_write_enabled, default off): the event log is
the authority atstatus_phase: 1, while legacy frontmatter remains the default,
sanctioned migration-window fallback. The corpusbackfill -> verify -> cutover,
the unconditional flag flip, and the legacy-fallback deletion are deferred to
follow-up #2816. -
Charter bundle consolidated into an authoritative
charter.yaml(#2773).
The four compiled bundle files (governance.yaml,directives.yaml,
metadata.yaml,references.yaml) plusconfig.yaml'sactivated_*keys
fold into a single git-tracked, authorablecharter.yamlthat becomes the
project's authoritative structured charter;charter.mdbecomes a curated
companion. The bundle manifest bumps to schema2.0.0(charter.yamlis the
sole content-hash input), andconfig.yamlgains a one-linecharter:
pointer. A deterministic, idempotent, fail-loudupgrademigration performs
the fold, sequenced strictly after the3.2.6activation-seed migrations
(verbatim activation copy; the seeds' post-state -configcarries
activated_*- is the fold's pre-state). -
charter activate/deactivate --resynthesizeopt-in eager refresh (FR-007, #2761).
Since the config↔derived freshness seam (#2759) madecharter activate/deactivate
visible to thesynthesized_drgfreshness signal, a routine activation now reports
staleuntil the operator reconciles it. The new--resynthesize/--no-resynthesize
flag (default off) gives operators an escape hatch: passing it re-runs the existing
charter generate+charter synthesizepipeline right after the config write, so the
derived bundle/DRG - and the freshness signal - are fresh immediately, without building
a second reconciliation pipeline. The default (no-flag) path is unchanged: a fast,
config-only write that spawns zero synthesis calls, locked in by a call-count spy
regression test (NFR-001); thespec-kitty upgrademigration andorg_charter
promote_activationspaths are untouched and remain synthesis-free (NFR-003). -
Development-assist test cleanup is now a standing wrap-up procedure (#2557).
A new built-indevelopment-assist-test-cleanupprocedure codifies a proactive,
judge-the-test pass at mission close (identify -> judge -> execute -> scope-the-residue)
that retires or splits the scaffolding tests a mission wrote to drive its own
slices - characterization/parity oracles, before/after timing gates, per-WP
shape /__module__/ delegate assertions - before they land as permanent suite
bloat. It appliesDIRECTIVE_041's keep-vs-delete axis and its
duplicate-test-knowledge-has-one-source rule as the proactive companion to that
reactive failing-test rubric, and is scoped distinct fromDIRECTIVE_025
(pre-existing debt in touched areas). It is wired intomission-wrap-up-sequence
as a new "retire or split the mission's development-assist tests" step after
acceptance (DRG: +1 node, +5 edges), and the wrap-up hand-off / notes summary now
records which tests were retired / split / kept-and-renamed. -
Relocation-hardened architectural dead-code scanners + content-addressed
ratchet allow-lists (#2546, #2547, #2072, #2548, #2077). Thetests/architectural/
ratchet allow-lists are re-keyed off position anchors (line numbers,module::Name)
onto content descriptors, and the 394-entry dead-symbol allow-list onto a
relocation-tolerantSymbolKey- content-only by default, escalating a
byte-identical same-name collision to amodule_pathtier (or fail-closing) via a
live, gate-time collision classifier - so a behaviour-preserving relocation no
longer forces a hand-edit without re-blinding the T004 no-false-negative
invariant. Adds a standing int-to-line-sink meta-guard, a tier-specific
dangling-entry ratchet, and the full production-path bite battery. The relocation
promise is honestly downscoped: relocation-proof for simple single-definition
entries; re-export/facade/fan-out entries staymodule_path-keyed (documented, not
a bug). Ships with a CaaCS change-coupling analysis of the test suite (epic #2071). -
First-class TEMPLATE + loose-contract ASSET doctrine kinds (#2495, #2469).
Org-pack templates are now first-class, graph-addressable, edge-wireable
DRG nodes, and a new loose-contractassetdoctrine kind lets a pack ship
and reference arbitrary addressable files (image,.docx, blob) via a sidecar
*.asset.yamlmanifest (id/mime/path, no blob schema). A single canonical
_NON_AUGMENTATION_ELIGIBLE_KINDSset makes both kinds node-declarable but not
augmentation-eligible or charter-activatable - closing the silent-leak defect
class by construction. Safety is enforced from day one: global URN-uniqueness
forasset:/template:(a single post-merge scan ->duplicate_asset_id/
duplicate_template_id), path-containment (asset_path_escape), and mime
validation (asset_mime_invalid). A new totality guard asserts every
ArtifactKind/NodeKind-keyed mapping table stays exhaustive. The 9 existing
kinds are unchanged. -
External orchestrators can submit structured review outcomes without
forcing lane transitions.orchestrator-api transitionnow accepts
--review-result-jsonwith reviewer, verdict, and reference fields, allowing
guardedin_reviewexits to remain fully host-enforced. The additive contract
change bumps the orchestrator API to 1.3.0. -
Dashboard WP cards show subtask progress (#2504). Cards (and the WP
detail view) previously showed a bare frontmatter count (4 subtasks);
they now show2/4 subtasks(with a ✓ at n/n), counted from the canonical
checkbox rows in the WP body - the same rows the lane-transition guard
blocks on, via a new shared single definition
(core/subtask_rows.py, now consumed by both the guard and the dashboard
so the two can't drift). WPs that don't track completion via checkboxes
keep the plain count badge (no false0/N). The kanban task payload gains
additivesubtasks_done/subtasks_totalfields; the typed-contract
baseline is regenerated accordingly. -
Seven activated governance artefacts are now reachable from the doctrine
graph, socharter activate --cascadepulls them in. Each of these was
shipped active in the default charter pack but had no inbound edge from
anything, which meant no cascade, no traversal, and no way for an agent to
arrive at it except by naming it directly. The authored edges are:
DIRECTIVE_035-> occurrence-classification-workflow,DIRECTIVE_003->
decision-marker-capture,DIRECTIVE_030-> no-parallel-duplicate-test-runs,
DIRECTIVE_030-> red-main-release-discipline,python-conventions->
python-review-checks,atomic-design-review-checklist-> atomic-design, and
structured-prompt-driven-development-> reasons-canvas-writing. This
changes what a cascade brings in for your project: activating one of the
seven source artefacts with--cascadenow also activates the target that
was previously stranded. If you have been relying on a cascade to produce a
specific, narrow set, re-check it after upgrading - the sets are now larger
by design, because the targets were always meant to travel with their
sources.
🐛 Fixed
-
A corrupt, truncated, or wrong-authority
meta.jsonis now rejected loudly by
every mission read path instead of being silently accepted (mission
meta-json-fail-closed-routing; closes epic#3259—#3228/#3229/
#3230/#3240). A mission'smeta.jsonis its canonical identity and
VCS-lock record. Several internal read paths — git ref-advance,implement, and
the acceptance-matrix merge-driver — still decoded it through hand-rolled parsers
that would quietly accept a malformed or wrong-authority file, exactly the
split-brain / wrong-authority failure the metadata-authority work exists to close.
Before, a badmeta.jsoncould let a mission proceed on corrupt identity state
and surface later as a confusing, hard-to-trace failure; now every remaining read
routes through one fail-closed decode seam and fails immediately with a clear,
path-named error. The duplicated VCS-lock comparison — two copies that could
return contradictory verdicts on the same file — is also unified into a single
authority. -
Two blocking CI gates now reflect what a PR actually changed (mission
ci-scoping-gate-reliability;#3008,#3147). Corpus data no longer ships
unguarded (#3008): a PR that changed only non-source corpus data — shipped
doctrine underpacks/**, a mission's planning artifacts underkitty-specs/**,
or charter config under.kittify/**— never triggered the quality workflow at
all, so every corpus-reading suite was silently skipped pre- and post-merge and a
regression in shipped data shipped invisibly. Narrow, discrete trigger globs now
start the workflow on such changes, and a new blockingfast-tests-corpusjob
runs the corpus-reading suites (selected by a@pytest.mark.corpusmarker, so
already-covered suites are not re-run) and gates the merge. The trigger
deliberately excludes lifecycle churn (status.events.jsonl, notes, trace) so it
does not fire on every mission PR. Docs dead-link gate no longer over-fires
(#3147): the blocking dead-link / related-edge check scanned the whole tree and
failed a docs PR for pre-existing broken links in files it never touched. The
blocking check is now scoped to the PR's own changed files (fail-closed on an
unresolvable base ref — a shallow clone or unfetched base errors rather than
passing trivially), while the unfiltered whole-tree scan is retained as the
non-blockingpush: mainbackstop so genuine repo-wide rot is still surfaced. -
The
charterandsyncmodules are cleared of their Sonar maintainability
backlog, including a super-linear-backtracking regex (mission
charter-sync-sonar-remediation;#3232-adjacent). 80 SonarCloud findings
across the two modules — 27 over-complex functions, 15 duplicate literals, 20
malformed suppression comments, unused parameters, too-many-parameter
signatures, and one ReDoS-class regex flaggedBLOCKER— were resolved with
behavior-preserving refactors and no new suppressions. Over-complex
functions were brought to the ≤15 cognitive-complexity ceiling via tested
helper extraction (each helper carries a focused test); duplicate literals
became named constants; thetoken_budgetheading regex was rewritten to
remove its./\sambiguity (proven match-equivalent by a characterization
test); and the two too-many-parameter event emitters (emit_wp_status_changed
andemit_token_usage_recorded) were slimmed by bundling their optional tail
fields into typed params objects (WPStatusChangeMetadata/
TokenUsageMetadata), with every call site migrated to the params object so
the tail stays fully type-checked rather than routed through an untyped
**kwargsbag. Two findings are Sonar false-positives (a Pydantic
PrivateAttrand an already-single-return method) with no clean code fix;
they remain for a SonarCloud UI won't-fix. No runtime behavior changes. -
Signature change for out-of-tree callers of two exported event emitters
(missioncharter-sync-sonar-remediation;#3317).emit_wp_status_changed
andemit_token_usage_recorded(re-exported from thesyncfacade) no longer
accept their optional tail fields (causation_id,force,evidence,
run_id,provider,model, …) as individual keyword arguments. Pass a
WPStatusChangeMetadata/TokenUsageMetadataobject via the keyword-only
metadata=parameter instead. All in-tree callers are already migrated; only
external plugins that called these helpers with the old keyword tail are
affected. -
Restored an honest red CI gate:
main's two standing reds now go green for
the right reason, andmove-task's pre-review gate stops crying wolf
(test-layer only; epic#3260— "a red gate must mean a real regression").
This is a CI/gate-correctness pass with no runtime or behavior change — no
src/change, no__init__.pytouch, no version bump — so nothing you install
or run changes. What changes is that the project's own signal is trustworthy
again:- The blocking
regression testsjob goes green because the last resident
red-first reproduction (#2782) was retired. That repro assertedagent mission create --jsonsynchronously emits adirect ingress skipped
diagnostic on stderr — a contract the deferred/offline sync architecture
cannot satisfy (mission-create queues its lifecycle event and dossier bodies
to the offline outbox and returns without an in-process ingress attempt, so
the diagnostic never fires there, even with consent recorded). It was
redesigned into a green functional test of the architecturally-honest
mission-create contract (rc=0+ strict-JSON stdout + no diagnostic prose on
stdout + the#2254drift guard), moved back beside its strict-JSON siblings,
and un-markedregression.-m regressionnow collects nothing, so the
blocking job passes. (The diagnostic's real firing is still proven at the
resolver seam by a separate test.) - The
integration-tests-clijob goes green because a drifted test was
locking a since-corrected contract at thecommit_to_branchseam. Following
#3269/ commit793872a19, only a genuine empty changeset maps to
unchanged; a rejecting pre-commit hook is a real failure. The test was
rewritten to assert the corrected contract (hook rejection re-raises and
leaves the artifact dirty). move-task's pre-review gate stops reporting phantom "new failures" —
two false reds that made the reliability gate untrustworthy (both children of
epic#3260).tests/doctrine/test_hatch_build.pynow guards its transitive
import withpytest.importorskip("hatchling"), so the pre-review gate's
ephemeral baseline venv (which lackshatchlingoutside thetestextra)
skips it cleanly instead of hard-failing collection; the normal test job
installs the extra and still runs every test (#3224). And
test_inline_meta_read_gatenow relativizes scanned paths against the scanned
tree's own root rather than the gate file's location, so a cross-tree
(git-worktree baseline) scan no longer produces absolute paths that silently
over-countmission_metadata.pyas a violation (#3241, PART 2).
- The blocking
-
The #2804 gate-artifact merge invariant is guarded again at the driver level,
and the doctrine module's Sonar maintainability debt is cleared (mission
gate-artifact-merge-driver-unit-gate;#3232). A prior refactor (b04da00e1)
deleted the unit test that pinned "a merge must never reset a filled acceptance/
issue gate artifact back to a scaffold placeholder", and it was never restored
because it had been written for a whole-file heuristic the row-union authority
model (#3076) replaced. This restores that guard as a fast, in-memory unit gate
over the shipped row-union reconcilers: a filled/accepted criterion is never reset
to the scaffold marker, the accepted evidence handle survives a merge (including
inside a structured conflict marker), and the merged verdict stays in its
admissible domain — each with a falsifying control. Bundled opportunistically:
thesrc/doctrine/Sonar backlog is cleared — 37 duplicate-literal findings hoisted
to named constants, 7 over-complex functions reduced to the ≤15 cognitive-complexity
ceiling via tested helper extraction, and 3 minor smells resolved — all
behavior-preserving with no new suppressions. (One 183-complexity function is
deferred to a dedicated mission.) -
spec-kitty auth login(and any caller of the SaaS URL helper) now points
you at the real hosted service whenSPEC_KITTY_SAAS_URLis unset, not a fake
placeholder (#3297, closes#3296). Previously, running the command
without that variable set told you toSet it to your spec-kitty-saas instance URL (e.g. https://api.spec-kitty.example.com)— a.example.complaceholder
that does not resolve, so a first-time user who copied it got a dead URL. The
guidance now names the actual hosted URL,https://app.spec-kitty.ai, so the
example is copy-paste-usable; self-hosted instances still override via the env
var exactly as before. -
Timestamps that Spec Kitty writes into your project no longer record local
time while labelling it UTC (missionkernel-clock-single-door;#3305,
closes#3289, owns the closed#3288). Roughly twenty places across the
tool — charter backup filenames, status-event stamps, auth-doctor report times,
and other persisted "now" values — read the machine's local clock and then
tagged the result as UTC. On any machine not set to UTC the stored time was
simply wrong: a backup taken at 09:00 CET was filed as…T09-00-00and read
back as 09:00 UTC, an hour or two off. Every such value now comes from one
canonical aware-UTC producer, so stamps are correct and consistent regardless
of the host timezone. Under the hood all wall-clock reads now go through a
singlekernel.clock"door" that every package can import, and a repo-wide CI
gate blocks any new rawdatetime.now()/time.time()read from
reintroducing the drift — but the change you can observe is simply: the
timestamps are right now. -
spec-kitty doctor auth --fixnow reports a real disk failure instead of
quietly claiming "nothing removed" (missionsonar-bug-blocker-remediation).
When breaking a stale auth lock hit a genuine I/O error — a full or failing
disk —force_releasemislabeled it as ordinary lock contention, so the
command told you nothing was wrong while the real fault went unseen. It now
lets genuine filesystem errors propagate while true lock contention still
returns cleanly, matching the rest of the locking layer. -
Activating a slug-named hub directive (e.g.
use-c4-model-techniques) in a
charter now resolves to the real doctrine node instead of a dangling
identifier (#3009,#3298). The directive-id normalizer folded numbered
slugs (024-...→DIRECTIVE_024) but left slug-named directives hyphenated
and uppercased (use-c4-model-techniques→USE-C4-MODEL-TECHNIQUES), which
is not the artifact's canonical node id (USE_C4_MODEL_TECHNIQUES). So
activating or referencing such a directive by its slug silently pointed at
nothing. The normalizer now folds hyphens to underscores, matching the
canonical node id — so slug-hub directives activate and cascade like any other. -
spec-kitty tracker sync publishon a local (beads/fp) binding now
prints a clear error instead of crashing with a Python traceback (#3168).
Local providers have no snapshot-publish transport, but the command delegated
to the backend unconditionally and hit an uncaughtAttributeError, which the
CLI let escape as a raw traceback. It now exits with a clean
"not supported for local providers — usetracker sync pushinstead" message. -
charter generateis now idempotent — a second run no longer degrades the
compiled catalog (#3292). Two independentactive_languagescomputations
fed a feedback loop:generatestampedcatalog.languages: []for a
language-agnostic charter, and the language-scope gate then read that empty list
back as an authoritative "admit no languages", degrading language-scoped
styleguide/toolguide titles and summaries to a"Definition unavailable in bundled doctrine"placeholder on the next run.active_languagesnow has a
single authority and an empty result means "no signal → admit all" (round-tripped
as an absent field, not a persisted[]), so repeated generates are byte-stable. -
The reason
spec-kitty sync doctorgives for a refused tracker egress can
no longer disagree with what the gate actually enforced (#3287,#3291).
The gate decided whether to refuse from one source but re-derived the why —
and the remedy shown to you — from a second, independent consent lookup that
could drift from the enforced answer, and it repeated a full git-identity
resolution on every gated sync. The reason now comes from the same evaluation
that enforces the decision: a refusal's explanation always matches the
refusal, a consent record that cannot be read is reported as its own state
instead of masquerading as "no consent recorded", and a gated
tracker syncresolves your project's consent once instead of twice. -
A spec commit that genuinely FAILS is no longer silently reported as
"unchanged" (#3269). When agit commitfailed for a real reason — a
rejecting pre-commit hook, a lock error —safe_commitcollapsed every
non-zero exit onto the same "nothing to commit" path, sospec-commitprinted
"Spec artifact(s) unchanged, no commit needed" and the spec silently never
landed. Emptiness is now decided by the staged tree (git diff --cached),
not by matching git's output text: a genuine no-op is still reported as
unchanged, while a real failure surfaces as an error carrying git's own
diagnostic — even when a failing hook prints its own "nothing to
commit"-shaped message. -
spec-kittyno longer crashes when an arbiter override lands on a
conflict-marked review-cycle file (missionverdict-seam-boundary-hardening;
#3244). An arbiter override written against areview-cycle-N.mdthat
still carried unresolved merge-conflict markers used to raise instead of
resolving the latest cycle; the cycle number is now read from the filename
alone, so the override applies cleanly. -
SaaS tracker retry/poll tests no longer flake on CI, and a new gate keeps
the whole bug class out (missionsync-sleep-count-3136;#3136). These
tests asserted ontime.sleepcall counts and failed non-deterministically
with errors likeExpected 'sleep' to be called once. Called 179 times.on
test nodes a change never touched — because patching the shared stdlib
time.sleeprecorded sleeps from anything else running in the same test
worker. Building on#3187(which gaveSaaSTrackerClientits own
self._sleepseam), the client now also bindsself._monotonic/
self._randbelow, so its retry/poll timing and jitter are fully isolated from
process-global state. A new architectural gate
(tests/architectural/test_shared_module_object_patches.py) refuses any future
test that patches a shared module object read by a count/equality assertion, so
this class of flake fails review instead of shipping. -
Restored green CI on two suites that regressed after the single-
PACKS_ROOT
/ charter-activation unification (missionresolution-activation-foundation).
tests/review/test_pre_review_gate_integration.pyand
tests/merge/test_profile_charter_e2e.pystill built bare fixtures that never
provisioned the default charter / mission-type activations, so once resolution
moved to a single fail-closedSPEC_KITTY_PACKS_ROOTread the pre-review gate
degraded tono_coverage(never blocking) and mission-template resolution
raisedMissionsRootNotFound. The fixtures now seed the default activations
via the production provisioner and mirror the built-inmissionsleaf under
the packs root. Test-harness only - no product behaviour changed; the
fail-closed resolution the sibling mission introduced is intentional. -
Commands run on a merged coordination mission no longer crash (
#3086).
Once a coordination mission was merged, later commands against it - creating a
retrospective, resuming implementation, or checking its status - failed
outright, and this hit every merged coordination mission. Merging now
finalizes the mission cleanly, so it stays usable afterward. -
decision widenno longer lets one mission's decision travel under
another mission's identity (missionegress-refusal-consolidation-3110;
#3111). Previously, ownership was resolved from whatever the decision
ledger it was handed reported rather than from the mission you actually
named, so a decision owned by mission A could be widened under a request
naming mission B and have the egress consent gate treat it as consented -
laundering A's identifying text out under B's team/token. Widening now
refuses whenever the acting mission and the decision's recorded owner
disagree. The governing rule: identity is a detector, never an actuator. -
The consent-refusal gate deciding whether project-identifying text may
leave your machine now has one definition site instead of two
near-identical copies that could drift apart (#3110).
saas_client/egress_consent.pyandtracker/egress_consent.pyare
replaced bysrc/specify_cli/egress.py. -
Listing missions on Python 3.14 no longer reports a permission error as
"no missions found" (#3177). An unreadable mission directory was
silently dropped instead of being recorded, so the CLI told you togit pullfor what was actually anEACCES. It now reports the directory as
unreadable; unaffected on the CLI's supported 3.11-3.13 interpreters. -
Internal: a stale docstring in the saas-client/egress invocation adapters
no longer claims re-registration only replaces a factory or resolver "when
the qualified name matches" (#3109). Both always replaced
unconditionally; only the documentation was wrong, and only the control
flow used to reach the same assignment differed. Behavior is unchanged. -
UnknownMissionTypeErrorno longer labels an activated mission type both
"unknown" and "registered" (mission
doctrine-consumer-surface-missions-extraction;#3183). When a mission
type is activated but has no loadable profile, the error now states that
distinctly, instead of the contradictory "Unknown mission type ''.
Registered types: ". -
The built-in-doctrine dead-path architectural gates no longer silently lose
coverage when a gate file is split, and the shippeddoctrine-daphneprofile
no longer carries a repo-localsrc/doctrine/graph.yamlreference (mission
doctrine-consumer-surface-missions-extraction;#3036,#3182). The
src/-wide gates and the doctrine-content gate are split by actual scope with
their discriminator proofs driven from planted synthetic fixtures, and the
relocated-tree cross-link scan now covers both shipped roots. -
The dashboard's Charter page no longer misreports a compiled-only project
as having no charter (missiondoctrine-charter-split-unification;
#3150). Before this fix, a project with a compiled
.kittify/charter/charter.yamlbut no display-onlycharter.mdcompanion
showed "no charter" in the dashboard sidebar and its API - both the artifact
scanner and the charter API endpoint still keyed presence on the legacy
charter.mdfile. Presence resolution now preferscharter.yaml(the read
authority) and falls back tocharter.mdwhencharter.yamlhas not been
compiled yet, across the dashboard, analysis-report, and
retrospective-policy surfaces - so acharter.md-only project that never
rancharter sync/compile still reports a charter, and a compiled project
still survivescharter.mddeletion; the prose body the Charter page
displays still comes fromcharter.mdwhen one exists. Two exceptions are
intentionally pinned, not
residual gaps:charter context's prose-presence gate (documented, C-003)
andcharter status's legacycharter.md-only fallback for
pre-consolidation bundles (_status_collectors.py, pinned by
test_status_collectors_legacy_md_shape.py, FR-006). -
A corrupt or malformed
meta.jsonnow always fails closed with a clear
error, never a raw crash (missiondoctrine-charter-split-unification;
#3140). Previously, dozens of call sites across mission-status,
coordination, migration, and CLI commands could leak an unhandled
ValueErrorwhen a mission'smeta.jsonwas corrupted or shaped
unexpectedly, surfacing as an opaque traceback instead of an actionable
error. Every one of those readers now routes through a single typed
MissionMetaReadError(or a documented silent-empty fallback where that was
always the contract), so a broken mission's metadata degrades predictably
instead of crashing the command you ran. -
Internal:
doctrine-charter-tests.ymlCI now actually covers the charter
CLI command layer, and the CLI-reference parity check runs instead of
silently skipping (#3149,#3107,#3102). A change under
src/specify_cli/cli/commands/charter/**previously wouldn't trigger the
workflow named for it; the docs-parity gate had pointed at a reference file
that no longer existed, so it never actually ran. -
Approving a work package after a rejection now actually sticks - no
override flag required (missionreview-verdict-write-integrity;#3044,
#2275,#2996,#990,#2697,#2646). Before this fix,
move-task --to approvedon a previously-rejected WP advanced the lane but
wrote no record of the approval - the stalerejectedverdict stayed
authoritative, somove-task --to doneandspec-kitty mergekept blocking
on it, forcing--skip-review-artifact-check(an arbiter-override flag) on
every ordinary reject -> fix -> approve cycle. Separately, the rejection
writer accepted any file as "feedback" without checking it wasn't itself a
prior cycle's own review artifact (by path or by a renamed copy), so a
mistaken re-submission (a reviewer accidentally re-pasting the same feedback
text) could silently duplicate an old review under fresh, fabricated
frontmatter. Neither writer ever git-committed its output under any
topology, so even a successful write could land untracked. Now: approving a
rejected WP through the normal path persists and commits a real
verdict: approvedreview-cycle artifact with a genuine reviewer identity;
the override flag still works for actual arbiter decisions but is no longer
needed for an ordinary approval; a rejection's feedback source is refused
if it's the same or a duplicate of a prior cycle's own artifact (a check
that a deliberate one-character edit still defeats - it targets accidental
duplication, not a determined actor); and bothmove-taskwrite paths now
commit their review-cycle artifact when auto-commit is resolved on
(--auto-commit, orauto_commit: true/default in.kittify/config.yaml),
surfacing a clear error if the commit itself fails rather than silently
discarding it. Under--no-auto-commit, or withauto_commit: false
configured, the write still lands but is not committed. A third writer,
_persist_review_feedback, does not yet thread a commit router at all.
agent tasks status's stale-verdict display (#2646) is confirmed to close
as a side effect of the durable writer, with no separate code change needed. -
A work package's current verdict now has a single authority that wins on
disagreement - the status event log, not a review artifact's frontmatter
(missionreview-cycle-verdict-seam-rebuild-01KZ2W7W; ADR
2026-08-03-1). Before this fix, "is this WP approved?" had no single
answer: some readers (the kanban board,move-task's review-readiness
check) parsed areview-cycle-N.mdartifact'sverdict:frontmatter field
directly, so a reader could disagree with the event log about which verdict
was current if the two ever drifted. Now the event-sourced verdict wins on
any such disagreement; frontmatter reads remain in place as a fallback for
the case where the event log carries no opinion (for example, legacy
coord-topology missions that predate this seam), and no new
frontmatter-reading path was added - the merge gate still drives its
readiness loop off a frontmatter reader for that no-opinion case, by design.
The review-cycle artifact remains authoritative for what the reviewer
said - the reviewer's prose, affected files, reproduction command - never
for which verdict is current once the event log has an opinion to give. -
An arbiter's override decision now durably persists and clears the merge
gate on its own - no separate flag or manual step required afterward
(missionreview-cycle-verdict-seam-rebuild-01KZ2W7W; ADR
2026-08-03-1). Before this fix, the arbiter override writer never
git-committed its output under any topology, so an override could be
present in a working tree and still vanish from a fresh clone or a
branch switch - the exact gap that made--skip-review-artifact-check
feel unreliable even when it had "worked." Now the override is committed
through the same seam every other verdict-relevant write uses, so it
survives a fresh clone and the merge gate accepts it without repeating the
override flag. -
An arbiter override can no longer be mistaken for, or silently absorbed
into, a genuine reviewer approval (mission
review-cycle-verdict-seam-rebuild-01KZ2W7W; ADR2026-08-03-1). Before
this fix, nothing distinguished a knowingly-overridden standing rejection
from an approval a reviewer actually performed - both could present the
same way to a consumer reading only the latest verdict. Now the override is
recorded and read back as its own first-class outcome (a stated actor and
reason, event-sourced on the same partition as the record it annotates),
never fabricated as an approval review nobody performed. -
Applying a charter pack no longer leaves your project worse off than doing
nothing (missioncharter-pack-usage-journey;#3104,#3105). Before
this fix,spec-kitty charter pack apply <pack>on an unconfigured project
disabled the safe generic-agent dispatch fallback - so the next unmatched
spec-kitty dispatchhard-failed withROUTER_NO_MATCH- andcharter context/charter statusstill reported the charter as "not found" because
they gated on the display-onlycharter.mdthatapplynever writes. Now:
applying a pack without compiling keeps the warned generic-agent net
engaged (an unmatched request still falls back, neverROUTER_NO_MATCH), and
checking that adds no extra doctrine-catalog load to the dispatch hot path
(#3118);apply --compiledoes the apply and compile in one step; after you
compile,charter context/charter statusreflect the pack's activated
governance and keep working even ifcharter.mdis deleted (the read
authority is the compiledcharter.yaml); and the governance resolver reports
the activated directive set rather than silently falling back to all
built-in directives. The documentedspec-kitty analyzesurface and the CLI
are now guarded to stay in agreement (#3096). Deliberate behaviour
change: the dispatch net now keys purely on whether a compiled bundle or a
routable profile/org pack exists, not on every activated doctrine dimension -
so a project that has activated only glossary packs, directives, or other
non-routing doctrine (no bundle, no routable profile) now falls back to the
generic-agent net too. This narrows the broader composite-dimension check the
#3064fix introduced above; it is a tested, recorded change, not a silent
regression. -
Contract note -
charter context --json(project_charter.present). The
read authority for the JSON charter-presence surface moved from the
display-onlycharter.mdto the compiledcharter.yaml: a project that has a
charter.mdbut never compiled now reportsproject_charter.present: false
(the newcharter_md_present/charter_md_pathkeys expose the display file
separately, so the information is additive, not lost). The payload also now
carries a top-levelcontext_schema_versionstamp (a versioned tracking
contract; a full shape freeze + deprecation policy stays deferred to the
#2519activation-surface work) so an external consumer can detect a shape
change instead of breaking silently. External--jsonconsumers should read
charter.yamlas the authority-of-record and pincontext_schema_version
(#2787). -
A non-terminating test now fails loudly instead of hanging the CI job
(missionverification-trust-3115;#3115,#3113).pytest.ini
registered atimeoutmarker but set no timeout, so a test that never
returned stalled its whole shard rather than failing - a mechanism reporting
nothing where it should report a failure. The fast-test CI jobs now carry a
default per-test timeout (--timeoutwith the signal method), and a new
architectural gate (test_ci_fast_jobs_have_timeout.py) keeps the flag from
being silently dropped, with two documented exemptions (the real-wheel docs
build and the-n0real-port orphan-sweep, where a signal mid-teardown is
unsafe). Also hardens the terminal-render width fold, the egress-consent
boundary, and thetests/syncleak-guard so each is provable rather than
asserted. No product code (src/specify_cli) changed - this is a
test-trust and CI change; Windows and local runs still get no per-test
timeout (tracked by#3143). (#3130,#3136referenced.) -
Fixed:
synccould deliver one project's events to another project's
workspace, including projects that never opted in (#3030). A machine
keeps one shared local event journal across every Spec Kitty checkout on
it. Sync's consent check only asked "is sync enabled for this checkout?"- once yes, it delivered the entire journal, so a single opted-in
project could ship another, unrelated project's events and metadata to the
hosted server. Every path that reads from the journal for delivery is now
gated per-project, consent is checked before events leave the machine
rather than only at the checkout level, and an unresolvable consent check
now fails closed (refuses to send) instead of failing open. If you use
hosted sync for more than one local project, only the project(s) you
explicitly opted in now have their data sent.
- once yes, it delivered the entire journal, so a single opted-in
-
Generated agent guidance reads grammatically again (mission
charter-delivery-finish-context-degod;#3082). The "verbatim-or-fetch"
disclosure lines every agent receives in charter context were frequently
ungrammatical - e.g.When you designing or reviewing significant code changes, run this command. The authoredwhenclause is now normalized into
the closed contract lead-in set (When you are about to ...) for every clause
shape, and the prompt-governance contract is asserted per stanza so a
regression can't hide behind another matching line. (A second, independent
render surface insection_bodies.pyis tracked separately as#3093.) -
DRG document-writer blind spot closed (mission
doctrine-delivery-activation;
#3075,#2977). All threeDRGGraphdocument-emit sites
(rewrite_opposed_by,charter.synthesizer.project_drg, and the
pack_assemblerforce-dedup path) now route through the canonical
graph_document_to_dictand are registeredDocumentWritermembers, guarded
by a new non-vacuous writer-discovery gate that fails on either an
unregistered dict-literal or a.model_dump()-shaped emitter. The
pack_assemblerpath was additionally crashingyaml.safe_dumpon the
Relationenum (and leaking withheld fields). The repository surfaces are
typed via a newArtifactRepositoryProtocol, removing 12
# type: ignore[attr-defined]. -
DRGGraphSchemaErrornow surfaces as a structureddoctrine validate
issue instead of an uncaught traceback, andAssetRepository.source_path
no longer disagrees withgetfor a manifest that failed validation (a base
_post_validatesuccess-path hook records the source path only after
validation) (#3062). -
The
test_every_load_deliveryfixture is now hermetic to an ambient
gitignored.kittify/charter/context-state.json, eliminating a local-only
false-red on thefirst_load/bootstrap assertions. -
Read-side placement seam: mission reads route through the kind-aware seam and
new bypasses are structurally impossible (#2922, #1878; also #2921, #2966
part-1). The write side already failed loud when an artifact's coordination
partition was gone, but ~50 modules still read mission artifacts through the
low-level kind-blind resolvers, so a read could silently substitute the primary
checkout for a deleted coord branch:- Every fail-loud-appropriate caller (72 sites, per a per-site classification
ledger) now reads viaPlacementSeam.read_dir(<kind>)with the correct
artifact kind. Coord-partition reads - a lane-based merge's event log, the
decision-log companion read, the doctrine synthesizer - now fail loud instead
of treating a deleted coordination branch as healthy. - 16 diagnostic/audit/SaaS-facing readers (dashboard scan, dossier API,
retrospective summary, status aggregation, and friends) stay lenient by
design and are recorded as justified allow-list entries - reporting surfaces
must not start raising. - A new whole-tree AST gate reds on any future direct kind-blind read in a
non-sanctioned module, reusing the same scanner as the write-side gate. The
allow-list is shrink-only: a staleness twin-guard reds until an entry that is
no longer needed is deleted. - Behavior is unchanged for the healthy case;
repair_lane_mismatchno longer
duplicates frontmatter into the document body (#2921), and
backfill_runtime_state's mission-id read is anchored on the PRIMARY leg. - Fail-loud does not mean crash on the paths that exist to recover from a
broken coordination branch.spec-kitty implement --recovernow reports a
COORDINATIONfinding carrying thedoctor coordination --fixremediation
instead of tracebacking,spec-kitty mergeaborts with a readable message
before any state change rather than mid-merge, andagent workflow review
degrades to an advisory warning instead of aborting while computing one. spec-kitty verify's mission-directory probe is now CWD-invariant: invoked
from a lane worktree it resolves the main repository'skitty-specs/rather
than reporting the mission as absent. Both production callers already
anchored on the main repo, so this is observationally a no-op for them.spec-kitty acceptno longer rewinds finished work packages to
claimed(#3013, #2985). The birth-cutover seed anchored its
planned -> claimedcarrier on a work package's earliest transition,
which for a force-jumped or pruned WP is its terminal one. The seed
then collided on timestamp, and because the reducer sorts by
(at, event_id)the hash-derived seed id outranked the real ULID - folding
adoneWP back toclaimed, so a secondaccepton an unchanged tree
did not converge. (This corrects the mechanism recorded on #2985: it is the
sort key, not append order.)acceptis also now event-count neutral on
missions whose runtime state is already canonical. Already-corrupted logs
do not self-heal - they still need the #3003 corpus regenerate, which
this unblocks.doctor mission-state --fixno longer destroysreview_result, log
order, or dropped duplicates (#3003)._build_canonical_rowis a closed
allowlist that omittedreview_result- a first-classStatusEventfield
and a hard FSM guard, since every transition out ofin_reviewis
rejected without it. Repairing a corpus therefore converted valid history
into events the reducer could no longer validate: 268 transitions across 44
missions, with none failing beforehand. The loss was worst-shaped, because
DoneEvidence.reviewmirrors the payload only on approval - so the
changes_requestedrows were the irrecoverable ones. The allowlist is now
gated againstStatusEvent.__dataclass_fields__so a newly added model
field cannot be dropped the same way. Two further fixes: rows now sort on
atortimestamp(lifecycle and retrospective rows date themselves
with the latter, so 839 of them were collapsing to""and being hoisted to
the head of an append-only log), and a dropped duplicateevent_idrow is
quarantined rather than only hashed.- The classification ledger is now the mechanical authority for the
stay-lenient allow-list - the gate parses it, so the doc and the gate cannot
drift apart silently. The gate additionally resolves import aliases, and its
coverage bounds (including the unpolicedprimary_feature_dir_for_mission)
are stated explicitly rather than implied.
- Every fail-loud-appropriate caller (72 sites, per a per-site classification
-
Almost the entire test suite was being skipped on pushes to
main(#2957).
CI decides which test jobs to start by looking at which paths a change
touched. That is a sensible optimisation for a pull request, but it was
applied to pushes to the protected branch as well - so a merge whose diff
happened to match none of the path filters started 10 of 50 test jobs and
left 31,547 of 33,822 test cases (93.3%) running nowhere. This was not
theoretical: four test files that pin frozen contracts were failing onmain
whilemainCI reported green, because the only jobs that would have run them
were filtered out. Path filtering is now a pull-request optimisation only. A
push to a protected branch starts 49 of the 50 suite jobs and leaves no test
uncollected; pull requests are unchanged. A new check
(tests/architectural/test_ci_collection_completeness.py) fails if any
collected test is left with no job that would run it on a push tomain. It
has no baseline and no allowlist - the only way to satisfy it is to make a job
actually run the test. -
Doctrine packs could declare things that quietly had no effect (mission
doctrine-silence-guards; ADRs 2026-07-26-1,
2026-07-26-2,
2026-07-26-3).
The doctrine layer's characteristic failure was silence rather than error: a
declaration was accepted, validated, reported as loaded, and then dropped. Every
case below now either works or fails with a message naming what was wrong.- Five of the sixteen artifact kinds were missing from the doctrine-graph
extractor -anti_pattern,asset,glossary,glossary_packand
glossary_scopewere dropped whenever the graph was rebuilt. All sixteen are
handled, and the mapping is now checked for completeness instead of being
maintained by hand. - Two of the twelve org-pack directory names crashed the merge outright
with a bareKeyError(mission_types/,glossary_packs/). One consequence
worth naming: the shipped test fixture calledaugment-all-kinds-packcould
not be merged at all, so the "all kinds" fixture never exercised all kinds. - Edges derived automatically from an org pack's agent-profile fields never
reached the graph. The producer emitted fully-qualified<kind>:<id>
endpoints; the code that consumed them looked up bare ids. Nothing matched,
so 100% of that path's output was discarded, silently. - An edge from a built-in artifact to a pack artifact was dropped with
neither a warning nor an error. It now resolves, or reports a typed error
naming the endpoint it could not resolve. - A bare name that referred to a styleguide was turned into a made-up
directive:<name>node rather than thestyleguide:<name>the author
meant. - Unrecognised keys in agent profiles and in graph nodes and edges were
silently ignored, so a typo or a retired key left an artifact that read
one way and behaved another (see Breaking Changes). - Activating the
doctrine-daphneagent profile pulled in 76 related
artifacts but not the one procedure the profile itself says it runs. The
edge to that procedure used a relation type (applies) that nothing
traverses, and it was the procedure's only inbound edge, so the profile's own
operating procedure was unreachable. The edge is retyped torequires, and
authoring anappliesedge into the shipped tree is now rejected. - Error messages sent operators to files that do not exist - an inline
reference rejection pointed atsrc/doctrine/graph.yaml, split into per-kind
fragments long ago, and two operator-facing skill documents told authors to
read asrc/doctrine/<kind>/shipped/layer that has never existed on disk.
Both are corrected and both are now guarded. - The generated schema for the docs structural lint had widened to "any
object", so a malformed lint configuration validated cleanly and the lint
ran on settings nobody had checked. The generator now emits the real
ten-key contract. - The documented example for declaring agent-profile lineage used a syntax
that exists nowhere in the vocabulary (urn:profile:...). Anyone who copied
it got a declaration that merged without complaint and produced no edge. The
example is corrected inAGENTS.md/CLAUDE.mdand in
the org-pack authoring guide, and
the accepted forms are now enforced at merge time.
- Five of the sixteen artifact kinds were missing from the doctrine-graph
-
spec-kitty agent tasks mark-statuscould not find subtask ids written the
way the shipped template tells you to write them (#2962). The command
resolved a subtask id by matching a row shape intasks.md- a checkbox, a
pipe-table row, or an inlineSubtasks: T001, T002list. The shipped
software-devtemplate instructs authors that subtask rows are reference
rows and explicitly not checkboxes, so atasks.mdthat follows the
template matched none of those shapes and every id came backNOT_FOUND.
That blocked the review workflow on every work package of every mission using
that template. The command now falls back to the authoredsubtasks:roster
in work-package frontmatter - the same list the lane-transition guard already
treats as canonical, so the two surfaces agree on what a work package's
subtasks are. The fallback runs last, so every row shape that worked before
resolves exactly as before. -
Placement-port residuals: partition routing is now enforced by the port, not
by caller discipline (#2923, #2924, #2926, #2932; epic #2931). Closes the
residuals deferred from the coord-write-placement-closure merge (PR #2920):- The sole
status_phasewriter (_flip_phase) resolves its write target
through the placement port and fails closed with a typed
PlacementMismatchError(writing nothing) if the resolved PRIMARY home
disagrees with the caller's directory - coincidental correctness becomes an
enforced invariant. - The two-target legacy cutover reads
tasks/frontmatter from the PRIMARY leg
while the status event write stays on COORD. - Three hand-rolled "resolve write target, else degrade" copies collapse into
oneresolve_write_target_or_degradehelper (each caller keeps its own
fail-open / fail-closed policy), and the retrospective trace loader degrades
to[]on a deleted coordination branch. A latent bug is fixed along the way:
a caller-supplied coordination branch that was silently discarded in the
pre-meta.jsonbootstrap window is now honored. - The write-side placement scan no longer blanket-exempts the
migration/
subtree, restoring whole-tree precision. The remaining deferred gate/contract
reds were already green on the current base and are verified in-mission.
- The sole
-
The merge review-readiness check no longer writes a stray
status.json
during a merge (#2934). The check that looks for a rejected review before
merge was reducing mission status through the writing materializer, so it
left astatus.jsonsnapshot on disk as a side effect. On a mission with no
status events yet, that snapshot had no matchingstatus.events.jsonl(the
append-only event log that is the real source of truth), and the merge could
commit the orphaned snapshot on its own - the invalid "snapshot without its
event log" state the status doctor flags. The check now reads status without
writing anything; a gate reads, it does not persist. Includes the regression
test and a de-mocked planning-only merge test that exposed the shape. -
Running Spec Kitty non-interactively (agents, CI, piped input) no longer
hangs waiting for a prompt that will never be answered (#2876; extended in
#2912; landed by #2910). UnderSPEC_KITTY_NON_INTERACTIVE=1,spec-kitty planand
specifyreached their discovery interviews and called a blocking prompt with
no non-interactive gate anywhere in the call graph, so an open-but-silent
stdin pipe made them wait forever. The interviews now honor the contract: they
still open every Decision Moment and write the decision index, but take
defaults and record each question as deferred instead of prompting - no prompt
is ever emitted non-interactively. A single authority,core/env.is_interactive()
(precedenceSPEC_KITTY_FORCE_INTERACTIVE>SPEC_KITTY_NON_INTERACTIVE> a
real TTY), now backs that decision, and #2912 routed the four other divergent
prompt-gates -init, themergehollow-review confirm, theintake
candidate picker, anddoctor- through it too, so every prompt obeys the
same rule (withdoctorkeeping its CI-environment veto layered on top). Also
fixes the garbled interview hint menu where Rich was eating[enter]/[text]/[d]
as style tags. -
Merging an accepted mission no longer discards the acceptance and
issue-matrix evidence recorded when it was accepted (#2804; scorer hardening
#2912; landed by #2910).acceptance-matrix.jsonandissue-matrix.mdare filled on the
target at accept time while the mission branch keeps empty scaffolds; the
squash integration rangit merge --squash -X theirs, so the empty scaffold
won the add/add conflict and the merged history kept blank gate artifacts -
losing the audit trail exactly when a reviewer needs it. Two new custom merge
drivers (merge-driver-acceptance-matrix/merge-driver-issue-matrix) now
keep whichever side actually carries evidence - scored, not fixed-side, so it
is correct whether the fill happened on the target or in a lane, with ties
going to the target where accept ran. The scorers were then hardened (#2912) to
read the scaffold marker from the field the scaffolder actually writes, and to
resolve verdict/title columns by canonical header rather than fixed position,
so a reordered or minimal matrix scores correctly. Seeded into.gitattributes,
theinitseed, and an upgrade migration for already-initialized repos -
following the same pattern as the #2709 meta/traces drivers. -
Lifecycle gates no longer emit a confident-but-wrong verdict when run from
the wrong worktree, and a misclassified lock write no longer blocks a claim or
merge (#1834, #2885, #2882, #2795). Every lifecycle gate now receives an
explicit execution context - which tree, which ref, which phase - and refuses,
recording a clear cannot-evaluate diagnostic instead of a pass/fail, when that
context is missing or inconsistent, rather than silently inheriting the
caller's working directory and judging a fact about one worktree against
another. Separately,spec-kitty implement/mergeno longer wrongly report a
work package as blocked because Spec Kitty mistook its own planning-partition
lock write for a dirty tree (the #2795 claim-blocker, now topology-agnostic).
Internally, eleven scattered gate-exemption predicates are retired onto one
owner guarded by an exemption-registry ratchet, and the accept gate now
asserts ref-agreement before it judges the acceptance matrix. -
The
issue-matrix.mdandacceptance-matrix.jsonmissing-file errors now name the regenerate command.
Both files are already scaffolded automatically duringspec-kitty tasks(finalize-tasks); the failure messages an operator actually sees when one is missing (atmove-task --to approvedand atspec-kitty accept) previously said nothing about that, so a missing file read as "no tooling exists for this" rather than "re-run finalize-tasks." Both messages now namespec-kitty agent mission finalize-tasks --mission <slug>, and the issue-matrix message also points at its schema/worked-example doc (src/specify_cli/cli/commands/review/ERROR_CODES.md). Thespec-kitty-mission-reviewskill's Gate 4 section gained the same pointer. -
Windows backslash in git tree-path misreported committed specs as uncommitted (#2836).
_git_commit_check_contextbuilt the git tree path withstr(Path(...)), which
renders using the OS-native separator - a backslash (\) on Windows. Git's
HEAD:<path>object syntax andls-filespathspec require forward slashes, so
both subprocess checks failed andis_committed()reported genuinely-committed
spec files as uncommitted, blocking the setup-plan workflow (e.g.
/spec-kitty.planrefusing to proceed). Both return sites now resolve through a
single worktree-aware kernel seam (kernel.paths.repo_tree_path) that renders
forward slashes on every host viaPurePosixPath. Invisible on POSIX
(macOS/Linux/CI) whereos.sepis already/. -
Posix path-separator normalization consolidated into one kernel seam.
Thestr(x).replace("\\", "/")idiom behind #2836 was scattered across ~17
sites incharter,mission_runtime, andspecify_cli(review, upgrade +
migrations, merge, git, skills, status, paths, bulk_edit) - each an independent
chance to reintroduce the Windows backslash defect. All now route through the
behaviour-agnostickernel.paths.to_posix(path: Path | str)seam (kernel being
the zero-dependency root every layer can import downward), leaving the seam
definition as the onlyreplace("\\", "/")insrc/. -
merge's hollow-review warning no longer flags a WP with a genuinely independent reviewer.
force_count >= 2alone can't distinguish "the reviewer used--forceto bypass an unrelated gate false-positive" from "no independent review happened" - both increment the same counter. When the event log positively confirms a different actor logged the approving transition than the one that most recently implemented the WP, the warning no longer fires; absence of that evidence still warns as before (fail-safe default, never suppressed on uncertain data). -
Honest force-provenance on evidence-gated backward edges (#2684, #2736, #2810).
PersistedStatusEvent.forceis now truthful - falsy on the evidence-gated
review-rejection edges (build_transition_planasks the FSM instead of
auto-promotingforce) and truthful on genuine guard-bypasses (leaving a
terminaldone). Fixes the false-force stamp found during #2736 / PR #2810. -
Off-axis emit sites resolve their write target from stored topology, never
Path.cwd()(#2647).
Runtime-state events can no longer be written against the wrong feature
directory when the CLI runs from a foreign working directory. -
Rollback to
plannedreleases the prior claim (#2512).
A rolled-back work package no longer retains a staleshell_pid/agentin the
reduced snapshot, so the next resume/re-claim is not blocked by a dead claim. -
Runtime-state backfill verify is now genuinely fail-closed on value tampering (#2684).
The parity check could silently pass a tampered value when a correcting seed annotation
happened to sort last in a same-timestamp reduce fold (a content-hash-ULID coin-flip); it
now flags any two annotations assigning different values to one slot as corruption,
regardless of fold order. (The backfill CLI wiring itself remains deferred to #2816.)
The runtime-state eviction also hardens the dual-write end-state: bypass frontmatter reads
routed onto the gated snapshot seam, the phase-1 authority predicate split from the legacy
lane-mirror, and the #2093 authority invariant asserted by imported-symbol identity. -
Fresh-project
charter synthesizeno longer crashes after thecharter.yamlinversion (#2800, #2773).
Post-#2773,charter generatestopped writingcharter.md, but the fresh-project
synthesize intercept still gated oncharter.mdbeing present - so on a real fresh
project the intercept never fired andcharter synthesizefell through to the production
adapter, exiting 1 with'str' object has no attribute 'get'. The intercept now gates on
the authoritativecharter.yaml(whatgeneratewrites), restoring the fresh-project seed
path and aligning with the inversion invariant thatcharter.mdis display-only, never a
resolving signal. -
Pre-review regression gate no longer silently skipped by a leaked sync toggle in the parallel suite (#2800, #2794).
The gate reuses the sync layer's process-wideSPEC_KITTY_SYNC_MINIMAL_IMPORT/
SPEC_KITTY_SYNC_DISABLEopt-outs; in a whole-tree-n autorun one could leak into an
xdist worker and skip the gate, reding the gate-observability tests. An autouse fixture now
unsets both toggles before every agent test, making them worker- and order-independent. No
production behaviour change; the deliberate gate↔sync coupling is tracked for a dedicated
opt-out in #2801. -
CI test-suite remediation: stale fixtures from two recent merges brought back to green (#2800).
Thecharter.yamlconsolidation (#2773) and the coord-rollback transactionality mission
(#2786) merged with un-flagged test-fixture debt a CI path-filter hid until #2800 surfaced
it: charter selection/bundle/synthesize/freshness fixtures still seeding the retired
governance.yaml/metadata.yamltriad; arch-gate remainders (golden-count, tmp-literal,
marker drift); a #2709 regression fixture missing the canonicalforceevent field; and 89
mechanicalmypy --stricterrors in the agent test package. Fixtures re-pointed onto
charter.yaml, gates re-based, and tests whose premise encoded a retired flow removed
outright per the standing-order §4 remediation framework (now amended to codify
superseded-design -> remove). The legacy-topology bootstrap CWD gap a strict-xfail guards
is now tracked in #2802. -
Sync batch-400 poison isolation - one invalid event no longer strands its whole batch (#2736; closes #2755).
A whole-batch HTTP 400 with no per-eventdetailsmade the CLI fan the batch-level error onto every
event asrejected(non-terminal), so the culprit re-poisoned every subsequent drain and the innocents
never delivered - the upstream cause of thewp_status_event_without_createprojection anomaly. The
receiver now recursively bisects a poison batch (delivery/receivers.py): split -> re-POST both halves
-> recurse to singletons, isolating the culprit (keptrejected/retryable, never force-parked) and
delivering every innocent, with create-before-status ordering preserved by sequential left-before-right
recursion (and a split-point clamp so a same-wp_idcreate+status pair still terminates). The live
offline-queue path (sync/batch.py_parse_error_responseno-detailsbranch) now dispositions a
batch-level 400 asfailed_transient(noretry_countbump) instead of poisoning innocents. A single
sharedcore/batch_partition.pyprimitive (split_in_half+create_aware_midpoint) de-duplicates the
batch-splitting math and closes #2755 by retrofitting the 413 byte-shrink onto it, guarded by a
behavioral + AST single-authority test. CLI-side only, no server-contract change; the SaaS
transition-matrix and reducer alignment are tracked cross-repo (spec-kitty-saas#509 / #510). -
Mission squash merge no longer clobbers target-newer acceptance provenance or traces (#2709).
The supported squash merge rangit merge --squash -X theirson every conflicting file,
silently reverting target-newermeta.jsonacceptance/VCS fields (andtraces/*.md
sections) to the older mission-branch copies, and the coord->target projection
blind-overwrote the durable event log andstatus.json. Merges now reconcile per
artifact class: planning artifacts stay mission-authoritative (-X theirsintent from
#1732 preserved), whilemeta.jsonacceptance/VCS keys field-merge target-authoritative
withacceptance_historyunioned andtraces/*.mddo a line-level union - via new
spec-kitty merge-driver-meta/merge-driver-tracesgit drivers whose activation is
scoped to the ephemeral squash merge (they no longer leak into a laterauto_rebase). The
projection unions the event log viamerge_event_payloadsand rematerializesstatus.json
from the reduced log instead of a blind copy. Seeded by migrationm_3_2_6. Ships with a
red-first reproduction and two class-closing architectural lints (no-blind-copy over the
projection path + driver-registry completeness sourced from the canonical artifact-kind
registry). -
Merge rollback and
--resumestay coherent after a failed target advance (#2711).
When target advancement failed afterapproved -> doneevents were committed to the
coordination branch, rollback reverted only working-tree bytes - leaving a committeddone
opposed to a reverted workingapproved- andspec-kitty merge --resumere-emitted a
freshdone(non-idempotent). Rollback now reverts the coordinationdonecommit
(coord-worktreegit revert, sourced from the canonical write-target ref; never a raw
update-ref), and--resumederives progress from the durable committed event log with
MergeState.completed_wpsdemoted to an advisory hint - so committed and working status
stay coherent and resume isevent_id-stable. INV-5 (#1827) merge-phase ordering is
preserved. Ships with a red-first reproduction and a resume non-reemission property guard.
Tracked follow-ups: #2786 (write a durable reconcile marker when the rollback revert
itself fails) and #2794 (aSPEC_KITTY_SYNC_MINIMAL_IMPORTtest-isolation leak that
skips the pre-review gate under the parallel CI suite). -
A failed coordination-
donerevert during rollback no longer silently re-opens the
#2711 split-brain (#2786), and the coordination write-set rolls back transactionally
(#2367 Mechanism B). When the rollback's coord-donegit revertitself failed (#2786),
or when a merge aborted mid-way through_record_merged_wps_done_for_mergebefore any revert
ran (#2367-B), rollback restored only working-tree bytes and left a committeddoneopposed to
a reverted workingapproved- a silently-stranded split-brain. Rollback now marks-not-raises:
a durableMergeState.pending_coord_reconcilemarker records the stranded WP(s) - derived from
the committed coordination ref (the reliable authority; a working-tree diff is empty at the
revert-failure point) over this merge's own pre-targetdonewrite-set, so a legitimately
pre-existing-doneWP is never re-stranded.spec-kitty merge --resumeheals via a strand-gated,
idempotentgit revert(byte-stable on re-run), andspec-kitty doctor coordinationdetects the
strand (re-verifying incoherence from the committed ref, not marker-presence) with a--fix. A
behavioral class-closing guard reds if any of the seven_restore_final_bookkeeping_snapshots
rollback sites (incl. the previously-unenumerated coord-reachable site) strands without marking.
INV-5 (#1827) ordering preserved; happy-path merge byte-identical. Ships red-first repros for both
mechanisms. Deferred with follow-ups: #2797 (unify the twogit reverttransport legs into
one shared helper). (#2795, #2367 Mechanism A - claim-time VCS-lock resync, was deferred here
and has since landed in #2906, above.) -
--jsonoutput is now plain regardless of terminal colour; CLI tests are colour-deterministic (#2632).
Under a colour-forcing harness (e.g.FORCE_COLOR=3) Rich syntax-highlighted--json
output - splicing ANSI escapes into the payload sojson.loadsand| jqchoked - and
split literal substrings in styled human output, red-ing ~81 pre-existing tests. All CLI
output now routes through a single canonicalCliConsoleseam
(specify_cli.cli.console) whose machine-output methods (emit_json/print_json) are
plain by construction, so--jsonis safe under any colour environment. Test determinism
comes from toggling colour on that one shared object (set_plain), never from mutating
os.environ. The whole CLI layer (~77 ad-hocConsole()constructions) was moved onto the
seam - no shim - and an architectural guard forbids a rawConsole()under
src/specify_cli/cli/. Deferred non-CLI consoles (including an activeretrospective/cli.py
--jsoncorruption vector) are tracked in #2634. ADR:2026-07-14-1-canonical-cli-console-seam. -
Implement-loop friction quick-wins II (#2570, #2493, #2555, #2566, #2589, #2533, #2580).
Eight fixes that make lifecycle guards no-op-stable against their own runtime writes and the
pre-review gate return real verdicts, each preserving the guard's true-positive:
the lane allocator ignores theshell_pid/base_*WP frontmatter it just wrote (batch lane
allocation needs no inter-commit); the analysis-report freshness check ignores pipe-table
[D]/[P]status-marker churn frommark-status; the pre-review gate resolvespytestvia
uv run(a real verdict instead of a spurious--forceon a green tree) and serializes
concurrent runs behind a machine-wide lock whose acquire-timeout is decoupled from the run
timeout; theagent_profilesmanifest storesoutput_pathrepo-relative sospec-kitty upgradeis cross-machine deterministic; the issue-matrix approval error names the offending
schema-drift column instead of a misleading "Missing rows"; bulk-edit inference no longer
trips on ordinary refactor verbs while genuine bulk edits still trip;setup-plan's first
scaffold write returnssuccess+scaffold_only(mirroring the specify twin) instead of
reading asblocked;move-taskroutes coord-lane planning-artifact staging through the
commit-router authority path (no lanekitty-specs/commit, STATUS_STATE placement
byte-unchanged) and closes the 4th divergentshell_pidwriter; and a solo PR-bound coord
mission routes its empty-coord status surface cleanly to primary instead of tripping a
split-brain warning. Successor to the loop-friction fast-follow; follow-ups filed for the
topology-derivation revisit (#2602), de-god debt (#2603, #2604), theimplement --json
output contract (#2605), and worktree-awaredoctor tool-surfaces --fix(#2613). -
Implement-loop friction quick-wins (#2581, #2573, #2549, #2577). Four independent
loop-friction fixes:mission create/spec-kitty specifynow derive the create-time
topology from context - a non-primary feature branch without--pr-bounddefaults to
single_branchinstead of minting a coordination branch the operator must manually flatten
(primary branch,--pr-bound, and explicit--topologystill default tocoord);
move-task --to for_reviewgains a--skip-pre-review-gateflag, honors the
SPEC_KITTY_SYNC_DISABLE/SPEC_KITTY_SYNC_MINIMAL_IMPORTdisable env, and surfaces a
progress notice so the scoped-test gate no longer reads as a hang (the full async redesign
stays deferred on #2573);finalize-tasks --jsonreports per-branch commit hashes
(commit_hashes) for the two-branch coord-topology commit set instead of a single
commit_hashthat omitted the coordination-branch commit (#2549 facet B; facet A deferred);
andcharter synthesizeno longer fails closed on an empty/first-run config demanding a
companion tactic for every built-in directive - first-run parity restored (#2526 regression). -
Coord-shadows follow-ups: canonical-source consolidation + gate/liveness robustness
(#2574, #2575, #2576, #2567, #2568). The triplicated subtask-gate-dir resolver is unified
onto oneresolve_subtasks_gate_dirseam with the strong git-ancestry fallback (the weak
status_transitionsite no longer reads a stale coordination husk);core/process_liveness
guards against PID reuse via a persisted creation-time baseline co-written at every claim site
(a recycled PID no longer reads as a live claim); the rollback-uncheck write routes through the
house path-guard and surfaces failures instead of silently leaving checked rows; the acceptance
gate's stray checkbox parser is migrated onto the canonicalcore/subtask_rows; and the review
lock folds onto the canonicalis_process_alive(last strayos.killliveness probe removed). -
Coord-shadows-primary read/gate arm closed (#2502, #2504, #2510, #2512, #2513, #1231).
Thefor_reviewsubtask gate no longer fails open:_infer_subtasks_completereads the
PRIMARY planning surface at every emit-layer caller and blocks on unchecked rows on both the
nativeagent statusand orchestrator-api paths (the redundant #2511 per-door pre-derivation
is retired). A single canonicalcore/subtask_rows._walk_wp_sectionnow defines a WP subtask
row for the guard, the dashboard count, and rollback-uncheck (the writer no longer re-enters a
re-appearing## WPnnheading). A recovered coordination-topology lane re-registers its
sparse-checkout and leaks nostatus.events.jsonl/status.json. Process liveness is promoted
tocore/process_liveness.is_process_alive(with async/daemonre-export alias) so the
stale-WP indicator suppresses false-stale for a live claim. Freshness-hash checkbox-insensitivity
(#1764) is pinned by a regression guard; #1862 is verified-already-fixed. -
Arch-suite warnings remediated at root (#2553, #2554). Report-only diagnostics
(migration patch-skips, duplicate-gate selection, ratchet shrinkage, legacy-contract
backfill) are routed off thewarningschannel viarecord_property(signal
preserved, no blanketfilterwarnings=ignore); a Toolguide model-vs-schema drift is
fixed soterminology-guard.toolguide.yamlvalidates (clearing thebase.pyskip
warning); and three agent profiles now citebdd-scenario-lifecycleas a procedure
rather than a tactic (clearingCharterCatalogMissWarning). -
Coord-authority
safe_commitmisfired on a coordination-husk identity read
(#2508). Identity-meta was read off the coordination husk rather than the primary
checkout, sosafe_commitcould target the wrong tree; the read now anchors on the
primary checkout (_load_coord_branch_meta/_commit_workflow_change). Pinned
red-first by a dedicated regression that fails against the pre-fix code. -
orchestrator-api
transitionno longer lets WPs intofor_reviewwith
unchecked subtasks (#2510). The command trusted a caller-asserted
--subtasks-complete; left unset, emit-time inference readtasks.mdoff
the STATUS feature dir - the coordination worktree husk mid-mission, where
the PRIMARY-partitiontasks.mdnever exists - and failed open,
silently bypassing the subtask guard nativemove-taskenforces (field
evidence: four WPs reacheddonewith 0/16 rows ticked,force=false).
Mirroring the command's own commit-gate precedent, the API now derives the
value server-side from the PRIMARY planning surface when the caller doesn't
assert it; explicit assertions keep working and--forcekeeps its bypass.
Fifth member of the coord-shadows-primary class
(#2331/#2430/#2502/#2508). -
Dashboard artifact viewers no longer render empty for in-flight
coordination missions (#2502). #2431 re-anchored the mission list and
kanban to read planning artifacts primary-first, but the viewer endpoint
family (/api/research, contracts/checklists listings, the spec/plan file
server) still resolved the feature dir through the coord-first resolver -
landing on the status-only coord husk, so clicking spec/plan/research on a
running mission showed nothing while the board showed the work. A new
resolve_feature_planning_dir()composes the coord-first resolver with the
primary-first planning re-anchor; all viewer endpoints use it, and
handle_kanban's split reads are made explicit (legacy-format check =
planning surface; weighted-progress read stays on the coord-first status
surface). Finished missions were unaffected (their coord worktrees are
gone), which is why the gap only showed mid-run - the same "broken precisely
while running" shape as #2430. -
Agent context payloads no longer lose their
Profile-Cited Directives
andTacticsanchors when the token budget trims a section (mission
charter-sole-door-bypass-closure). When the token-budget algorithm
substituted a section for a shorter fetch-command stanza, it deleted the
section's own header line along with its body - so a governance payload
that ran over budget could silently lose the
Profile-Cited Directives (<profile>):andTactics:anchors an agent
reading the payload needs to find the section it wants. The budget
algorithm now keeps each populated kind's header intact and swaps out only
its body. -
spec-kitty agent tasks statusno longer silently degrades the board on
a malformed.kittify/config.yaml(mission
charter-sole-door-bypass-closure). Previously, a broken activation
config (for example,activated_agent_profilesset to something other
than a list) was swallowed by a blanket exception handler, and the
command rendered a status board with no human-in-charge marker and no
indication anything was wrong. It now surfaces the underlying
CharterPackConfigErroras a structured error instead of degrading
silently - you'll notice this as a new, explicit failure where the
command previously ran quietly with missing markers. -
Bump
cryptographyfloor to>=50.0.0, fixing CVE-2026-69247 (#3181).
cryptographyis the dependency behind AES-256-GCM + scrypt KDF encrypted
auth session storage; the previously locked49.0.0was affected. There is
no user-facing behavior change - encrypted session storage keeps working
exactly as before, just on a patched dependency version. Run
spec-kitty auth statusafter upgrading if you want to confirm your stored
session is still recognized; no re-login is required.
♻️ Changed
-
The test suite is leaner and its CI ownership is explicit (mission
assertive-test-suite-sanitation). Removed permanently inert tests,
dominated duplicates, historical shape/count pins, stale scaffolds, and
non-causal structural scanners while preserving reviewed live-path and
planted-fault guards. The shared pytest environment now publishes a validated
staged build with lease heartbeats, eliminating the#3283sibling timeout
cascade. Regression remains a generic blocking marker route with honest
empty-set handling; quarantine remains an empty Tier-3 visibility route whose
owner manifest is checked against repository-wide marker discovery. The
originaldoctor restart-daemonNFR-002 ≤10-second shared-runner wall-clock
claim is explicitly retired; its controlled Linux lane now enforces
distinct-PID restart and healthy-control-plane behavior under bounded
operation timeouts. Hosted-macOS performance evidence is deferred to a
controlled developer/canary harness rather than shared-runner wall clock.
The integrated census records 36,013 nodes / 2,667 Python test files versus
the frozen 37,444 / 2,731 baseline, with every frozen candidate terminally
owned. -
The
doctrinemodule now has a curated, enforced public API surface, so
runtime code imports it through stable doors instead of reaching into its
internals (missiondoctrine-public-api-surface;#3179, closes the
runtime→doctrine half of#2986). A newdoctrine.apimanifest names the
symbols the futurespec-kitty-doctrinewheel will export, thecharter.*
facades re-export them by object identity, and ~34specify_climodules move
off directdoctrine.*imports onto those facades. A lazy-import ratchet plus a
source-side laundering guard keep new reach-through from creeping back in. This
is an internal architecture boundary only — no CLI command, output, or runtime
behavior changes — and it unblocks the#3101doctrine wheel cutover. -
The contributor planning surface (
docs/plans/) gains durable, version-spanning
domain plans and a curated index (#3324). Two domain throughlines — SaaS & hosted sync,
and doctrine & charter — now hold the standing strategy and invariants for their
surface across releases, distinct from the release-scoped3.2.xplans that retire
once distilled. The plans index is reorganized into domain throughlines, portfolio &
milestone planning, and a single working-collections list; loose top-level notes are
filed into their subject subdirectories; and subdirectory landing files are
standardized onindex.md. This is an internal contributor-facing change — the CLI
and templates are unaffected. -
The beginner guides now open with illustrated Mission Kitty splashes, and the
Spec-Driven Development page carries a real diagram instead of ASCII art
(#3276). Getting Started, Understanding Missions, Your First Mission, and
When to use Spec Kitty modes each gain a hero illustration; When to use modes
adds a four-path comparison board; and the SDD workflow — previously a plain
ASCII box diagram — is now a styled SVG flowchart that renders cleanly in light
and dark mode. The guide prose also switches to native DocFX alerts
(> [!NOTE]/> [!TIP]) so callouts stand out on the published site. Where a
stylized splash shows a workflow-like board, a one-line caption points to the
authoritative steps, modes, or mission types on the same page, so the artwork
reads as illustration and the page text stays the source of truth. The written
content and structure are unchanged; the pages are just easier to scan. -
Spec Kitty's documentation now lives under one predictable
docs/root with
canonical sections, so you can find a page by where it logically belongs
(missioncommon-docs-convergence;#2215,#2887,#3273). The scattered
root folders (research/,examples/,glossary/,media/) and the duplicate
reference/vsapi/split are gone. How-tos are now organised by audience —
user how-tos underguides/, contributor how-tos underdevelopment/— each
subdivided by concern behind a single landing page, so you reach what you need
in fewer clicks. Old documentation URLs redirect to their new homes, so existing
links and bookmarks keep working. Every page now declares its intended reader
and Divio type, and the architecture docs present one living design with prior
versions filed as ADRs. -
Dead queue-backed event-drain code removed from
sync/batch.py, closing a
latent consent-bypass path (missionchain-b-consent-bypass-3167;#3167,
#3190,#3187,#3220).batch_syncandsync_all_queued_eventshad no
production caller - the journal dispatcher remains the only event drain Spec
Kitty actually uses - but if either were ever reconnected, they could send a
project's events without checking that project's consent first. Both are
deleted, along with 31 supporting symbols and 91 test nodes that existed only
to serve them, and the now-inertE15egress-allowlist entry is removed. A
tests/syncautouse fixture that had been silently granting "consent" to
almost every test in the folder - which is why this gate went uncovered for
as long as it did - no longer patches the removed seam. No behaviour changes
for existing projects. -
Doctrine-pack resolution and charter activation now run through a single
authority (missionresolution-activation-foundation;#2657,#3210).
One kernel primitive resolves the packs root and honours
SPEC_KITTY_PACKS_ROOTfor every reader - the duplicate runtimehome.py
resolver is collapsed onto it - so resolution can no longer diverge between
call sites. Freshspec-kitty initprovisions the default charter by
deterministic copy instead of re-scanning, so a freshly initialised project
and a migrated one start from an identical charter. The provisioned charter
is the sole authority for what is activated, and an unresolved mission-type
now fails closed at the create/use boundary instead of resolving implicitly.
Existing projects need no action. -
The event log is now the single authority for a work package's review
verdict; the review-cycle.mdno longer carries one (mission
verdict-seam-write-unification-01KZ9Q35). Every verdict reader and merge/
review gate resolves the event-sourcedreview_resultslot in
status.events.jsonl; thereview-cycle-N.mdfrontmatterverdictfield is
retired, and durability moved to the event log (the.mdwrite is now
best-effort). To keep existing projects safe across this reader collapse, a
new auto-discovered upgrade migration (verdict_provenance_backfill) runs on
spec-kitty upgradeand backfills each mission's stranded terminal.md
verdict into the event log (FR-012/SC-008); it is idempotent (keyed on a
deterministic ULID) so re-runningupgradeis safe.spec-kitty accept
carries a non-blocking diagnostic that names any still-stranded WP and points
atupgrade. Runbook:
docs/migrations/verdict-provenance-backfill.md.
Nopyproject.toml/__init__.pyversion bump accompanies this entry: the
mission changed only thestatus/__init__.pyfacade, not the CLI entry
point, so a CHANGELOG entry without a version bump is correct here. -
Built-in mission data now ships under
packs/built-in/missions/, alongside
every other built-in doctrine kind (mission
doctrine-consumer-surface-missions-extraction;#3091). The mission-type
profiles, step prompts, step contracts, and per-type content that previously
lived embedded in thedoctrinePython package atsrc/doctrine/missions/
now resolve from the shippablepacks/built-in/missions/location; the 11
.pylogic modules stay in place as an ordinary package. Every reader -
across kernel, doctrine, charter,specify_cli, and the upgrade migrations -
was repointed in one atomic change, and mission-asset resolution now targets
the pack location explicitly so it can no longer silently fall back to the
(now data-less) package directory in a built/wheel layout. No user action is
required:spec-kitty initand mission resolution behave exactly as before,
and the regenerated DRG graph fragments are byte-identical. -
Mission-asset resolution is now a single doctrine-agnostic kernel primitive
(missiondoctrine-consumer-surface-missions-extraction;#3091). The
kernel's built-in-content lookup no longer hard-codes thedoctrinepackage
name or mission-type vocabulary;doctrine/pack_paths.pyand
MissionTemplateRepository.default_missions_root()converge onto that one
primitive, guarded by a kernel-scoped architectural test that fails on any
reintroducedkernel -> doctrineedge. -
Retrospective policy set in
charter.yamlnow takes precedence over
charter.mdfrontmatter (missiondoctrine-charter-split-unification).
If both files configure retrospective behaviour and they disagree, the
compiledcharter.yamlvalue now wins - matching how every other
charter-governed decision already resolves. Projects that configure
retrospective policy only viacharter.mdfrontmatter (nocharter.yaml
retrospective block) are unaffected and keep resolving from the markdown as
before. -
Internal: doctrine/charter changes now get a dedicated, path-filtered CI
workflow (missioncharter-pack-usage-journey;#3102). A PR that touches
onlysrc/doctrine/**/src/charter/**(or touches neither) now runs the
DRG-freshness, charter-context-resolution, and architectural/adversarial
checks for that layer in an isolated workflow instead of paying for the full
suite - faster, more targeted feedback for doctrine/charter work, no change
for unrelated PRs. Thecharter pack apply->charter generatetwo-step and
the empty-charter dispatch-safety-net behaviour are also now documented in
the charter journey guides (#3107). -
The
charter/context.pygod-module was decomposed 3243 -> 570 lines (mission
charter-delivery-finish-context-degod;#2532). The charter-context
engine - previously a single 3243-line module - is now a thin orchestration
surface delegating to 16 focused sibling modules undersrc/charter/
(catalog diagnosis, token budget, artifact bodies, selection rendering,
activation, bootstrap text, compact governance, JSON builder, profile
resolution, doctrine-service builder, ...), and a long-standing internal import
cycle is dissolved. This is a pure maintainability change: a byte-for-byte
parity fixture over the three public entry points (build_charter_context/
_include/_json) is green before and after, and a completion gate asserts
each seam is really used. No behaviour or output changes. -
Glossary-term casing gate is prose-only; the baseline ratchet is retired (#2830, #2823).
All pre-existing non-canonical glossary-term casing acrossdocs/prose was paid down and the
frozen baseline (tests/architectural/glossary_canonical_terms_baseline.txt) deleted, so
test_glossary_canonical_terms.pynow enforces zero non-canonical occurrences in scanned
prose instead of freezing an escape hatch. The gate skips fenced code blocks and inline-code
spans (captured CLI--helpoutput and emitted-string literals keep their real casing) and
exempts the historicaldocs/adr/anddocs/changelog/trees. Also refreshes theAGENTS.md
Canonical Kind Vocabulary table: charter-activatable tokens now listprocedure(not
template) and addglossary-pack;template,asset, andanti_patternremain
ArtifactKindmembers but are not charter-activatable (excluded via
_NON_AUGMENTATION_ELIGIBLE_KINDS- this is a governance clarification, not a kind
rename). Retires the Contextive generated-file staleness test. -
The docs anti-sprawl structure ratchet (R1) is retired (#2851 follow-up). The per-PR
blockinganti_sprawl_ratchet.py --strictstep is removed fromdocs-freshness.yml, and
the script plus its orphaneddoctrine.directives.common_docsconstant (its only consumer)
and self-test are deleted.DIRECTIVE_042's single-root / 13-section / no-shadow-tree policy
is unchanged - it remains a red-line integrity rule - but is now maintained by periodic bulk
curation rather than a per-PR CI gate, matching the operator's bulk-remediation ops intent and
trimming a blocking CI step. The other docs rulers (R2 related-edge validator, R3 lockfile
freshness, description-length and body-link gates) are unaffected and stay blocking. -
Runtime-state corpus cutover completed; the phase-1 dual-write flag is deleted (#2816, #2848).
Completes the #2684 / #2093 eviction. WP runtime state - lane, claim,agent/assignee,
shell_pid, subtask completion,tracker_refs, review-cycle fields, and resolved
role/profile/model bindings - is now unconditionally reconstructed from the append-only
event log through a single reader (reconstruct_wp_view); the_phase1_dual_write_enabled
flag and the legacy frontmatter-runtime fallback are removed, sotasks/WP##.mdfrontmatter
is no longer a runtime authority. The existing corpus was migrated (per-missionbackfill -> verify -> status_phaseflip). Addsspec-kitty migrate backfill-runtime-state, which
seeds a mission's legacy runtime as events, verifies the reduced snapshot against the old
reader by count + value (fail-closed - never flips a mission that fails verify; per-mission
best-effort), and flipsmeta.jsonstatus_phaseto snapshot-authority only for verified
missions; the same seed->verify->flip path ships as an auto-discovered upgrade migration for
consumer repos. -
Skill projection delivers copies, never absolute symlinks (#2412, ADR 2026-07-19-1).
Projected skill files under.claude/skills/,.agents/skills/, etc. used to be
absolute symlinks into the user-global canonical root - which dangle when the repo
is mounted into a dev-container or synced to a remote box, are unreadable to agent
harnesses sandboxed to the repo root, and all break at once if the global root
moves._project_skill_filenow always delivers a real copy (the pre-existing
Windows fallback path, promoted): hash-equal destinations are left untouched
(idempotent re-runs), legacy symlinks are replaced with copies organically on each
project's next init/upgrade/repair run (no migration needed), andrepair_skills
always repairs to a copy, healing pre-existingdelivery_mode: symlinkmanifest
entries as it goes. Freshness is preserved because every install run re-projects
the full skill set; copies inherit the canonical root's read-only mode. Supersedes
the per-project-symlink half of ADR 2026-04-08-3 (the global canonical root is
unchanged). -
charter generateseeds a startercharter.mdcompanion when absent (#2800).
After the #2773 inversioncharter generateproduced nocharter.mdat all, leaving a
fresh project without the display-only rationale companion and no signal one should exist.
generatenow writes a minimal startercharter.mdonly when the file is absent
(create-if-absent); an existing curated companion is left byte-for-byte untouched,
preserving the #2772 never-clobber invariant.charter.mdremains display-only, never a
resolving input. ADR 2026-07-18-1 amended. -
Internal: the
specify_cliaware-UTC clock contract is now enforced structurally
(#2496). Follow-up to #2494's clock-consolidation sweep. Routes the remaining
byte-identicaldatetime.now(UTC).isoformat()"now"-stamp call sites inspecify_cli
onto the single canonicalnow_utc_iso()helper, the canonical producer of that form.
Behaviour-preserving: theUTC/timezone.utcspellings serialize byte-identically
underrequires-python >=3.11. Adds an AST negative gate over the whole
src/specify_clitree (tests/specify_cli/test_clock_consolidation.py), alongside
the pre-existing owned-file inventory, so a module added later is covered the moment it
lands rather than silently regressing while the suite stays green; the gate ships a
self-mutant non-vacuity test and a stale-exemption check. It targets the fluent
single-expression<x>.now(<aware-UTC>).isoformat()idiom (import aliases resolved);
distinct contracts are deliberately out of scope and not flagged: the second-precision
%Y-%m-%dT%H:%M:%SZstamp family,isoformat(timespec=...), naivenow(), the
datetime-returning family, and non-fluent forms (a variable-split or a space-separated
str()of an aware instant). -
Internal: the coord-authority trio is decomposed into ports + pure cores
(#2464, #2465). The three coord-authority god-modules are restructured
behaviour-preservingly into the shipped Typer-shell + request-dataclass + pure-cores
(ports injected) + executor pattern - followingMissionResolver(#2494) and
tasks.py(#2308):workflow.py->workflow_cores.py+workflow_executor.py,
implement.py->implement_cores.py(aGitPortinjected and two# noqa: C901
suppressions removed), andacceptance/->summary_core.py+gates_core.py. The
trio's leaf resolvers now consume the kind-aware placement seam, preserving all three
(lenient) read contracts (#2465). No user-facing behaviour change; pinned by
seam-only + cores-no-I/O architectural tests and a characterization safety net over
implement / review / accept / next. -
Read-side placement seam closure: the last unpoliced primitive is gone
(#2886, #3014; closes the read-side placement-seam migration opened by
#2922/#2921). The #2922 migration above left one gap on purpose - its own
gate coverage bounds namedprimary_feature_dir_for_missionas unpoliced,
because the wrapper itself, and the ~34 call sites still composing a
mission's primary directory by calling it directly, hadn't been routed
through the seam yet. They are now: every one of those call sites asks
placement_seam(root, slug).read_dir(<kind>)for the artifact kind it
actually wants, instead of asking a topology-blind wrapper to guess a path
from a slug. The terminalKITTY_SPECS_DIRassembler survives as the
seam's own module-private leaf - the one place still allowed to compose a
path, because something has to. One behavioural nuance was accepted rather
than papered over: on a backfilled mission where the old blind composition
returned a directory that does not exist, the seam now recovers the real
bare-<slug>directory instead of repeating the miss (identity-confirmed,
not just path-shaped). A new architecture reference,
docs/architecture/artifact-placement-seam.md,
documents the L0-L4 read-placement layering end to end;branch-target-routing.md
is narrowed to the branch-target sense now that placement has its own home.
Governed by ADR
2026-06-24-1
and ADR
2026-07-23-1;
no new architectural decision was needed. -
Activating a subset of directives, tactics, styleguides, toolguides,
mission-step contracts, or glossary packs now actually narrows what your
project sees - it didn't before (missioncharter-sole-door-bypass-closure).
If yourcharter.yaml/pack config selects only some of a pack's
directives (or tactics, styleguides, toolguides, mission-step contracts,
glossary packs), the runtime, CLI, and rendered context now consistently
show only the selected set for those 6 kinds. Previously this filtering
only worked forparadigm,procedure, andagent_profile- the other 6
kinds fell through unfiltered, so a deactivated pack could still show up.
Mission-type selection was already honoured before this mission; it is
now covered by regression tests confirming that. If you haven't
configured any activation selection, nothing changes - a
bare/unconfigured project's result set is unaffected. One deliberate
exception:.kittify/profiles(a local-override directory outside the
doctrine activation model) is explicitly still unfiltered by this
change - a scope call, not a missed site; that area is slated for
separate future rework.Under the hood, this closes every enumerated direct-construction and
._innerbypass around the charter'sDoctrineServicefactory and
extends activation gating from 3 kinds to all 9 charter-activatable kinds
plus themission-typetoken. It does not close the full ~20-door
list:doctrine.template_catalog.resolve_template_by_id(5 importers),
specify_cli/runtime/resolver.py's tier-1-4 reimplementation,
runtime/home.py'simportlib.resourcesroot lookup, three
root-relative missions-root duplicates, and one escalated
AgentProfileRepositorysite (tool_surface/profiles/projection.py)
remain - each named in the mission spec and tracked as its own follow-on
(#3176 for the last). This mission also promotes
MissionTemplateRepository.default_missions_root()as the single shared
missions-root authority and retargets two duplicate hardcoded path
constructions onto it, but that consolidation does not claim
convergence withdoctrine.pack_paths.built_in_dir- full convergence
remains#3091's to deliver.One more consumer-visible narrowing, for projects that declare
languages. If your project's compiled `charter.yaml