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
| Before | After | Notes |
|---|---|---|
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-check | spec-kitty charter consistency-check | Checks 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 validate | spec-kitty charter fetch / new / validate / org init / org validate | Already shared handlers; handlers move out of doctrine.py. charter org validate also validates presets. |
spec-kitty doctrine mission-type list | spec-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
| Situation | Exit | Output | Writes |
|---|---|---|---|
| Applied | 0 | governed keys written/removed; --json: {"pack","preset","written":{…},"removed":[…],"target_file"} | one atomic write |
| Unknown pack | 1 | names the pack; lists available packs | nothing |
| Unknown preset | 1 | names the pack; lists its presets | nothing |
| Unresolvable id in preset | 1 | names preset file and id | nothing |
Governed key would change, no --force | 1 | per-key diff | nothing |
--preset with positional KIND ARTIFACT_ID | 2 | usage error | nothing |
--cascade with --preset | 2 | usage error | nothing |
--pack, --force or --json without --preset (an explicit --pack built-in counts) | 2 | usage error | nothing |
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>]
| Situation | Exit | Output |
|---|---|---|
| Pack found | 0 | the pack root; --json: {"pack", "path"} |
| Pack and preset found | 0 | the preset file; --json: {"pack", "path", "preset"} |
| Unknown pack | 1 | PACK_NOT_FOUND; lists the available packs |
| Unknown preset | 1 | PRESET_NOT_FOUND; names the pack and lists its presets |
--json shapes
| Command | Shape |
|---|---|
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 --json | the 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
| Code | Raised by | Replaces | Payload |
|---|---|---|---|
ACTIVE_CHARTER_CONFIG_INVALID | ActiveCharterConfigError (mission create, charter commands --json) | CHARTER_PACK_CONFIG_INVALID | unchanged shape; golden tests/core/golden/mission_create_refusals.json updated; added to src/charter/activation/ERROR_CODES.md |
LEGACY_CHARTER_STATE | CLI-root gate (FR-011) | — (new) | first finding, remedy text (see cli.md) |
DEFAULT_PRESET_MISSING | init / charter generate / upgrade provisioning when the built-in default preset is absent | DefaultCharterPackMissingError | preset path |
PRESET_NOT_FOUND, PACK_NOT_FOUND, PRESET_ID_UNRESOLVED, PRESET_WOULD_OVERWRITE | charter activate --preset | — (new) | pack, preset, ids / diff |
PRESET_INVALID | charter 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_FAILED | charter 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_FIELD | pack 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_applicableplacesruns_firstmigrations 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 carryingdoctrine_pack_id), even whenmetadata.yamlalready 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):
| Key | Content |
|---|---|
moved | paths moved from .kittify/doctrine/ to .kittify/charter-packs/ |
rewritten | config keys and path references rewritten (file and key) |
reset | stale lists, stale kind gates and [] reset to absent (file and key) |
kept_for_review | customised or near-miss lists and unconvertible entries kept |
matches_minimal | lists equal to the released minimal preset, kept |
skills_removed | installed copies of removed skills deleted |
skills_kept | edited copies kept |
errors | refusals (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_BUILTINkinds expanded against the built-in inventory at comparison time). - A second
spec-kitty upgradechanges 0 bytes;detect()is False. - A pre-rc35 project reaches the cutover version in one
spec-kitty upgradewith 0 errors.