Migration note: This page documents a migration path or historical transition. It is not the current 3.2 happy path.
Migration: Mission ID as Canonical Identity
Status: Shipped with mission 083-mission-id-canonical-identity-migration.
ADR: 2026-04-09-1
Issue: Priivacy-ai/spec-kitty#557
Audience: Operators upgrading existing Spec Kitty projects to the 3.x line
that ships the mission_id identity model.
Why This Matters
Before mission 083, Spec Kitty used the three-digit numeric prefix
(mission_number) that shows up in directory names like
kitty-specs/001-auth-system/ as the canonical identity for every mission.
This caused four distinct failure modes in real projects:
- Collision on import. Two repositories each had a
001-auth-systemdirectory. Merging them or running a cross-repo dashboard scanner produced silently-merged state, because both missions looked identical to the selector. - Silent fallback on ambiguous handles. A user ran
spec-kitty agent tasks status --mission 020in a project that had both020-feature-aand020-feature-b(from a botched rebase). The CLI picked one arbitrarily — and usually the wrong one. - Branch and worktree name collisions. Two missions with the same
human-chosen slug would fight over the same
.worktrees/<slug>-lane-adirectory and the samekitty/mission-<slug>-lane-abranch. - Early numbering pressure.
mission_numberhad to be assigned the moment a mission was created, which meant the number had to be globally unique at creation time, which forced a cross-checkout lock that did not actually exist.
Mission 083 fixes all four by making mission_id (a ULID) the canonical
machine identity, minted at creation and immutable. mission_number becomes
display-only metadata, null until merge time, and assigned as
max(existing_numbers)+1 inside the merge-state lock — the only place where a
global invariant can actually be enforced.
Breaking (3.2.5+): this backfill is now mandatory for coordination. As of 3.2.5, a pre-3.2.x mission with no resolvable
mission_id/mid8hard-fails on coordination operations (status transitions,move-task, review/merge coordination writes) instead of silently degrading — the coordination-workspace seam refuses to compose a malformedkitty/mission-<slug>-ref (#2091). If you seeCOORDINATION_WORKSPACE_MID8_REQUIRED, run the backfill below (spec-kitty migrate backfill-identity) to modernize the mission; audit first withspec-kitty doctor identity. The dual-era legacy-bridge fallback is intentionally removed (#2462).
What Changed
| Field | Before (2.x) | After (083+) |
|---|---|---|
| Canonical machine identity | mission_number (3-digit string) |
mission_id (26-char ULID) |
| Selector routing | mission_number / mission_slug prefix match |
mission_id, mid8, or mission_slug, disambiguated by mission_id |
| Mission branch naming | kitty/mission-<slug> |
kitty/mission-<slug>-<mid8> |
| Lane branch naming† | kitty/mission-<slug>-lane-<id> |
kitty/mission-<slug>-lane-<id> (unchanged — see footnote) |
| Lane worktree naming† | .worktrees/<slug>-lane-<id> |
.worktrees/<slug>-lane-<id> (unchanged — see footnote) |
| Ambiguous selector | Silent first-match fallback | Structured MISSION_AMBIGUOUS_SELECTOR error |
When mission_number is assigned |
At mission creation | At merge time, under the merge-state lock |
mid8is the first 8 characters of the ULID. It is the short disambiguator used in the Mission branch and coordination identifiers.- Pre-083 missions without a
mission_idare called legacy missions. The doctor and backfill commands below mint amission_idfor them. - † Lane branch and worktree naming never derived from
mission_id. Mission 083 originally documented lane names as embeddingmid8the same way the Mission branch does. That was never what lane creation produced — lanes were always named from the slug and lane id alone — and the gap between what six other read sites assumed and what creation actually did caused a merge defect (#5108). ADR2026-09-26-2makes the created (slug-only) name the only name any caller can compose.mid8appears in a lane name only when the recorded Mission slug happens to embed it (see Execution Lanes §Naming).
Step 1 — Upgrade spec-kitty-cli
Install the pre-release that contains the 083 work:
pipx install --force --pip-args="--pre" spec-kitty-cli
spec-kitty --version
Expected: a version at or above the 083 release tag.
Note:
spec-kitty-cliis installed viapipx, notpip. See the project'sCLAUDE.mdfor the rationale.
Step 2 — Run the identity audit
From the root of each project you want to migrate:
spec-kitty doctor identity --json
The command walks kitty-specs/ and classifies every mission:
ok— mission already has amission_id; nothing to do.legacy— mission has nomission_id; backfill required.conflict— two or more legacy missions share amission_slugormission_number; they need human disambiguation before backfill.
Expected output for a project with one legacy mission:
{
"status": "legacy_present",
"total": 12,
"ok": 11,
"legacy": 1,
"conflicts": 0,
"missions": [
{
"dir": "kitty-specs/001-auth-system",
"mission_slug": "auth-system",
"mission_number": 1,
"mission_id": null,
"state": "legacy"
}
]
}
If conflicts > 0, resolve them first: rename one of the colliding
directories, or delete a stale checkout. The backfill refuses to run while
conflicts are present, by design — we do not want the CLI to guess.
Step 3 — Run the backfill
Once the audit is clean of conflicts:
spec-kitty migrate backfill-identity
This command:
- Loads every legacy mission.
- Mints a fresh
mission_id(ULID) per mission. - Writes
mission_idintometa.json. No other field is touched —mission_number,mission_slug,created_at,target_branch,friendly_name,mission_typeare all preserved byte-for-byte. - Commits the change on the current branch with a deterministic message.
Expected output:
Scanning kitty-specs/ ...
001-auth-system legacy -> minted 01J6XW9KQT7M0YB3N4R5CQZ2EX
003-dashboard legacy -> minted 01J6XW9VMJ5Z3QRXPFW5K2H1MA
Backfilled 2 missions. meta.json updated. Git commit: abc1234
Backfill is additive-only. Existing data is never overwritten. The
backfill is safe to re-run: missions that already have a mission_id are
skipped.
Step 4 — Re-run the audit
Confirm the project is clean:
spec-kitty doctor identity --json
Expected output:
{
"status": "ok",
"total": 12,
"ok": 12,
"legacy": 0,
"conflicts": 0
}
If any mission is still legacy, rerun backfill against that mission
directly. If a conflict appears after backfill, open an issue — backfill
should never produce one.
What Backfill Does Not Change
Backfill mints mission_id and writes it into meta.json; it does not
touch lanes.json or rename anything already created on disk:
- Lane branches and worktrees keep their created names. Backfilling a
legacy mission's identity does not rename its lane branches or lane
worktrees, because lane naming never took the identity as an input in the
first place (see the footnote in What Changed above and ADR
2026-09-26-2). A lane created before backfill and a lane created after backfill compose the identical name, for the identical slug and lane id. lanes.json's recordedmission_branchis preserved across re-finalize (FR-011). Only the firstfinalize-tasksfor a mission defines its recorded Mission branch. Backfilling the identity and then re-runningfinalize-tasksleaves that recorded value byte-identical — it is never recomposed from the (now-different) identity. This is what keeps a backfilled mission's laterimplement/mergesteps targeting the Mission branch that was actually created, instead of a phantom name that was never created.
Step 5 — Understanding the new branch and worktree naming
This step applies only to a Mission's first finalize-tasks — the one
that defines lanes.json's recorded Mission branch for the first time. Once
a mission has a mission_id, that first finalize (via the following
spec-kitty implement cycle) produces a Mission branch that embeds
mid8. A re-finalize on an already-finalized Mission is a different
case: it preserves the already-recorded Mission branch byte-identically (see
What Backfill Does Not Change above) rather than recomputing it. Lane
branches and lane worktrees are keyed on the slug and lane id only in either
case, never on mission_id directly.
Case A — a legacy Mission backfilled after it was already finalized. Its lanes were created from the pre-backfill slug, so its lane names do not change:
Mission branch: kitty/mission-auth-system
Lane branch: kitty/mission-auth-system-lane-a
Lane worktree: .worktrees/auth-system-lane-a/
Backfilling this Mission's identity does not by itself change any of the above — see What Backfill Does Not Change.
Case B — a Mission created fresh on 083+. Its recorded slug already
embeds the mid8 (minted at mission create, before the first finalize
ever runs), so the Mission branch and the lane names both contain it —
not because lane naming looked up the identity, but because it is already
part of the slug it composes from:
Mission branch: kitty/mission-auth-system-01J6XW9K
Lane branch: kitty/mission-auth-system-01J6XW9K-lane-a
Lane worktree: .worktrees/auth-system-01J6XW9K-lane-a/
Where 01J6XW9K is the first 8 characters of
mission_id = 01J6XW9KQT7M0YB3N4R5CQZ2EX. Compare Case A: there, the slug
never embedded a mid8, so the lane names never picked one up either — lane
naming only ever reflects whatever the recorded slug already contains, never
the identity directly (see ADR
2026-09-26-2).
What this means in practice:
- You may see both legacy and new Mission branches side-by-side during the transition. That is expected.
- Existing worktrees for a mission do not rename automatically. Lane worktrees never need to — they were never keyed on the identity.
Step 6 — What to do if a selector is ambiguous
The --mission flag on every command now accepts three forms:
mission_id— full 26-char ULID. Always unique. Always works.mid8— first 8 chars of the ULID. Unique in practice; the resolver falls through to a structured error if two missions somehow sharemid8.mission_slug— human-readable slug. Preferred for interactive use; the resolver disambiguates bymission_idwhen two missions share a slug.
If the resolver cannot disambiguate, you get a MISSION_AMBIGUOUS_SELECTOR
error without fallback:
Error: Handle 'auth-system' matches 2 missions:
- mission_id=01J6XW9KQT7M0YB3N4R5CQZ2EX slug=auth-system number=1
- mission_id=01J7YZ0DPN5A2B3C4D5E6F7G8H slug=auth-system number=14
Pass the full mission_id or mid8 to disambiguate, e.g. --mission 01J6XW9K.
Copy the mid8 from the error and re-run the command. This is deliberate —
mission 083 removed silent fallback (work package WP07) because it was the
root cause of the collision class of bugs.
Rollback Plan
mission_id is a forward-only, additive change. If something breaks
after the migration:
- The stored data is safe. Backfill only adds
mission_idtometa.json; it never removes or modifies existing fields. A project whosemeta.jsonfiles have bothmission_idandmission_numberis in the normal state for 083+. - Pin the CLI back.
pipx install spec-kitty-cli==<pre-083-version>returns you to the 2.x line. The old CLI will ignore the newmission_idfield inmeta.jsonand continue to route bymission_number. The new Mission branches created under 083 will remain on disk but will not be used by the old CLI; you can eithergit worktree removethem or leave them as archived state. - Report the failure. File an issue at
Priivacy-ai/spec-kitty#557
or the tracking issue for the release with the output of
spec-kitty doctor identity --jsonattached.
Do not hand-edit meta.json to remove mission_id. The file is watched
by the event log, and a missing mission_id will
cause it to classify the mission as legacy and prompt for another
backfill — at which point the mission will receive a different ULID, and
any event log entries keyed off the original ULID will become orphaned.
Related Documentation
- Mission Identity Model in
CLAUDE.md— developer-facing contract summary. - Event Envelope Reference — how
mission_idflows into the machine contract. - Orchestrator API Reference —
--missionselector semantics. - Execution Lanes — lane branch and worktree naming.
- Feature Detection architecture note — historical context for the pre-083 selector.
- Feature Flag Deprecation — the earlier
--feature→--missionmigration.