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.
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 |
| Branch naming | kitty/mission-<slug>-lane-<id> |
kitty/mission-<slug>-<mid8>-lane-<id> |
| Worktree naming | .worktrees/<slug>-lane-<id> |
.worktrees/<slug>-<mid8>-lane-<id> |
| 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 |
| Dashboard scanner key | mission_slug |
mission_id (distinct rows for duplicate prefixes) |
mid8is the first 8 characters of the ULID. It is the short disambiguator used in filesystem and git identifiers.- Pre-083 missions without a
mission_idare called legacy missions. The doctor and backfill commands below mint amission_idfor them.
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.
Step 5 — Understanding the new branch and worktree naming
Once a mission has a mission_id, the next spec-kitty implement cycle will
produce new branches and worktrees that embed mid8.
Legacy form (still resolvable for pre-083 state):
Branch: kitty/mission-auth-system-lane-a
Worktree: .worktrees/auth-system-lane-a/
New form (083+):
Branch: kitty/mission-auth-system-01J6XW9K-lane-a
Worktree: .worktrees/auth-system-01J6XW9K-lane-a/
Where 01J6XW9K is the first 8 characters of
mission_id = 01J6XW9KQT7M0YB3N4R5CQZ2EX.
What this means in practice:
- You may see both legacy and new branches side-by-side during the transition. That is expected.
- The dashboard scanner keys rows by
mission_id, so two missions that share a numeric prefix now appear as distinct rows instead of overwriting each other. - Existing worktrees for a mission do not rename automatically. They
continue to work until the next
implementcycle, at which point the new lane worktree is created in the new form. You can delete the old one withgit worktree removeonce you have moved any in-flight work.
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 branches and worktrees 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 the dashboard scanner, and a missing mission_id will
cause them 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.