ALWAYS invoke this skill when migrating specs to spx, reviewing migrations, or debugging migration issues. NEVER migrate specs without this skill.
From spx-legacynpx claudepluginhub outcomeeng/claude --plugin spx-legacyThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Details PluginEval's skill quality evaluation: 3 layers (static, LLM judge), 10 dimensions, rubrics, formulas, anti-patterns, badges. Use to interpret scores, improve triggering, calibrate thresholds.
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>
<constraints>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 |