06 — Unified Charter Bundle
Status: v2.0.0 (introduced by mission unified-charter-bundle-chokepoint-01KP5Q2G, WP01,
as v1.0.0; inverted to the single-file model by mission
consolidate-charter-bundle-01KXSYB9, IC-02).
Canonical manifest module: src/charter/bundle.py
v2.0.0 contracts: charter-yaml-schema.md, manifest-v2.md
v1.0.0 JSON Schema (archived, superseded): kitty-specs/unified-charter-bundle-chokepoint-01KP5Q2G/contracts/bundle-manifest.schema.yaml
Purpose
Spec Kitty's governance surface is a small bundle of files rooted at
.kittify/charter/. The unified charter bundle contract is a single typed
manifest — CharterBundleManifest — that every reader, migration, and CLI
surface consults to know which files the bundle contains and how they relate.
This file is the narrative source of truth. The Pydantic model in
src/charter/bundle.py is the machine-readable authority. The two MUST stay
in sync; changes to either REQUIRE a SCHEMA_VERSION bump and an
accompanying migration.
v2.0.0 scope: a single authoritative file
The v1.0.0 manifest (mission unified-charter-bundle-chokepoint-01KP5Q2G)
declared charter.md as the sole tracked source and three sync-derived files
(governance.yaml, directives.yaml, metadata.yaml) as its content-hash
input set. Mission consolidate-charter-bundle-01KXSYB9 inverted that
model: the git-tracked, authorable, structured charter is now
.kittify/charter/charter.yaml — a single file nesting governance,
directives, catalog, activation, and overrides. charter.md remains
tracked as a curated companion, but nothing is derived from it any more and
it contributes nothing to the content-hash.
| Role | Path | Notes |
|---|---|---|
| tracked | .kittify/charter/charter.md |
Curated companion — human-authored, never parsed for policy |
| tracked + content-hash input | .kittify/charter/charter.yaml |
The single authoritative structured charter |
derived_files is empty in v2.0.0 — nothing under .kittify/charter/
is generated-and-gitignored any more. content_hash_files is a field
distinct from derived_files (it always was; the historic 4-vs-3
file-count mismatch between BUNDLE_CONTENT_HASH_FILES and derived_files
made that distinction explicit) and holds exactly [charter.yaml].
gitignore_required_entries is also empty — there is nothing left that the
bundle requires a project's .gitignore to exclude.
Full field-level guarantees live in the contracts:
charter-yaml-schema.md
(the charter.yaml shape) and
manifest-v2.md
(the manifest diff from v1.0.0).
Out of manifest scope
The following files live under .kittify/charter/ but are not managed
by the manifest. They are produced by separate pipelines with their own
lifecycle and ownership:
.kittify/charter/context-state.json— runtime state written bysrc/charter/activation/context.pyinsidebuild_charter_context(). This is lazy, per-invocation runtime state; it is not part of any reproducibility contract and is intentionally absent from the manifest..kittify/charter/interview/answers.yaml— optional companion input for charter generation, owned by the interview surface.
The bundle validate CLI surfaces these as informational warnings
when present, never as failures. Operators can leave them in place, remove
them, or extend their .gitignore to match; the manifest does not forbid
them.
Tracked vs. content-hash classification
The split exists so tooling can reason about the two classes separately:
- Tracked files MUST exist on disk, MUST be committed to git, and MUST
appear in
git ls-files.charter.mdandcharter.yamlare both tracked. - Content-hash files are the input set for the bundle's freshness hash —
a strict subset of tracked files (
[charter.yaml]only). A tracked file need not be a content-hash input;charter.mdis the example: tracked (git must have it) but excluded from the hash (it is not read for policy).
The manifest asserts the shape of this split via the Pydantic
_validate hook: no path may be in both the tracked and derived sets; every
derivation_sources key must be a derived path; every value must be a
tracked path. charter.yaml lives only in tracked_files (and
content_hash_files), never in derived_files — that disjointness
invariant is unchanged from v1.0.0 and MUST NOT be relaxed; keeping
"authored ≠ generated" meaningful is the point.
Canonical-root contract
All readers resolve the canonical project root through a single helper —
charter.resolution.resolve_canonical_repo_root() — which correctly maps a
worktree path back to its main-checkout location. See
contracts/canonical-root-resolver.contract.md
for the full contract (unchanged by the v2.0.0 inversion).
Config pointer resolution
The active charter.yaml is located through a single charter: pointer in
.kittify/config.yaml (default .kittify/charter/charter.yaml). The
resolver reads the pointer, then loads that file. The pointer may redirect
to a sibling, shared, or cross-project charter — a swap is a one-line config
change. config.yaml no longer carries the flat activated_* /
mission_type_activations keys itself; those moved to charter.yaml's
root (flat, matching src/charter/activation/packs/default.yaml) as part of the same
inversion — config.yaml keeps only the pointer plus org_packs.
A charter: pointer to a missing or unreadable file fails loud; there is no
fallback to a legacy file.
Content-hash semantics
The bundle's freshness signal is content-hash driven, not sync-driven:
compute_bundle_content_hashhashes exactly the files incontent_hash_files(charter.yaml, declared order) via the unchanged per-file recipe (BOM-strip/CRLF normalization).- Write-side stampers (
write_pipeline.py,resynthesize_pipeline.py) and the freshness reader (charter_runtime/freshness/computer.py) all route through that single recipe. charter.md's own SHA-256 hash — the v1.0.0 staleness mechanism compared against acharter_hashfield — is retired. A hash ofcharter.yamlcannot live insidecharter.yaml(chicken-egg), sometadatacarries no self-referential hash;metadata.bundle_schema_versionis the only fieldversioning.pyreads from it.
charter sync / ensure_charter_bundle_fresh() are retained for
canonical-root resolution and back-compat call sites (the dashboard, the
bundle-migration upgrader, charter context), but no longer perform
extraction — every call is a no-op (synced=False, files_written=[]).
Gitignore policy: MUST-INCLUDE, not exclusive
gitignore_required_entries is a MUST-INCLUDE set, currently empty in
v2.0.0 (nothing left under .kittify/charter/ is required to be ignored).
The .gitignore at the project root:
- MUST contain every entry the manifest lists on its own line (none, today).
- MAY carry additional entries, including entries for the out-of-scope
files enumerated above (
context-state.json, provenance sidecars, synthesis manifests).
bundle validate only fails when a required entry is missing. It
does not enforce exclusivity and does not warn about additional
.kittify/charter/* entries.
Schema versioning policy
CANONICAL_MANIFEST.schema_version carries an independent semver
(2.0.0 as of the consolidate-charter-bundle inversion). The manifest
version is not tied to the spec-kitty package version.
- Major bump (e.g.
3.0.0): breaking change to the manifest shape or required fields — e.g., adding a required key, renaming an existing key, removing the tracked/content-hash split. Requires a new migration module undersrc/specify_cli/upgrade/migrations/. - Minor bump (e.g.
2.1.0): scope expansion or additive optional fields. Requires a migration that extends the manifest and updates every reader site simultaneously. - Patch bump (e.g.
2.0.1): narrative or docstring fixes that do not change the shape or scope. No migration needed.
Future manifest versions ship with their own migration; there is no
fallback for older manifests at runtime (per C-001). A project that
lags a manifest bump must upgrade before it can use the bundle CLI. The
upgrade migration that folds the legacy governance.yaml /
directives.yaml / metadata.yaml / references.yaml quartet (plus
config.yaml's activated_* keys) into charter.yaml and mints the
charter: pointer is the v1.0.0 → v2.0.0 migration path.
Related contracts
charter-yaml-schema.md— thecharter.yamlstructured shape (v2.0.0).manifest-v2.md— theCharterBundleManifestv1.0.0 → v2.0.0 diff.migration-contract.md— the fold migration that retires the legacy quartet.bundle-manifest.schema.yaml— archived v1.0.0 JSON Schema, superseded by the contracts above.bundle-validate-cli.contract.md— CLI contract forspec-kitty charter bundle validate.canonical-root-resolver.contract.md— canonical-root resolution (unchanged by the inversion).chokepoint.contract.md— reader chokepoint contract.