Contracts

activation-preset.schema.yaml

Activation preset file: <pack>/presets/<name>.yaml (FR-019, ADR 2026-10-06-1 §2-3)

$schema: "https://json-schema.org/draft/2020-12/schema" $id: "https://spec-kitty.ai/schemas/activation-preset.schema.yaml" title: Activation preset type: object additionalProperties: false required: [name, description] properties: name: type: string pattern: "^[a-z][a-z0-9](-[a-z0-9]+)$" maxLength: 64 description: Must equal the file stem. description: type: string minLength: 1 activated_kinds: type: array uniqueItems: true items: type: string # any ArtifactKind plural; the validator checks membership against ArtifactKind (the kind gate covers every kind, not only governed ones) mission_type_activations: type: array uniqueItems: true items: { type: string, minLength: 1 } patternProperties: "^activated_(directives|tactics|styleguides|toolguides|paradigms|procedures|agent_profiles|mission_step_contracts|anti_patterns)$": type: array uniqueItems: true items: { type: string, minLength: 1 }

Semantic rules enforced by charter pack validate / charter org validate (not expressible here):

- the kind list is derived from ArtifactKind (presets govern every charter-activatable kind except skill and

glossary_pack, so anti_pattern is included (#5409); template and asset are not activatable);

this enum is illustrative and the validator reads the authority, never this list;

- every id resolves somewhere in the offering (built-in, org chain, project layer); the error names file and id;

- when activated_kinds is present it includes every kind the preset lists ids for;

- name equals the file stem.

cli.md

CLI contract: charter-pack-cutover-01M491G6

Every "Before" spelling is removed (C-001, OD-3): it exits 2 through Typer's unknown-command path. No hidden alias, no hint.

Command map

BeforeAfterNotes
spec-kitty charter pack apply <name> [--force] [--compile] [--json]`spec-kitty charter activate [--pack <pack>] --preset <preset> [--force] [--compile\--no-compile] [--resynthesize] [--json]`
spec-kitty charter pack list (presets only)spec-kitty charter pack list [--json]One row per pack (built-in, each org pack, project) with its presets.
spec-kitty charter pack path <preset>spec-kitty charter pack path <pack> [--preset <preset>]Prints the pack root, or the preset file with --preset.
spec-kitty charter pack consistency-checkspec-kitty charter consistency-checkChecks the active charter; flags unchanged.
spec-kitty doctrine pack validate <dir>spec-kitty charter pack validate <dir>Also validates presets/ and rejects retired descriptor/activation fields.
spec-kitty doctrine pack assemble …spec-kitty charter pack assemble …Same flags.
spec-kitty doctrine regenerate-graph [--check]spec-kitty charter pack regenerate-graph [--check]Manifest generated_by follows.
spec-kitty doctrine asset list / asset path <id>spec-kitty charter pack asset list / asset path <id>
spec-kitty doctrine fetch / new / validate / org init / org validatespec-kitty charter fetch / new / validate / org init / org validateAlready shared handlers; handlers move out of doctrine.py. charter org validate also validates presets.
spec-kitty doctrine mission-type listspec-kitty charter mission-type list --include-inactive
spec-kitty doctrine (group)—Removed.
spec-kitty doctor doctrine [--json]spec-kitty doctor charter-packs [--json]JSON keys unchanged.
spec-kitty tracker … --doctrine-mode <m>--ownership-mode <m>JSON output key doctrine_mode removed (ownership_mode only).
spec-kitty doctor tool-surfaces --kind doctrine-skill [--json]spec-kitty doctor tool-surfaces --kind charter-skill [--json]ToolSurfaceKind value doctrine_skill → charter_skill; surface-id segment .doctrine_skill. → .charter_skill..

charter activate --preset outcomes

SituationExitOutputWrites
Applied0governed keys written/removed; --json: {"pack","preset","written":{…},"removed":[…],"target_file"}one atomic write
Unknown pack1names the pack; lists available packsnothing
Unknown preset1names the pack; lists its presetsnothing
Unresolvable id in preset1names preset file and idnothing
Governed key would change, no --force1per-key diffnothing
--preset with positional KIND ARTIFACT_ID2usage errornothing
--cascade with --preset2usage errornothing
--pack, --force or --json without --preset (an explicit --pack built-in counts)2usage errornothing

Usage errors go through Click's usage path before any I/O. The usage text names the conflicting options (a test can tell it from an unknown-option error).

charter pack path <pack> [--preset <preset>]

SituationExitOutput
Pack found0the pack root; --json: {"pack", "path"}
Pack and preset found0the preset file; --json: {"pack", "path", "preset"}
Unknown pack1PACK_NOT_FOUND; lists the available packs
Unknown preset1PRESET_NOT_FOUND; names the pack and lists its presets

--json shapes

CommandShape
charter activate --preset … --json{"pack": str, "preset": str, "written": {<key>: [ids]}, "removed": [<key>], "target_file": str}
charter pack list --json`{"packs": [{"name": str, "tier": "built-in" \
charter pack path <pack> [--preset <preset>] --json{"pack": str, "path": str} plus "preset": str when --preset is given
doctor tool-surfaces --kind charter-skill --jsonthe shape --kind doctrine-skill had, with kind value charter_skill and surface ids carrying .charter_skill.

A failure under --json emits json_error(code, message) (specify_cli.cli.json_contract) and exits 1.

Error text format

Text mode prints every coded error as one line Error (<CODE>): <message>, followed by any detail lines (available packs or presets, per-key diff, unresolved ids). Codes are listed in contracts/errors.md.

Unmigrated project (FR-011)

Usage errors come first: a command line that Click would reject as a usage error (for example an unknown option such as the removed --doctrine-mode) exits 2 with Click's usage message even in a legacy project, because the gate checks parse-ability before refusing (amended 2026-10-08, WP14 review). Unknown commands are refused by the gate.

Any command except upgrade, init, --version, --help, the git merge drivers (merge-driver-*) and the hook entry points (live-work and session-start hooks), run in a project (or current checkout) whose state matches the legacy predicate, exits 1 with code LEGACY_CHARTER_STATE and:

This project uses the retired doctrine layout (<first finding>).
Run `spec-kitty upgrade` to migrate it. In a lane worktree, upgrade the
repository root and merge the target into this lane (do not rebase).
See docs/migrations/charter-pack-cutover.md.

errors.md

Error and diagnostic contract: charter-pack-cutover-01M491G6

CodeRaised byReplacesPayload
ACTIVE_CHARTER_CONFIG_INVALIDActiveCharterConfigError (mission create, charter commands --json)CHARTER_PACK_CONFIG_INVALIDunchanged shape; golden tests/core/golden/mission_create_refusals.json updated; added to src/charter/activation/ERROR_CODES.md
LEGACY_CHARTER_STATECLI-root gate (FR-011)— (new)first finding, remedy text (see cli.md)
DEFAULT_PRESET_MISSINGinit / charter generate / upgrade provisioning when the built-in default preset is absentDefaultCharterPackMissingErrorpreset path
PRESET_NOT_FOUND, PACK_NOT_FOUND, PRESET_ID_UNRESOLVED, PRESET_WOULD_OVERWRITEcharter activate --preset— (new)pack, preset, ids / diff
PRESET_INVALIDcharter activate --preset, charter pack list, charter pack path --preset (a preset file that fails the strict preset loader)— (new; owner-approved 2026-10-07)pack, preset file, offending field. A DRG load failure while checking ids is PRESET_ID_UNRESOLVED; an unreadable/invalid activation target is ACTIVE_CHARTER_CONFIG_INVALID
RESYNTHESIS_FAILEDcharter activate --preset --json --resynthesize when the preset was written but the follow-up resynthesis failed— (new; owner-approved 2026-10-07)message states the preset was applied; error payload carries the applied-preset data
RETIRED_PACK_FIELDpack validation and loading— (new; was a generic "extra field" error)file, field (accompanies_doctrine_pack, doctrine_pack_id), replacement

All codes listed in the changelog Before/After (FR-017).

upgrade-migration.md

Upgrade migration contract: charter-pack-cutover-01M491G6 (FR-012)

Identity and ordering

  • One migration module m_4_0_0rc6_charter_pack_cutover.py, migration_id = "charter_pack_cutover", target_version = "4.0.0rc6" (the current version; no version bump), runs_first = True.
  • MigrationRegistry.get_applicable places runs_first migrations before every other selected migration; get_all() keeps version order.
  • Re-selection: the runner selects the cutover migration whenever its structural predicate is true (legacy project root .kittify/doctrine/ present, legacy config keys present, or an activation entry carrying doctrine_pack_id), even when metadata.yaml already records it as applied. A pulled teammate checkout or a merge that brings legacy state back is therefore migrated again, never wedged behind the FR-011 gate. The structural predicate excludes the [], stale-list, kind-gate and installed-skill checks.
  • runs_on_worktrees = False (worktrees are skipped by the runner, #5457).

detect(project_path) -> bool

True when any item of the FR-012 inventory is present: legacy keys in config.yaml / charter.yaml / governance.yaml / answers.yaml / tracker block; .kittify/doctrine/ exists; a path reference carries the old prefix; an activation entry carries doctrine_pack_id; while the migration is not yet recorded as applied: an activated_* key matches a snapshot or is [], or activated_kinds matches the snapshot 8-kind list or [directives, tactics] (after the first application these reset predicates are never consulted again); a removed skill is installed. Shares the cheap legacy predicate with the CLI-root gate; the expensive checks run only in detect().

apply(project_path, dry_run) -> MigrationResult

Order inside apply (each step idempotent):

1. Preflight: if .kittify/doctrine/ and .kittify/charter-packs/ share a path with different content → success=False, errors lists the paths, nothing written. 2. Move .kittify/doctrine/ → .kittify/charter-packs/ (plain move; locked file → refuse naming the path). 3. Rewrite path references: synthesis manifest, provenance sidecars, .kittify/skills-manifest.json source_ref, .gitignore rules. 4. Rewrite config keys (legacy single-pack form gets an explicit pack name ≠ default). 5. Rewrite doctrine_pack_id → charter_pack_id in project activation entries. 6. Reset stale lists, stale kind gates and per-artifact [] to absent (resolved activation store and the other file if it also carries keys). Runs only on the first application (recorded); a structural re-selection never resets a deliberate post-cutover activated_<kind>: []. 7. Remove installed removed skills (manifested, or unmanifested and hash-equal to a released copy); keep edited copies. 8. Record results.

MigrationResult fields used: changes_made (one line per item: moved / rewritten / reset), warnings (kept-for-review lists, minimal-equal lists, edited skill copies, each reset [] with the file and key to restore), manual_review_required = bool(warnings), preserved_paths (edited skill copies). spec-kitty upgrade prints these as the upgrade summary. dry_run=True reports the same lines and writes nothing.

spec-kitty upgrade --json: migration_reports.charter_pack_cutover

changes_made[0] carries the report as JSON; the CLI decodes it into migration_reports["charter_pack_cutover"]. Keys, each an ordered list of strings (one line per item):

KeyContent
movedpaths moved from .kittify/doctrine/ to .kittify/charter-packs/
rewrittenconfig keys and path references rewritten (file and key)
resetstale lists, stale kind gates and [] reset to absent (file and key)
kept_for_reviewcustomised or near-miss lists and unconvertible entries kept
matches_minimallists equal to the released minimal preset, kept
skills_removedinstalled copies of removed skills deleted
skills_keptedited copies kept
errorsrefusals (collision paths, locked files)

Neutralised migrations

m_3_2_0rc35_default_charter_pack, m_3_2_x_normalize_activation_absence, m_2_1_2_fix_glossary_context_skill: detect() returns False (recorded as skipped); bodies removed; ids unchanged.

Guarantees (tested on every NFR-001 fixture)

  • after ⊇ golden before, with the per-fixture expected relation of spec NFR-001 (ALL_BUILTIN kinds expanded against the built-in inventory at comparison time).
  • A second spec-kitty upgrade changes 0 bytes; detect() is False.
  • A pre-rc35 project reaches the cutover version in one spec-kitty upgrade with 0 errors.