Technical Debt Triage skill for Codex
Rank debt by the interest it charges every month. Codex builds a tech debt register from evidence rather than opinion: git churn times complexity from a bundled hotspot script, bug and incident links, CI times, outdated dependencies and team interviews. Each item gets an interest estimate, effort and risk score and a paydown strategy, and the top few become tickets with a measurable definition of done that managers can approve.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- technical-debt-triage Installs to ~/.agents/skills/technical-debt-triage/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install technical-debt-triage The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project technical-debt-triage Installs to .agents/skills/technical-debt-triage/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/technical-debt-triage.zipunzip -o technical-debt-triage.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) technical-debt-triage PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $technical-debt-triage, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds a technical debt register from evidence - git churn and complexity hotspots, incident and bug links, slow builds, outdated dependencies, TODO markers and developer pain - scores each item by cost of delay against effort and risk, and turns the top items into scoped, fundable work with a definition of done, instead of a wish list of rewrites. Includes a hotspot script. Use when asked to assess tech debt, decide what to refactor first, make the case for a cleanup sprint, prioritise a debt backlog, or explain to managers why delivery is slowing down.
Use it when
- Delivery is slowing down and nobody can say exactly why
- You need to choose which refactor to fund this quarter
- A debt backlog exists but has no priorities or owners
- Managers ask for evidence before approving cleanup work
Use something else when
- You already know what to refactor and need to do it safely: use safe-refactoring
- You are deciding whether to replace a whole system: use strangler-fig-migration
How to check the result
Every register row has a location, evidence and an interest estimate with its assumption, and rerunning the hotspot script after paydown shows targeted files falling in score.
The mistake it prevents
Ranking debt by how ugly code looks, so stable untouched modules get rewritten while the weekly-changed hotspot keeps producing bugs.
Triage is done when every item on the register names where the debt lives, what it costs today in hours, incidents or lost velocity, what fixing it takes, and what “paid” looks like, and the top few items are scheduled work with owners. Debt that slows nobody down is not urgent, however ugly it looks.
Principles
- Debt is measured by interest, not by ugliness: the extra time, defects and risk it causes every month. Code that never changes accrues almost no interest.
- Hotspots (high change frequency combined with high complexity) are where interest is paid. Start there.
- Prefer incremental paydown inside feature work over big-bang rewrites. A rewrite is a last resort that needs its own business case (see strangler-fig-migration).
- Some debt was a good trade. Record the decision; do not shame it.
Workflow
- [ ] 1. Agree scope and horizon with the user (one service? whole repo? next quarter?)
- [ ] 2. Gather evidence (hotspots, incidents, build times, dependencies, team pain)
- [ ] 3. Build the register: one row per debt item, with location and evidence
- [ ] 4. Score each item (interest, effort, risk) and classify
- [ ] 5. Pick a paydown strategy per item
- [ ] 6. Turn the top 3-5 into tickets with a definition of done
- [ ] 7. Agree how progress will be measured
Step 2: gather evidence
Run the hotspot script first; it needs only git and the standard library:
python3 scripts/debt_hotspots.py --since 12.months --top 25
python3 scripts/debt_hotspots.py --path services/billing --ext .py --csv hotspots.csv
It ranks files by commits in the window times an indentation-weighted size (a cheap complexity proxy), and shows author count and TODO/FIXME/HACK markers per file. Paths are relative to the repository root. Read the top ten files before believing the ranking: generated code, fixtures and large config files can rank high and should be excluded.
Other evidence, cheapest first:
| Signal | How |
|---|---|
| Bug density | git log --since=12.months --format=%s -- <path> | rg -ci "fix|bug"; issue tracker labels by component |
| Incidents | Postmortems and incident tickets that name a component |
| Change coupling | Files that always change together across modules signal a missing abstraction (git log --name-only and count pairs) |
| Build and test time | CI duration trends; slowest test files |
| Dependency age | npm outdated, pip list --outdated, go list -m -u all, cargo outdated (network needed) |
| Known-vulnerable deps | npm audit, pip-audit, govulncheck ./... (network needed) |
| Type and lint suppressions | rg -c "@ts-ignore|eslint-disable|# type: ignore|noqa|nolint" |
| Developer pain | Ask the user or team: “Which area do you dread changing, and why?” |
Interviews matter: the hotspot script cannot see a fragile deployment process or a manual release step that costs a day every week.
Step 3: the register
Keep it as a Markdown table or CSV in the repo (docs/tech-debt.md), not only in a
tracker, so it is reviewed with code.
| ID | Item | Location | Evidence | Interest (per month) | Effort | Risk if ignored | Type |
|---|---|---|---|---|---|---|---|
| TD-1 | Order pricing in one 1,900-line function | src/orders/pricing.py | #1 hotspot (score 48k); 7 of 12 pricing bugs last year | ~3 dev-days slower features, 0.6 bugs | M (2 wks) | High: pricing errors hit revenue | Reckless-inadvertent |
| TD-2 | CI takes 38 min | .github/workflows/ci.yml | p50 38 min; 25 PRs/day | ~16 dev-hours waiting | S (3 days) | Medium | Tooling |
| TD-3 | Python 3.9 runtime (EOL) | Dockerfile, pyproject | EOL Oct 2025; blocks library upgrades | Security exposure | M | High | Platform |
Quantify interest even roughly; “about 3 developer-days per month” persuades in a way “messy code” never does. State the assumption behind every number.
Step 4: score and classify
Use a simple, explainable score. Recommended default:
priority = (interest x 3 + risk x 2) / effort each on a 1-5 scale
| Score band | Action |
|---|---|
| Top 3-5 by priority | Schedule as explicit work this quarter |
| High interest, high effort | Break into incremental steps; pay down inside features touching it |
| Low interest, any effort | Leave it; record and revisit in 6 months |
| High risk, low interest (security, EOL runtimes, unsupported deps) | Treat as risk work with a date, not a debt preference |
Tag each item with a cause so patterns show up: deliberate shortcut, outdated design (the domain changed), missing tests, tooling, platform/dependency, knowledge loss.
Step 5: paydown strategies
| Strategy | When |
|---|---|
| Boy-scout rule inside features | Debt is in a hotspot that is changed weekly anyway; add 10-20% to those tickets |
| Dedicated slice of capacity (for example 20%) | Steady backlog of medium items |
| Focused project with a goal | One item dominates (CI time, a framework upgrade) |
| Strangler-fig replacement | Component is beyond incremental repair and business-critical |
| Accept and document | Low interest; or the code is scheduled for retirement |
| Delete | The feature is unused (see dead-code-removal) |
Pair every structural change with tests first (legacy-code-characterization) and small behaviour-preserving steps (safe-refactoring).
Step 6: make it fundable
Each scheduled item becomes a ticket:
### TD-1: Split order pricing into tested rules
**Problem:** pricing.py is the #1 hotspot; 7 of 12 pricing bugs in 2025 originated here.
**Cost today:** pricing features take ~2x estimate; 0.6 production bugs per month.
**Plan:** characterization tests -> extract discount, tax, shipping rules -> remove legacy path.
**Done when:** each rule module < 300 lines with unit tests; pricing bug rate tracked for 2 releases.
**Effort:** ~2 weeks, 1 engineer. **Risk while doing it:** medium; behind flag `pricing-rules-v2`.
Gotchas
- Lines of code and cyclomatic complexity alone rank stable, ugly, harmless code as urgent. Always combine with churn or defect data.
- Rewrites justified only by taste routinely take 3x their estimate and freeze features. Require measured interest before approving one.
- A register nobody revisits becomes a graveyard. Review it quarterly; close items that stopped costing anything.
- Do not start refactoring during triage. Triage produces decisions and tickets; changes follow in their own reviewed PRs.
Output
## Technical debt triage: <scope> (<date>)
**Method:** 12-month hotspots, incident review, CI metrics, interviews with <n> engineers
**Register:** docs/tech-debt.md (14 items)
### Top items to schedule
1. TD-2 CI time (priority 4.2) - 3 days, saves ~16 dev-hours/month
2. TD-1 Pricing hotspot (3.6) - 2 weeks incremental
3. TD-3 Python 3.9 EOL (risk) - deadline-driven
### Accepted for now
- TD-9 Legacy admin UI - low churn, retiring Q3
### Measures of progress
- CI p50 under 12 min; pricing bugs per month; hotspot score of pricing.py
Verification
- Every register row has location, evidence and an interest estimate with its assumption.
- The top items each have a definition of done that can be checked (a metric or a test).
- Rerunning the hotspot script after paydown shows the targeted files dropping in score.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Technical Debt Triage"
short_description: "Rank debt by the interest it charges every month"
default_prompt: "$technical-debt-triage Assess the technical debt in this repo and tell me what to fix first" scripts/debt_hotspots.py
#!/usr/bin/env python3
"""Rank code hotspots for technical-debt triage: files that change often AND are
large or deeply nested are where debt costs the most. Standard library only;
needs git and must run inside a git checkout. Read-only.
score = commits_in_window * complexity_proxy, where complexity_proxy is
non-blank lines weighted by average indentation depth. Also counts
TODO/FIXME/HACK/XXX markers and distinct authors.
Usage:
python3 scripts/debt_hotspots.py [--since 12.months] [--top 25] [--path src/]
[--ext .py,.ts] [--csv out.csv]
"""
from __future__ import annotations
import argparse
import csv
import re
import subprocess
import sys
from collections import Counter, defaultdict
from pathlib import Path
MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b")
DEFAULT_EXT = (".py,.ts,.tsx,.js,.jsx,.mjs,.go,.rs,.java,.kt,.rb,.php,.cs,.swift,"
".scala,.ex,.exs,.c,.cc,.cpp,.h,.vue,.svelte,.dart")
SKIP_PARTS = {"node_modules", "vendor", "dist", "build", "generated", "__generated__",
"migrations", "third_party"}
def git(*args: str) -> str:
r = subprocess.run(["git", *args], capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit(f"git {' '.join(args)} failed: {r.stderr.strip()}")
return r.stdout
def complexity(path: Path) -> tuple[int, float, int]:
"""Return (non-blank lines, indentation-weighted size, marker count)."""
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return 0, 0.0, 0
loc, weighted, markers = 0, 0.0, 0
for line in text.splitlines():
stripped = line.strip()
if not stripped:
continue
loc += 1
expanded = line.expandtabs(4)
depth = (len(expanded) - len(expanded.lstrip(" "))) / 4
weighted += 1 + depth
markers += len(MARKER_RE.findall(line))
return loc, weighted, markers
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--since", default="12.months", help="git --since window (default 12.months)")
ap.add_argument("--top", type=int, default=25)
ap.add_argument("--path", default=".", help="limit to this path inside the repo")
ap.add_argument("--ext", default=DEFAULT_EXT, help="comma-separated extensions to include")
ap.add_argument("--csv", help="also write all rows to this CSV file")
args = ap.parse_args()
root = Path(git("rev-parse", "--show-toplevel").strip())
exts = {e.strip().lower() for e in args.ext.split(",") if e.strip()}
log = git("-C", str(root), "log", f"--since={args.since}", "--no-merges",
"--format=@@%ae", "--name-only", "--", args.path)
churn: Counter[str] = Counter()
authors: defaultdict[str, set[str]] = defaultdict(set)
author = ""
for line in log.splitlines():
if line.startswith("@@"):
author = line[2:]
elif line.strip():
churn[line] += 1
authors[line].add(author)
rows = []
for f, commits in churn.items():
p = root / f
if p.suffix.lower() not in exts or not p.is_file():
continue
if SKIP_PARTS.intersection(Path(f).parts):
continue
loc, weighted, markers = complexity(p)
if loc == 0:
continue
rows.append({"file": f, "commits": commits, "authors": len(authors[f]), "loc": loc,
"avg_depth": round(weighted / loc - 1, 2), "markers": markers,
"score": round(commits * weighted)})
if not rows:
print(f"No matching source files changed since {args.since} under '{args.path}'")
return 1
rows.sort(key=lambda r: r["score"], reverse=True)
print(f"Hotspots since {args.since} (score = commits x indentation-weighted size)\n")
print(f"{'score':>9} {'commits':>7} {'authors':>7} {'loc':>6} {'depth':>5} {'todo':>4} file")
for r in rows[: args.top]:
print(f"{r['score']:>9} {r['commits']:>7} {r['authors']:>7} {r['loc']:>6} "
f"{r['avg_depth']:>5} {r['markers']:>4} {r['file']}")
total = sum(r["score"] for r in rows)
top_share = sum(r["score"] for r in rows[: args.top]) / total if total else 0
print(f"\n{len(rows)} files scored; top {min(args.top, len(rows))} hold {top_share:.0%} of total score.")
if args.csv:
with open(args.csv, "w", newline="", encoding="utf-8") as fh:
w = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print(f"Wrote {args.csv}")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.