Data Model: 3.1.1 Post-555 Release Hardening

Mission: 079-post-555-release-hardening Purpose: Conceptual entities, their fields, validation rules, and state transitions as they exist post-mission. Engineering implementation in plan.md §6 references these entities by name.

This is a conceptual data model. It is not an ORM schema, not a database design, and not a JSON schema spec. It exists to give reviewers a single place to see what the new fields/states look like and how they relate.


Entities

1. Mission

A unit of planned work tracked under kitty-specs/<mission_slug>/.

Fields (after Track 3):

FieldTypeSourceMutabilityNotes
mission_idstring (ULID)core.mission_creation (NEW — Track 3)immutable after creationCanonical machine-facing identity. Format per ADR architecture/adrs/2026-04-09-1-mission-identity-uses-ulid-not-sequential-prefix.md. Generated via python-ulid (already in pyproject.toml).
mission_numberstring (3-digit zero-padded, e.g. "079")core.mission_creationdisplay-onlyNumeric prefix kept for human display. NOT used as machine-facing identity in any new flow.
slugstringcore.mission_creationdisplay-onlyThe unnumbered slug, e.g. "post-555-release-hardening".
mission_slugstringcore.mission_creationdisplay-onlyNumbered slug, e.g. "079-post-555-release-hardening". Continues to be the directory name under kitty-specs/.
friendly_namestringcore.mission_creationmutable (operator)Human-readable mission title.
mission_typestring ("software-dev" \"research" \...)core.mission_creation
target_branchstringcore.mission_creationdisplay-onlyFinal merge target.
created_atISO 8601 stringcore.mission_creationimmutableUTC timestamp of mission creation.
vcsstring ("git")core.mission_creationdisplay-onlyVersion control system.

Persistence: kitty-specs/<mission_slug>/meta.json

Invariants:

  • mission_id MUST be present in meta.json for every mission created after Track 3 lands.
  • mission_id MUST NOT change after creation.
  • mission_id uniqueness is statistically guaranteed by ULID (128-bit, random tail). No explicit collision check is required.
  • For historical missions (created before Track 3), mission_id MAY be absent. The MissionIdentity loader treats this as a legacy-tolerance display-only state.

State transitions: A mission has no internal lane state of its own. Lane state lives on its child Work Packages.


2. Work Package (WP)

A finalize-tasks-derived unit of work that belongs to a mission.

Fields (no schema change in this mission, but execution-mode handling unifies via lane):

FieldTypeSourceMutabilityNotes
wp_idstring (e.g. "WP01")tasks.md authorimmutableMission-scoped work package id.
mission_slugstringtasks.md frontmatterimmutableBackreference to parent mission. May also reference parent by mission_id post-Track 3 in machine-facing flows.
dependencieslist[string]tasks.md frontmatter (explicit)mutable until acceptedExplicit dependency declaration. NEVER overwritten by parser inference (Track 4).
execution_modeenum (code_change \planning_artifact)ownership inference (src/specify_cli/ownership/inference.py)derived
owned_fileslist[glob]ownership inferencederivedThe file globs this WP owns; aggregated into the lane's write_scope.
lane_idstringcompute_lanes() (Track 2 — now also for planning-artifact WPs)derivedAfter Track 2, planning-artifact WPs receive lane-planning; code WPs receive lane-a/lane-b/etc.

Persistence: tasks.md frontmatter + kitty-specs/<mission_slug>/lanes.json (lane assignment).

Invariants:

  • Every WP MUST have a lane assignment after compute_lanes() (Track 2 closes the planning-artifact gap).
  • Explicit dependencies: declarations in frontmatter MUST be preserved through finalize-tasks (Track 4).
  • A WP whose execution_mode == planning_artifact MUST receive lane id "lane-planning" (Track 2).

3. Lane

The canonical grouping that maps WPs to a workspace.

Fields (ExecutionLane dataclass at src/specify_cli/lanes/models.py:72-89):

FieldTypeMutabilityNotes
lane_idstringimmutableE.g. "lane-a", "lane-b", "lane-planning" (NEW canonical from Track 2)
wp_idstuple[string, ...]immutableOrdered WP execution sequence
write_scopetuple[string, ...]immutableUnion of WP owned_files globs
predicted_surfacestuple[string, ...]immutableSurface taxonomy tags
depends_on_lanestuple[string, ...]immutableLane ids with blocking dependencies
parallel_groupintimmutableLanes with same group number run in parallel

Persistence: kitty-specs/<mission_slug>/lanes.json (full LanesManifest serialization)

State transitions:

                    ┌─────────────────────────────────────────────┐
                    │                                             │
              compute_lanes()                                     ▼
            (in finalize-tasks)                       lane-planning
                    │                                  resolves to
                    │                                  main repo checkout
                    ▼                                  (no worktree)
            ┌──────────────┐
            │   Lane(s)    │ ── lane-a, lane-b, ... ─► .worktrees/<mission>-<lane>
            └──────────────┘
                    │
                    ▼
            write_lanes_json()
            (atomic temp-rename)
                    │
                    ▼
            kitty-specs/<mission_slug>/lanes.json

Invariants (post-Track 2):

  • Lane id "lane-planning" MUST resolve to the main repo checkout via paths.get_main_repo_root(), NOT to a .worktrees/... directory.
  • lane_branch_name(mission_slug, "lane-planning") MUST return the planning branch (the mission's target_branch), NOT a kitty/mission-<slug>-lane-planning namespace branch.
  • Every WP returned by compute_lanes() MUST be assigned to exactly one lane. No "filtered" lane-less WPs after Track 2.

4. Mission Identity (MissionIdentity dataclass)

Resolver-facing identity object loaded from a mission's meta.json.

Fields (post-Track 3):

FieldTypeNotes
mission_id`str \None`
mission_slugstrAlways present.
mission_numberstrDisplay-only.
mission_typestrE.g. "software-dev".

Persistence: derived from meta.json via mission_metadata.resolve_mission_identity(feature_dir).

Invariants:

  • For missions created after Track 3 lands, mission_id MUST be a non-empty string and MUST parse as a valid ULID.
  • mission_id is None is only acceptable for missions whose meta.json was written before Track 3. New machine-facing flows MUST treat None as a hard error (per FR-202).
  • mission_id is the canonical identifier in any new event payload, status snapshot, lane manifest, or sync message added in 3.1.1.

5. Credential

Authentication state stored on disk for the spec-kitty user.

Fields (no schema change in this mission):

FieldTypeNotes
tokens.accessstringBearer token.
tokens.refreshstringRefresh token; rotated by the server on refresh.
tokens.access_expires_atISO 8601Access token expiry.
tokens.refresh_expires_atISO 8601Refresh token expiry.
user.usernamestringIdentity.
user.team_slugstring \None
server.urlstringAPI base URL.

Persistence: ~/.spec-kitty/credentials (TOML format), with sibling lock file ~/.spec-kitty/credentials.lock.

State transitions (post-Track 5):

                                     ┌──────────────────────────────┐
                                     │                              │
                              acquire FileLock                       ▼
                                     │                       hold lock for
                                     ▼                       FULL transaction
                            ┌────────────────┐
                            │ refresh_tokens │
                            │  (locked)      │
                            └────────────────┘
                                     │
                                     ▼
                              read on-disk creds
                                     │
                                     ▼
                              POST /token/refresh/
                                     │
                       ┌─────────────┴─────────────┐
                       ▼                           ▼
                  200 success                   401
                       │                           │
                       ▼                           ▼
                  parse new tokens         re-read on-disk creds
                       │                           │
                       ▼                           ▼
                  save (under lock)         creds changed since
                       │                    function entry?
                       ▼                           │
                  release lock           ┌─────────┴─────────┐
                                         ▼                   ▼
                                       YES                  NO
                                  (stale 401)           (real 401)
                                         │                   │
                                         ▼                   ▼
                                  exit cleanly       clear (under lock)
                                  (no clear)               │
                                         │                 ▼
                                         │           release lock
                                         ▼
                                   release lock

Invariants (post-Track 5):

  • refresh_tokens() MUST acquire FileLock at function entry and release in finally.
  • The HTTP POST to /token/refresh/ MUST occur inside the held lock.
  • On 401, the function MUST re-read on-disk credentials and compare to the entry-time refresh token before treating the failure as authoritative.
  • clear_credentials() called from refresh_tokens() MUST occur under the same held lock; never outside.
  • The standalone CredentialStore.clear() (used by direct logout) is unchanged — it still acquires its own per-I/O lock.

6. Init Model

The set of files, side-effects, and printed guidance produced by spec-kitty init under D-1.

Conceptual fields (post-Track 1):

ElementPre-Track-1Post-Track-1
.git/created via git initNOT created
Initial commitcreated with literal message "Initial commit from Specify template"NOT created
.agents/skills/ shared rootseededNOT seeded
Per-agent skill directories (e.g. .codex/prompts/)seededseeded (unchanged — this is the per-agent path that survives)
.kittify/config.yamlcreatedcreated
.kittify/metadata.yamlcreatedcreated
Slash-command files (per agent)createdcreated
README.md (project's own)createdcreated (unchanged content for the project README)
Next-steps textlists /spec-kitty.dashboard, /spec-kitty.charter, ..., /spec-kitty.implement, ... — names top-level CLI as canonicallists slash commands as canonical; clarifies that /spec-kitty.implement is a slash command driven by the agent runtime, not a top-level CLI users invoke directly

Invariants:

  • For an init invocation against a fresh empty directory, the post-init file set MUST be deterministic and enumerable from documentation.
  • For an init invocation inside an existing git repository, the file set MUST be the same minus any files that already existed; no git state is touched.
  • Re-running init in an already-initialized directory MUST be either idempotent or fail-fast with a clear message; it MUST NOT silently merge or overwrite state.

7. Release Cut

A constraint object describing the gates that must hold for a 3.1.1 release tag to be cut.

Conceptual fields (post-Track 7):

GateSource of truthValidator
pyproject.toml version == targetpyproject.toml:3scripts/release/validate_release.py:load_pyproject_version()
.kittify/metadata.yaml version == target.kittify/metadata.yaml:6 (spec_kitty.version)NEW (Track 7) validate_metadata_yaml_version_sync()
pyproject.toml version == .kittify/metadata.yaml versionboth filesNEW (Track 7) — same function
CHANGELOG.md has an entry for the target versionCHANGELOG.mdscripts/release/validate_release.py:changelog_has_entry() (existing; ensure called in branch mode for FR-606)
Version progression is monotonicgit tag historyvalidate_version_progression() (existing)
Tag matches version (tag mode only)git tag at HEADensure_tag_matches_version() (existing)
Dogfood command set runs cleanly against the working repo/private/tmp/311/spec-kitty at the release commitNEW (Track 7) tests/release/test_dogfood_command_set.py
Structured release-prep draft artifact is produciblesrc/specify_cli/release/payload.py:build_release_prep_payload()NEW (Track 7) tests/release/test_release_payload_draft.py

Invariants (post-Track 7):

  • All gates above MUST pass in branch mode for validate_release.py to exit 0.
  • The git tag v3.1.1 step is a human action (per CLAUDE.md). Track 7 does NOT automate it.

Cross-entity references

Mission ──── (1:N) ──── Work Package ──── (N:1) ──── Lane
   │                         │
   │                         │
   ▼                         ▼
mission_id              execution_mode
(Track 3)               (Track 2 unifies via lane)
   │
   ▼
emitted in
sync events
(Track 3)

Credential ──── (N/A — orthogonal) ──── Mission
   │
   ▼
locked by
FileLock
across full
refresh transaction
(Track 5)

Init Model ──── creates (one-shot) ──── new project directory
                                              │
                                              ▼
                                        kitty-specs/
                                        .kittify/
                                        per-agent dirs

Release Cut ──── validates ──── working repo at release commit
                  │
                  ▼
            pyproject.toml + .kittify/metadata.yaml + CHANGELOG.md
            (all gates must pass — Track 7)

Validation rules summary

RuleTrackEnforced where
mission_id present and ULID-shaped at creation3tests/core/test_mission_creation_identity.py
mission_id immutable after creation3invariant, asserted in tests
Concurrent mission creation does not collide on mission_id3tests/core/test_mission_creation_concurrent.py
Every WP receives a lane assignment2tests/lanes/test_compute_planning_artifact.py
lane-planning resolves to main repo checkout2tests/lanes/test_branch_naming_planning.py
Resolver returns coherent ref for planning-artifact WP2tests/context/test_resolver_planning_artifact.py
Explicit dependencies: not overwritten by parser4tests/core/test_dependency_parser.py (extension)
Final WP section bounds at top-level non-WP heading4tests/core/test_dependency_parser.py (extension)
refresh_tokens() holds lock for full transaction5tests/sync/test_auth_concurrent_refresh.py
Concurrent refresh + rotation does not log out5tests/sync/test_auth_concurrent_refresh.py
init does not create .git/ or commit1tests/init/test_init_minimal_integration.py (extension)
init does not seed .agents/skills/1tests/init/test_init_minimal_integration.py (extension)
init next-steps does not name top-level spec-kitty implement1 + 6tests/init/test_init_next_steps.py
init is idempotent or fail-fast on re-run1tests/init/test_init_idempotent.py
init does not touch existing git repo state1tests/init/test_init_in_existing_repo.py
spec-kitty implement --help marks command as internal infrastructure (implementation detail of spec-kitty agent action implement) and names spec-kitty next and spec-kitty agent action implement/review as the canonical user-facing commands6tests/agent/cli/commands/test_implement_help.py
spec-kitty implement still runs (compatibility)6tests/agent/cli/commands/test_implement_runs.py (extension)
pyproject.toml.kittify/metadata.yaml version sync7tests/release/test_validate_metadata_yaml_sync.py
CHANGELOG.md entry for target version exists7tests/release/test_validate_changelog_entry.py
build_release_prep_payload produces valid draft7tests/release/test_release_payload_draft.py
Dogfood command set runs cleanly7tests/release/test_dogfood_command_set.py