skills/deprecation-and-migration/SKILL.md
Manage the lifecycle of removing systems, APIs, features, and dependencies. Use when sunsetting a capability, replacing one implementation with another, consolidating duplicates, killing zombie code, or planning the eventual deprecation of something new at design time. Covers the Churn Rule (owners fund migration cost), the strangler / adapter / feature-flag patterns, the five-question decision matrix, advisory-vs-compulsory deprecation, and how to drive it through an OpenSpec change.
npx skillsauth add jankneumann/agentic-coding-tools deprecation-and-migrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Adapted from addyosmani/agent-skills under its upstream license; localized to this repo's OpenSpec workflow and Python defaults.
Code is a liability, not an asset. Every line has ongoing cost — bugs, dependency upgrades, security patches, mental overhead, onboarding tax. Deprecation is the discipline of removing code that no longer earns its keep; migration is the process of moving users safely from the old surface to the new one.
Most teams are good at adding things. Few are good at removing them. This skill closes that gap and ties the discipline to OpenSpec changes so deprecations ship with the same rigor as new features.
The value of code is the functionality it provides, not the code itself. When the same functionality can be delivered with less code, fewer dependencies, or sharper abstractions, the old code should go.
With enough users of an API, every observable behavior — including bugs, timing quirks, and undocumented side effects — becomes depended upon.
This is why deprecation requires active migration, not just announcement. Consumers can't "just switch" when they depend on behavior the replacement does not replicate.
When building something new, ask: "How would I remove this in three years?" Systems designed with clean interfaces, feature flags, and minimal surface area are easy to deprecate. Systems that leak implementation details everywhere become permanent.
If you own the infrastructure being deprecated, you fund the migration cost.
You either migrate consumers yourself, or you ship a backward-compatible adapter so consumers don't have to act. You do not announce a deadline and walk away. The owner's budget pays for the churn.
Run every deprecation candidate through this matrix before writing a line of code:
| # | Question | If "yes" / high | If "no" / low | |---|----------|-----------------|---------------| | 1 | Value — Does it still provide unique value users would miss? | Maintain it. Stop here. | Continue to Q2. | | 2 | Consumers — How many users / call sites / dependents are there? | Quantify scope before promising a date. | Removal is cheap; just do it. | | 3 | Replacement — Does a production-proven replacement exist? | Continue to Q4. | Build the replacement first. Do not deprecate without an alternative. | | 4 | Migration cost — What is the per-consumer cost to switch? | If automatable, automate; if manual + high, weigh against Q5. | Migrate now; trivial cases lose to inertia if delayed. | | 5 | Maintenance cost of NOT deprecating — Security, ops, opportunity cost, complexity tax. | Justifies forcing migration (advisory → compulsory). | Stay advisory; let consumers move on their own timeline. |
Document the answers in the OpenSpec proposal (see Deprecating in OpenSpec below). A reviewer should be able to check the math without re-doing the analysis.
| Type | When to use | Mechanism | Owner obligation |
|------|-------------|-----------|------------------|
| Advisory | Old system is stable; migration is optional; cost of carrying both is bounded. | Warnings, docs, deprecation banners, type-level @deprecated. Users migrate on their own timeline. | Provide a guide; respond to migration questions. |
| Compulsory | Security risk, blocks platform progress, or maintenance cost is unsustainable. | Hard removal date. Migration tooling. Active consumer outreach. | Migrate the long tail yourself; do not just announce a deadline. |
Default to advisory. Compulsory mode requires a budget for tooling, docs, and hand-holding — the Churn Rule applies in full.
Do not deprecate without a working alternative. The replacement must:
## Deprecation Notice: OldService
**Status:** Deprecated as of 2025-03-01
**Replacement:** NewService (see migration guide below)
**Removal date:** Advisory — no hard deadline yet
**Reason:** OldService requires manual scaling and lacks observability.
NewService handles both automatically.
### Migration Guide
1. Replace `from old_service import client` with `from new_service import client`
2. Update configuration (see examples)
3. Run the verification script: `python -m new_service.migrate_check`
Move consumers one at a time. For each:
Only after every consumer is migrated:
## REMOVED Requirements delta so the spec stops promising the capability.Run old and new side by side. Route traffic incrementally. When the old path serves 0%, delete it.
Phase 1: New = 0%, Old = 100% (replacement deployed but dark)
Phase 2: New = 10%, Old = 90% (canary)
Phase 3: New = 50%, Old = 50% (verify parity at scale)
Phase 4: New = 100%, Old = 0% (idle but reversible)
Phase 5: Remove old (irreversible — only after a soak period)
Expose the old interface; forward to the new implementation. Consumers keep their code; you swap the engine.
TypeScript (preserved from upstream):
// Adapter: old interface, new implementation
class LegacyTaskService implements OldTaskAPI {
constructor(private newService: NewTaskService) {}
// Old method signature, delegates to new implementation
getTask(id: number): OldTask {
const task = this.newService.findById(String(id));
return this.toOldFormat(task);
}
}
Python equivalent — wrapping a deprecated requests-based client behind a new httpx-based interface during the strangler period:
# adapters/task_client_legacy.py
from __future__ import annotations
import warnings
from typing import Any
import httpx # new transport
from new_task_client import NewTaskClient # the replacement
class LegacyTaskClient:
"""Drop-in adapter that exposes the old `requests`-style surface
but routes calls through the new httpx-based client.
Lets existing call sites keep working unchanged while we migrate them
one at a time. Delete this module once `git grep LegacyTaskClient` is empty.
"""
def __init__(self, base_url: str, timeout: float = 10.0) -> None:
warnings.warn(
"LegacyTaskClient is deprecated; import NewTaskClient from new_task_client. "
"See docs/decisions/<capability>.md for migration guide.",
DeprecationWarning,
stacklevel=2,
)
self._inner = NewTaskClient(
transport=httpx.HTTPTransport(retries=2),
base_url=base_url,
timeout=timeout,
)
# Old signature: returned a dict. New client returns a Task model.
def get_task(self, task_id: int) -> dict[str, Any]:
task = self._inner.get(str(task_id))
return {
"id": int(task.id),
"title": task.title,
"done": task.status == "completed",
}
The adapter buys you time without freezing consumers. It is itself a deprecation artifact: schedule its removal in the same proposal that introduces it.
Switch consumers cohort-by-cohort using a runtime flag. Decouples deploying the new code from exposing it.
# routing.py
from feature_flags import flags
from legacy_task_service import LegacyTaskService
from new_task_service import NewTaskService
def get_task_service(user_id: str):
if flags.is_enabled("new-task-service", user_id=user_id):
return NewTaskService()
return LegacyTaskService()
Use feature flags when consumers are end users (not internal callers), or when you need an instant rollback path.
Zombie code is code that nobody owns but everybody depends on. It is not actively maintained, accumulates security debt, and silently constrains everything around it. Look for these five signals:
Response: Either assign an owner and fund maintenance, or deprecate it with a concrete migration plan. Zombies cannot stay in limbo — they get investment or removal.
In this repo, deprecations ship as OpenSpec changes — the same proposal → review → implement → cleanup loop as new features. The spec deltas express intent precisely:
## REMOVED Requirements delta in spec.md — use when the capability is going away entirely. Lists the requirements being deleted along with a one-line reason. After the change archives, the requirement disappears from the active spec.## MODIFIED Requirements delta in spec.md — use for Advisory deprecation. Mark the requirement as deprecated, link to the replacement, and document the migration guide inline. The capability still exists; the spec just signals "do not build new dependencies on this".## ADDED Requirements delta — for the replacement surface, written in the same change so reviewers see the trade together.proposal.md — record the answers to the 5-question matrix and pick advisory vs compulsory. Future archaeologists thank you.Workflow:
/plan-feature "deprecate <capability>" — produces the proposal with the matrix answers and the spec deltas./implement-feature <change-id> — lands the adapter / strangler scaffolding, the DeprecationWarning, and the migration tooling. Consumers may migrate now or later (advisory) or before the deadline (compulsory).MODIFIED deprecation to REMOVED and deletes the code./update-specs keeps openspec/specs/ in sync with reality after the deprecation lands — invoke it from the cleanup phase so the canonical spec no longer promises the deprecated surface./cleanup-feature orchestrates the merge, archives the proposal, and prunes branches. It is the standard exit door for both halves of the deprecation (the "mark deprecated" change and the "remove" change).If you find yourself wanting to remove code without an OpenSpec change, stop — that is exactly the silent-removal pattern that breaks Hyrum's Law consumers. Run the change.
| Rationalization | Why it's wrong | |---|---| | "It still works — why remove it?" | Working code that nobody maintains accumulates security debt and complexity. The cost grows silently and shows up as an incident, not a line item. | | "Someone might need it later." | If it's needed later, rebuild it from a clean spec. Keeping unused code "just in case" costs more over a year than rebuilding from scratch. | | "Migration is too expensive." | Compare migration cost to ongoing maintenance over 2–3 years, not a single sprint. The carrying cost almost always wins. | | "We'll deprecate after the new system ships." | Deprecation planning starts at design time. By the time the new system ships, you have new priorities and the old one is permanent. | | "Users will migrate on their own." | They won't. The Churn Rule says you fund the migration — provide tooling, codemods, or do it yourself. | | "We can maintain both indefinitely." | Two systems doing the same job is double the tests, docs, on-call, and onboarding cost — forever. | | "The OpenSpec change is overkill for a deletion." | Deletions are the riskiest operations in the codebase; the proposal is the audit trail that lets reviewers catch a Hyrum's Law trap. |
CODEOWNERS entry and active import sites.proposal.md or docs/decisions/<capability>.md) before any deprecation warning ships.spec.md carries the correct delta: MODIFIED (advisory) or REMOVED (final removal); /update-specs was run during cleanup.development
Open the artifacts relevant to a review (OpenSpec proposal, branch changes, or explicit paths) in VS Code, in a curated read-order, in the right worktree.
tools
Render and seed coordinator-owned task status block in OpenSpec tasks.md
testing
User-invocable skill that omits the tail block
tools
Missing several required keys