Help us improve
Share bugs, ideas, or general feedback.
From spx-legacy
ALWAYS invoke this skill when migrating specs to spx, reviewing migrations, or debugging migration issues. NEVER migrate specs without this skill.
npx claudepluginhub outcomeeng/claude --plugin spx-legacyHow this skill is triggered — by the user, by Claude, or both
Slash command
/spx-legacy:migrating-spec-to-spxThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
<context>
Migrates flat feature spec folders into a system-based hierarchy under .specify/systems/. Performs system detection and moves numbered feature folders to appropriate system subdirectories.
Migrates DIA projects between versions (v1->v2->v3): runs migration scripts, renames IDs, flattens analysis, regenerates BACKLOG.md. Idempotent. For brownfield projects use /reverse-engineering.
Generates Run IDs, creates isolated worktrees, brainstorms requirements, writes lean spec documents referencing constitutions, validates architecture quality, and reports completion for new features.
Share bugs, ideas, or general feedback.
This is a reference skill providing domain knowledge for migration operations. It does NOT execute migrations - it provides the knowledge for agents that do.
Users should invoke:
spx:spec-to-spx-migrator agent - to execute a migrationspx:spec-to-spx-reviewer agent - to verify a migrationAgents using this skill should:
Provide the domain knowledge needed to migrate capabilities from the legacy specs/work/ structure to the Outcome Engineering framework spx/ structure, including naming conventions, test reverse-graduation, and coverage verification.
This skill is referenced by:
spec-to-spx-migrator agent - executes migrationsspec-to-spx-reviewer agent - verifies migrations<quick_start>
If you're migrating, read these sections in order:
<success_criteria> - Know what success looks like FIRST<verification_gates> - Understand where to STOP and check<failure_modes> - Learn from past mistakes<legacy_system> and <target_system> - Understand legacy vs target structureIf you're reviewing a migration, read <success_criteria> and <verification_gates>, then verify each criterion.
</quick_start>
<success_criteria>
Read this FIRST. A migration is successful when you can answer YES to all of these:
| Criterion | How to Verify |
|---|---|
| SPX tests exist | ls spx/.../NN-slug.story/tests/*.{unit,integration,e2e}.py |
| SPX-MIGRATION.md exists | cat spx/.../NN-slug.story/SPX-MIGRATION.md |
| DONE.md does NOT exist in spx/ | ! test -f spx/.../NN-slug.story/tests/DONE.md |
| If DONE.md in worktree: tests match | Count tests in DONE.md table vs SPX test files |
Note: Stories WITHOUT DONE.md in the worktree still get migrated - they're incomplete stories. Migrate all tests found in specs/.../tests/. The SPX-MIGRATION.md documents what was found and migrated.
| Criterion | How to Verify |
|---|---|
| ALL stories in feature migrated | Every story in worktree has corresponding SPX story |
| ALL stories pass Per-Story Success | Each story has SPX-MIGRATION.md, no DONE.md in spx/ |
| Coverage parity at legacy file level | See concrete example below |
| SPX-MIGRATION.md exists at feature | cat spx/.../NN-slug.feature/SPX-MIGRATION.md |
| No DONE.md anywhere in spx/ feature | ! find spx/.../NN-slug.feature -name DONE.md | grep . |
| Legacy tests removed | git status shows deletions, not modifications |
| Criterion | How to Verify |
|---|---|
| ALL features migrated | Every feature passes Per-Feature Success criteria |
| SPX-MIGRATION.md exists at cap level | cat spx/NN-slug.capability/SPX-MIGRATION.md |
| No DONE.md anywhere in spx/ cap | ! find spx/NN-slug.capability -name DONE.md | grep . |
| Legacy specs removed | ls specs/work/*/capability-NN_slug/ returns nothing |
| Tests pass | just test or pnpm test shows 0 failures |
| Validation passes | just check or pnpm run validate succeeds |
For 43-status-determination.feature, success looked like:
Legacy tests:
tests/unit/status/state.test.ts (5 tests)
tests/integration/status/state.integration.test.ts (19 tests)
Total: 24 tests
Coverage on src/status/state.ts: 86.3%
SPX tests:
spx/.../21-initial-state.story/tests/state.unit.test.ts (5 tests)
spx/.../32-state-transitions.story/tests/state.integration.test.ts (7 tests)
spx/.../43-concurrent-access.story/tests/state.integration.test.ts (4 tests)
spx/.../54-status-edge-cases.story/tests/state.integration.test.ts (8 tests)
Total: 24 tests
Coverage on src/status/state.ts: 86.3%
Verdict: ✓ Test count matches, coverage matches, migration successful
If coverage differs by more than 0.5%, STOP. Find which tests are missing.
</success_criteria>
<verification_gates>
Do NOT proceed past a gate until it passes.
$WORKTREE_PATHspx/ does NOT exist in worktree (confirms correct commit)# Verify gate 1
[ -d "$WORKTREE_PATH" ] && [ ! -d "$WORKTREE_PATH/spx" ] && echo "GATE 1: PASS"
# Verify gate 2 - coverage comparison
pnpm vitest run tests/unit/status/state.test.ts tests/integration/status/state.integration.test.ts --coverage 2>&1 | grep "state.ts"
pnpm vitest run spx/.../43-status-determination.feature --coverage 2>&1 | grep "state.ts"
# Numbers must match
pnpm test passes (0 failures)pnpm run validate passesgit status shows only expected changes (SPX additions, legacy deletions)rm (all deletions via git rm)# Verify gate 3
pnpm test && pnpm run validate && git status
Never hand off in the middle of a feature. Either complete the feature or abandon and reset.
</verification_gates>
<failure_modes>
These failures occurred during actual migrations. Avoid them.
What happened: Agent compared coverage per-story, saw "39.72%" for one story and panicked.
Why it failed: Multiple stories contribute to the same legacy test file. Story-level coverage is meaningless.
How to avoid: ALWAYS compare at the legacy file level. If tests/integration/status/state.integration.test.ts has tests from stories 32, 43, and 54, compare the COMBINED coverage of all three SPX story tests against that ONE legacy file.
What happened: Agent guessed which tests belonged to which story based on file names.
Why it failed: File names don't reliably indicate origin. Only DONE.md documents the actual mapping.
How to avoid: ALWAYS read DONE.md from the worktree. The "Graduated Tests" table is the ONLY source of truth.
What happened: Agent accepted handoff claim that "coverage would drop" without verifying.
Why it failed: The claim was based on wrong-granularity comparison (see Failure 1).
How to avoid: Verify ALL claims from previous handoffs. Re-run coverage comparisons yourself. Trust, but verify.
What happened: Agent removed tests/integration/cli.test.ts after migrating story-32, but stories 43 and 54 also used that file.
Why it failed: Didn't build the sharing map first.
How to avoid: Build the legacy file → stories map BEFORE starting migration. Only remove a legacy file after ALL contributing stories are migrated.
rm Instead of git rmWhat happened: Files disappeared from working directory but Git still tracked them. Caused confusion on next commit.
Why it failed: rm doesn't update Git index.
How to avoid: ALWAYS use git rm. This removes the file AND stages the deletion.
What happened: Agent migrated 2 of 4 stories, then handed off. Next agent didn't know which stories were done.
Why it failed: Partial state is hard to communicate. SPX-MIGRATION.md wasn't updated mid-feature.
How to avoid: Complete entire features before handoff. If you must stop mid-feature, update SPX-MIGRATION.md with explicit "Stories migrated: X, Y. Stories remaining: Z" section.
What happened: Agent moved DONE.md to spx/ but kept the filename as DONE.md.
Why it failed: DONE.md is the legacy name. In spx/, the corrected record is called SPX-MIGRATION.md.
How to avoid:
git mv to move AND rename in one operation:
git mv specs/.../tests/DONE.md spx/.../NN-slug.story/SPX-MIGRATION.md
<spx_migration_md>)! find spx/ -name DONE.md | grep .What happened: Agent only migrated stories that had DONE.md, skipping "incomplete" stories.
Why it failed: DONE.md absence means the story wasn't marked complete - NOT that it should be skipped. The story's tests still exist and need to be migrated.
How to avoid: Migrate ALL stories in the capability, regardless of DONE.md presence. For stories without DONE.md, the SPX-MIGRATION.md documents "No completion record found - migrated all tests from specs/.../tests/".
</failure_modes>
<legacy_system>
specs/
work/
backlog/ # Not started
doing/ # In progress
done/ # Completed
capability-NN_slug/
slug.capability.md
slug.prd.md # Optional PRD
feature-NN_slug/
slug.feature.md
slug.trd.md # Optional TRD
story-NN_slug/
slug.story.md
tests/
DONE.md # MAY exist - documents completion
*.py # Tests MAY still be here
decisions/
adr-NN_slug.md # Separate ADR directory
pdr-NN_slug.md # Separate PDR directory
tests/
unit/ # Tests MAY have graduated here (or not)
integration/
e2e/
Key characteristics:
{type}-{BSP}_{slug}/ (e.g., capability-27_spec-domain/)⚠️ MESSY REALITY: The legacy system evolved. Tests might be:
specs/.../tests/ (never graduated)tests/ but also still in specs/.../tests/ (duplicated - specs/ copies may be stale)tests/ with DONE.md referencing themPhantom graduation: DONE.md may claim tests graduated to tests/unit/... but:
tests/ directory doesn't existspecs/.../tests/# DONE.md says tests are here:
ls tests/unit/test_foo.py # File not found!
# Tests are actually here:
ls specs/.../tests/test_foo.py # Found!
Migration approach:
tests/ → use thosespecs/.../tests/ → use those (ignore DONE.md claims)specs/.../tests/ may be stale duplicates</legacy_system>
<target_system>
spx/
{product}.prd.md
NN-{slug}.adr.md # ADRs interleaved with containers
NN-{slug}.pdr.md # PDRs interleaved with containers
NN-{slug}.capability/
{slug}.capability.md
tests/ # Tests STAY here
*.unit.test.{ts,py}
*.integration.test.{ts,py}
NN-{slug}.adr.md
NN-{slug}.pdr.md
NN-{slug}.feature/
{slug}.feature.md
tests/
NN-{slug}.story/
{slug}.story.md
tests/
Key characteristics:
{BSP}-{slug}.{type}/ (e.g., 27-spec-domain.capability/)</target_system>
<naming_transformation>
| Element | Legacy (specs/) | Outcome Engineering (spx/) |
|---|---|---|
| Directory pattern | {type}-{BSP}_{slug}/ | {BSP}-{slug}.{type}/ |
| Capability example | capability-27_spec-domain/ | 27-spec-domain.capability/ |
| Feature example | feature-32_parsing/ | 32-parsing.feature/ |
| Story example | story-54_validate-args/ | 54-validate-args.story/ |
| Spec file | {slug}.{type}.md | {slug}.{type}.md (unchanged) |
| ADR location | decisions/adr-NN_slug.md | NN-{slug}.adr.md (in tree) |
| PDR location | decisions/pdr-NN_slug.md | NN-{slug}.pdr.md (in tree) |
| Status tracking | Directory location (backlog/doing/) | Test results |
| Test location | tests/{level}/ (graduated) | spx/.../tests/ (co-located) |
Key transformations:
-) separates BSP from slug (not underscore)decisions/ directoryADRs/PDRs may have been migrated earlier with RENUMBERED BSPs. Before migrating:
# Check for existing ADRs/PDRs
ls spx/*.adr.md
ls spx/*.pdr.md
# Example: adr-07 was renumbered to 21
# decisions/adr-07_python-tooling.md → spx/21-python-tooling.adr.md (EXISTS)
# Action: Update references in spec files, do NOT re-migrate
Input: capability-27_spec-domain
├─────────┘ ├┘ └──────┘
type BSP slug
Output: 27-spec-domain.capability
├┘ └──────┘ └────────┘
BSP slug type
</naming_transformation>
<bsp_uniqueness>
BSP numbers are ONLY unique among siblings at the same level.
capability-21/feature-32/story-54 ← One story-54
capability-28/feature-32/story-54 ← DIFFERENT story-54
capability-21/feature-87/story-54 ← DIFFERENT story-54
ALWAYS use FULL PATHS when referencing work items:
| Wrong (Ambiguous) | Correct (Unambiguous) |
|---|---|
| "story-54" | "capability-21/feature-54/story-54" |
| "feature-32" | "capability-27_spec-domain/feature-32_parsing" |
</bsp_uniqueness>
<done_md_format>
DONE.md documents test completion for work items. Read this file to find where tests are.
Test locations in DONE.md may be:
specs/.../tests/ - Tests stayed in place (common now)tests/unit/... etc. - Tests were graduated (older pattern)Example:
# Story Complete: validate-args
## Tests
| Requirement | Test File | Level |
| ---------------------------- | ------------------------------------ | ----------- |
| Parses --config flag | tests/unit/cli/parsing.test.ts | Unit |
| Validates config file exists | tests/integration/cli/config.test.ts | Integration |
| Full CLI workflow | tests/e2e/cli/workflow.test.ts | E2E |
## Coverage
- Lines: 94%
- Branches: 87%
## Completion Date
2025-01-15
Key information to extract:
test_*.unit.py)</done_md_format>
<test_file_naming>
Read the test. Check what it uses. Apply the /testing skill's Quick Reference table.
| Evidence needed for... | Level |
|---|---|
| Business logic | 1 |
| Parsing/validation | 1 |
| File I/O with temp dirs | 1 |
| Database queries | 2 |
| HTTP calls | 2 |
| CLI binary behavior | 2 |
| Full user workflow | 3 |
| Real credentials | 3 |
| Browser behavior | 3 |
Example:
pyproject.toml with tomllib → Level 1 (parsing)subprocess.run(["pre-commit", ...]) → Level 2 (CLI binary)A story can have Level 2 tests. A capability can have Level 1 tests. Determine level by what the test USES, not where it lives.
TypeScript uses .test. SUFFIX:
| Level | Pattern | Example |
|---|---|---|
| Level 1 | *.unit.test.ts | parsing.unit.test.ts |
| Level 2 | *.integration.test.ts | cli.integration.test.ts |
| Level 3 | *.e2e.test.ts | workflow.e2e.test.ts |
| Level 3 (PW) | *.e2e.spec.ts (browser) | login.e2e.spec.ts |
Python uses test_ PREFIX (for pytest discovery):
| Level | Pattern | Example |
|---|---|---|
| Level 1 | test_*.unit.py | test_parsing.unit.py |
| Level 2 | test_*.integration.py | test_cli.integration.py |
| Level 3 | test_*.e2e.py | test_workflow.e2e.py |
Transformation examples:
# TypeScript
tests/unit/parsing.test.ts → spx/.../tests/parsing.unit.test.ts
tests/integration/cli.test.ts → spx/.../tests/cli.integration.test.ts
# Python
specs/.../tests/test_foo.py → spx/.../tests/test_foo.unit.py
tests/integration/test_bar.py → spx/.../tests/test_bar.integration.py
</test_file_naming>
<shared_test_files>
Multiple stories often graduate tests to the SAME legacy file. For example:
tests/integration/status/state.integration.test.tstests/integration/status/state.integration.test.tstests/integration/status/state.integration.test.tsThis means:
Before migrating, scan ALL DONE.md files in the feature to build:
legacy_file -> [story1, story2, ...]
Example output:
tests/unit/status/state.test.ts:
- story-21 (5 tests)
tests/integration/status/state.integration.test.ts:
- story-32 (7 tests)
- story-43 (4 tests)
- story-54 (8 tests)
</shared_test_files>
<migration_workflow>
Before starting any migration:
For each work item being migrated:
tests/, verify:
tests/?specs/.../tests/?tests/ → use those (they're authoritative)specs/.../tests/ → use thosespecs/.../tests/ copies may be stale duplicatesgit mv to spx/.../tests/ (preserves history):
git mv specs/.../tests/test_foo.py spx/.../tests/test_foo.unit.py
git mv to preserve historygit mv specs/.../tests/DONE.md spx/.../NN-slug.story/SPX-MIGRATION.md
<spx_migration_md>)git rm if tests existed in multiple locations:
tests/ if graduated copies exist theretests/ leaves duplicates!The directory location (backlog/doing/done) is irrelevant for migration. Migrate content regardless of which directory it's in.
After migration, verify:
tests/unit/ for this work itemtests/integration/ for this work itemtests/e2e/ for this work itemspecs/.../tests/ (tests moved, DONE.md became SPX-MIGRATION.md)spx/ (! find spx/ -name DONE.md | grep .)</migration_workflow>
<worktree_requirement>
Create the worktree first. One command. Just do it.
git worktree add "../$(basename $(pwd))_pre-spx" \
$(git log --oneline --diff-filter=A --all -- 'spx/' | tail -1 | cut -d' ' -f1)^
Then verify:
[ ! -d "../$(basename $(pwd))_pre-spx/spx" ] && echo "✓ Worktree valid"
Why mandatory:
Do NOT ask "should I create a worktree?" Just create it.
</worktree_requirement>
<coverage_verification>
Baseline from worktree (always available):
# Run tests from worktree to get baseline coverage
cd "$WORKTREE_PATH" && just test "path/to/original/tests --cov --cov-report=json"
After moving tests with git mv:
# Run moved tests from spx/ location
just test "spx/.../tests/ --cov --cov-report=json"
# Compare coverage to baseline - MUST MATCH
If coverage doesn't match - debug using worktree:
# Compare test files
diff -u "$WORKTREE_PATH/path/to/test.py" spx/.../tests/test.py
# Run specific tests from worktree to identify what's missing
cd "$WORKTREE_PATH" && just test "path/to/test.py -v"
The worktree always has the original state - debugging is trivial.
</coverage_verification>
<valid_migration_checklist>
A migration is complete and valid when:
{BSP}-{slug}.{type}/)git mv and renamed correctlygit mv from decisions/ to in-tree location.unit.test.ts, etc.)FIXTURES_ROOT instead of __dirname)git rm (never rm)git rm -r<spx_migration_md>)</valid_migration_checklist>
<spx_migration_md>
DONE.md documents what was claimed. SPX-MIGRATION.md documents what actually happened.
If DONE.md exists at a level, SPX-MIGRATION.md MUST exist at the same level:
| If DONE.md exists here... | Create SPX-MIGRATION.md here... |
|---|---|
specs/.../story-NN/tests/DONE.md | spx/.../NN-slug.story/SPX-MIGRATION.md |
specs/.../feature-NN/tests/DONE.md | spx/.../NN-slug.feature/SPX-MIGRATION.md |
specs/.../capability-NN/tests/DONE.md | spx/.../NN-slug.capability/SPX-MIGRATION.md |
If NO DONE.md exists at a level but tests were migrated there, create SPX-MIGRATION.md anyway.
SPX-MIGRATION.md is NOT just a changelog. It is the corrected verification record.
# SPX-MIGRATION: {spx-name}
## Original Completion Record
**From**: `{path to original DONE.md}`
**Verdict**: {APPROVED/etc from DONE.md}
**Original Date**: {from DONE.md}
**Migration Date**: {today}
## Test Location Corrections
DONE.md claimed tests were at locations that may not exist. This table shows:
- What DONE.md claimed
- Where tests actually were
- Where they are now in spx/
| DONE.md Claimed Location | Actual Location Found | SPX Location | Why This Level |
| -------------------------- | ----------------------------- | --------------------------------------- | ---------------- |
| `tests/unit/foo.py` | `specs/.../tests/test_foo.py` | `spx/.../tests/test_foo.unit.py` | Pure computation |
| `tests/integration/bar.py` | `specs/.../tests/test_bar.py` | `spx/.../tests/test_bar.integration.py` | Uses subprocess |
**"Why This Level"** must reference the testing skill's evidence table:
- Level 1: Business logic, parsing, file I/O with temp dirs
- Level 2: Database, HTTP, CLI binary behavior
- Level 3: Full workflow, real credentials, browser
## Complete Test Inventory
ALL test files at this level, with test counts:
| Test File | Test Count | Requirements Covered |
| ------------------------- | ---------- | -------------------- |
| `test_foo.unit.py` | 11 | FR1, FR2, QR1 |
| `test_bar.integration.py` | 4 | FR3, FR4 |
**Total**: {N} tests
## Decision Record Reference Updates
If ANY spec file had ADR/PDR references updated:
| Spec File | Old Decision Reference | New Decision Reference |
| -------------- | ------------------------------- | -------------------------- |
| `foo.story.md` | `decisions/adr-07_tooling.md` | `21-python-tooling.adr.md` |
| `bar.story.md` | `decisions/pdr-10_lifecycle.md` | `10-lifecycle.pdr.md` |
If no ADR/PDR references were updated, state: "No ADR/PDR references required updates."
## Verification
\`\`\`bash
# Command to verify all tests pass
just test "spx/.../tests/"
# Result
{N} passed in {X}s
\`\`\`
## Files Moved/Removed
Legacy files and their disposition:
| Legacy File | Action | Destination |
| ----------------------------- | -------- | -------------------------------------- |
| `specs/.../tests/test_foo.py` | `git mv` | `spx/.../tests/test_foo.unit.py` |
| `specs/.../tests/DONE.md` | `git mv` | `spx/.../SPX-MIGRATION.md` (this file) |
| `tests/unit/test_bar.py` | `git rm` | (duplicate removed) |
What happens: Agent creates SPX-MIGRATION.md with just "migrated tests" and no details.
Why it fails: Next agent cannot verify:
How to avoid: Use the template above. Every section is mandatory. If a section doesn't apply, explicitly state "N/A" or "None".
DONE.md often contains phantom graduations - claims that tests are in tests/unit/... when they're actually in specs/.../tests/.
SPX-MIGRATION.md MUST:
This creates an audit trail that explains discrepancies.
</spx_migration_md>
rm or rm -r to delete filesgit rm or git rm -rEvery operation MUST be reentrant (can be interrupted and resumed):
| Step | If interrupted | On restart |
|---|---|---|
| Create worktree | No state change | Idempotent - skips if exists |
| Read DONE.md | No state change | Reads again (from worktree) |
| Capture baseline | No state change | Run from worktree again |
| git mv tests | Some moved | Skips existing, moves rest |
| git mv DONE→SPX-MIGRATION | Moved or not | Skips if SPX-MIGRATION.md exists |
| Edit SPX-MIGRATION.md | Partial edit | Re-edit (worktree has original) |
| Verify coverage | No state change | Runs again (worktree has baseline) |
| git rm duplicates | Some removed | Skips already-removed |
| git rm old specs | Some removed | Skips already-removed |