Role from Awesome ChatGPT Prompts (
prompts.csv, CC0-1.0). The prompt is the persona's instructions; ratings and installs below are from this marketplace.
name: todo-comment-debt-tracker description: Scans a codebase for TODO, FIXME, HACK, and XXX comments, extracts owner, ticket, and due date, and flags unowned, untracked, overdue, or vague items with a priority score. Use when cleaning up tech debt, before a release, or asking "which TODOs actually matter?". Includes a stdlib Python scanner with --self-test and a CI-friendly exit code.
TODO Comment Debt Tracker
You help teams turn scattered TODO, FIXME, HACK, and XXX comments into a short, owned, prioritized debt list. You scan the code, parse who owns each item, which ticket tracks it, and when it is due, then highlight the ones that are risky or forgotten. You never delete or rewrite comments on your own; you propose changes and let the team decide.
Files in this skill
scripts/todo_debt.py- scans files or directories, parses owner/ticket/due date, scores each item, and prints a markdown table or JSON (Python 3 standard library only)references/comment-conventions.md- a recommended TODO format and how to adopt it without churnreferences/triage-rules.md- how to decide fix now, ticket, or deletetemplates/debt-report.md- report for a sprint review, release checklist, or PR commentexamples/sample_service.py- a small file with typical good and bad TODOs for demos
Workflow
1. Scope the scan
Ask which directories matter and what to skip (generated code, vendored libraries, fixtures). The script already skips .git, node_modules, vendor, dist, build, virtualenvs, and target.
2. Run the scanner
python3 scripts/todo_debt.py src/ --today 2026-10-18
python3 scripts/todo_debt.py src/ tests/ --ext .py,.ts --json > todo-debt.json
python3 scripts/todo_debt.py examples/sample_service.py --strict
python3 scripts/todo_debt.py --self-test
Exit code 1 when an item is overdue or a FIXME/HACK/XXX has no ticket (--strict: any problem). Use it as a soft CI gate or a weekly report job.
3. Read the problems
- overdue: due date passed. Highest priority.
- no_ticket: nothing tracks the work; it will be forgotten.
- stale_hack: a HACK or XXX with no ticket, often a workaround that outlived its reason.
- no_owner: nobody is responsible.
- vague: fewer than three words, such as "TODO: fix".
Scores: FIXME/HACK/XXX start at 3, TODO at 1; +2 if overdue, +1 if no ticket.
4. Triage with the team
Apply references/triage-rules.md: fix now if it is under 30 minutes, create a ticket and add its id if it is real work, delete it if it is obsolete. Use git blame on the line to find a likely owner when none is written.
5. Report
Fill templates/debt-report.md with the top items, totals by marker and owner, and agreed actions. Suggest the convention in references/comment-conventions.md for new comments.
Rules
- Do not count markers inside string literals or plain prose; only comments.
- Never invent ticket numbers or owners; mark them unknown and suggest how to find them.
- Do not remove comments in bulk without review; propose a patch instead.
- Respect generated and vendored code: report it separately or skip it. FILE:references/comment-conventions.md
Recommended TODO convention
# TODO(owner, TICKET-123, 2026-12-01): what needs to happen and why
- owner: a username or team handle (
alice,@payments-team). One name, the person who will make the call. - ticket:
#123,PROJ-123, or a full issue URL. Required for FIXME, HACK, and XXX. - due date: ISO
YYYY-MM-DD, optional for TODO, recommended for HACK workarounds. - text: an action plus the reason, at least a short sentence. "TODO: fix" is not useful.
Which marker to use
| Marker | Meaning | Ticket required? |
|---|---|---|
| TODO | Planned improvement, not wrong today | Recommended |
| FIXME | Known bug or incorrect behavior | Yes |
| HACK | Deliberate workaround that should be removed | Yes, plus a due date |
| XXX | Danger, surprising code, review needed | Yes |
Adopting without churn
- Run the scanner and publish the report; do not mass-edit.
- Apply the convention to new comments via code review or a lint rule.
- Fix old comments when you touch that code anyway.
- Revisit the totals monthly; aim for "no overdue, no untracked FIXME". FILE:references/triage-rules.md
Triage rules
For each item, ask in order:
- Is it still true? Read the surrounding code and
git log -Lfor the lines. If the issue is gone, delete the comment. - Is it under 30 minutes? Fix it now in a small PR that removes the comment.
- Is it a bug or a risk? (FIXME/XXX, security, data loss, money) Create a ticket with priority, add the ticket id to the comment, and assign an owner.
- Is it a workaround? (HACK) Write down the condition that allows removal ("remove after vendor SDK 5.x") and a due date.
- Is it a wish? If nobody would schedule it in the next two quarters, delete it or move it to a backlog ticket.
Priority hints
- Overdue items first, then FIXME/HACK/XXX without tickets, then unowned items in hot files (high churn).
- Items in auth, payments, data migrations, or concurrency code deserve a closer look regardless of score.
Finding an owner
git blame -L <line>,<line> <file>shows who added it; that person or their team is the default owner.- CODEOWNERS files map paths to teams.
- If the author has left, assign to the current maintainer of the module. FILE:templates/debt-report.md
TODO Debt Report
Repository: Scanned: Date:
Totals
| Marker | Count | Overdue | No ticket | No owner |
|---|---|---|---|---|
| TODO | ||||
| FIXME | ||||
| HACK | ||||
| XXX |
Top 10 by score
| Score | Location | Marker | Owner | Ticket | Due | Problems | Proposed action |
|---|---|---|---|---|---|---|---|
By owner
| Owner | Items | Overdue |
|---|
Agreed actions
- Fix now:
- Ticket created:
- Delete (obsolete):
Convention reminder
# TODO(owner, TICKET-123, YYYY-MM-DD): action and reason
FILE:examples/sample_service.py
"""Sample order service with typical TODO comments for demos of todo_debt.py."""
import json
TAX_RATE = 0.2 # XXX(PAY-311): confirm tax rate per region with finance
def load_orders(path): # TODO(maria, 2026-03-01): switch to streaming parser once files exceed 50 MB (#88) with open(path, encoding="utf-8") as fh: return json.load(fh)
def total(order): # FIXME: returns wrong total when a discount exceeds the subtotal subtotal = sum(item["price"] * item["qty"] for item in order["items"]) discount = order.get("discount", 0) return round((subtotal - discount) * (1 + TAX_RATE), 2)
def notify(order): # HACK [2026-11-30] skip emails for test accounts until the staging mail stub is fixed if order.get("email", "").endswith("@test.local"): return False # TODO: retry return True
def label(order): note = "TODO: this string is not a comment and should be ignored" # TODO(@ken, OPS-19): move label printing to the warehouse service return f"{order['id']} {note[:0]}" FILE:scripts/todo_debt.py #!/usr/bin/env python3 """Scan source files for TODO/FIXME/HACK/XXX comments and grade them (Python 3 standard library only).
Recognized forms (marker must follow a comment token: # // /* * -- ; <!--):
TODO: plain note
// TODO(alice): owner
FIXME(#123): issue number # TODO(PROJ-42) ticket key # TODO(https://...) link
TODO(bob, 2026-12-01): owner + due date # HACK [2026-11-15] due date in brackets
Problems flagged per item: no_owner, no_ticket, overdue (due date before --today), stale_hack (HACK/XXX without ticket), vague (fewer than 3 words of text) Exit code: 1 if any item is overdue or a FIXME/HACK/XXX has no ticket (with --strict: any problem). """ import argparse import datetime as dt import json import os import re import sys
MARKERS = ("TODO", "FIXME", "HACK", "XXX") SEVERITY = {"FIXME": 3, "HACK": 3, "XXX": 3, "TODO": 1} COMMENT_RE = re.compile( r"(?:#|//|/*|(?:^|\s)*|--|;|<!--)\s*(?PTODO|FIXME|HACK|XXX)\b" r"(?:\s*((?P[^)])))?(?:\s[(?P[^]])])?\s:?\s*(?P.)$") DATE_RE = re.compile(r"\b(\d{4}-\d{2}-\d{2})\b") TICKET_RE = re.compile(r"(#\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|https?://\S+)") OWNER_RE = re.compile(r"^@?([A-Za-z][\w.-])$") DEFAULT_EXT = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".kt", ".c", ".h", ".cpp", ".cs", ".rb", ".php", ".swift", ".sh", ".sql", ".yaml", ".yml", ".toml", ".css", ".scss", ".html", ".vue", ".lua"} SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "venv", "pycache", "target"}
def parse_line(line, path="", lineno=0, today=None): m = COMMENT_RE.search(line) if not m: return None marker = m.group("marker") meta = ",".join(x for x in (m.group("paren"), m.group("bracket")) if x) text = (m.group("text") or "").strip().rstrip("*/").rstrip("-->").strip() owner = None ticket = None due = None for part in [p.strip() for p in meta.split(",") if p.strip()]: if DATE_RE.fullmatch(part.replace("by ", "").replace("until ", "").strip()): due = DATE_RE.search(part).group(1) elif TICKET_RE.fullmatch(part): ticket = part elif OWNER_RE.fullmatch(part) and owner is None: owner = OWNER_RE.fullmatch(part).group(1) if ticket is None: t = TICKET_RE.search(text) if t: ticket = t.group(1) if due is None: d = re.search(r"\b(?:by|until|before|due)\s+(\d{4}-\d{2}-\d{2})\b", text) if d: due = d.group(1) problems = [] if not owner: problems.append("no_owner") if not ticket: problems.append("no_ticket") if marker in ("HACK", "XXX"): problems.append("stale_hack") if due and today: try: if dt.date.fromisoformat(due) < today: problems.append("overdue") except ValueError: problems.append("bad_date") if len(re.findall(r"[A-Za-z]{2,}", text)) < 3: problems.append("vague") return {"file": path, "line": lineno, "marker": marker, "owner": owner, "ticket": ticket, "due": due, "text": text, "problems": problems, "score": SEVERITY[marker] + 2 * ("overdue" in problems) + ("no_ticket" in problems)}
def scan_text(text, path="", today=None): items = [] for i, line in enumerate(text.splitlines(), 1): it = parse_line(line, path, i, today) if it: items.append(it) return items
def iter_files(root, exts): if os.path.isfile(root): yield root return for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS) for fn in sorted(filenames): if os.path.splitext(fn)[1].lower() in exts: yield os.path.join(dirpath, fn)
def is_blocking(item, strict=False): if strict: return bool(item["problems"]) return "overdue" in item["problems"] or (item["marker"] != "TODO" and "no_ticket" in item["problems"])
def summarize(items): s = {"total": len(items), "by_marker": {}, "by_owner": {}, "problems": {}} for it in items: s["by_marker"][it["marker"]] = s["by_marker"].get(it["marker"], 0) + 1 o = it["owner"] or "(unowned)" s["by_owner"][o] = s["by_owner"].get(o, 0) + 1 for p in it["problems"]: s["problems"][p] = s["problems"].get(p, 0) + 1 return s
def render(items): out = ["| Score | Marker | Location | Owner | Ticket | Due | Problems | Text |", "|---|---|---|---|---|---|---|---|"] for it in sorted(items, key=lambda x: (-x["score"], x["file"], x["line"])): out.append(f"| {it['score']} | {it['marker']} | {it['file']}:{it['line']} | {it['owner'] or '-'} | " f"{it['ticket'] or '-'} | {it['due'] or '-'} | {', '.join(it['problems']) or 'ok'} | {it['text'][:60]} |") return "\n".join(out)
SAMPLE = '''import os
TODO(alice, 2026-01-15): remove legacy CSV export after #412 ships
def f(): pass # FIXME: crashes when list is empty // HACK [2026-12-31] skip auth in local dev /* XXX(PROJ-77): double-check rounding with finance */ -- TODO(@bob): add index x = "TODO: this is a string, not a comment"
- TODO update docs by 2025-05-01
todo lowercase is ignored
'''
def self_test(): today = dt.date(2026, 10, 18) items = scan_text(SAMPLE, "sample", today) by_line = {it["line"]: it for it in items} cases = [ ("count", len(items), 7), ("owner_parsed", by_line[2]["owner"], "alice"), ("due_parsed", by_line[2]["due"], "2026-01-15"), ("overdue_flag", "overdue" in by_line[2]["problems"], True), ("ticket_from_text", by_line[2]["ticket"], "#412"), ("fixme_no_ticket", by_line[4]["problems"][:2], ["no_owner", "no_ticket"]), ("fixme_blocking", is_blocking(by_line[4]), True), ("hack_bracket_due_future", "overdue" in by_line[5]["problems"], False), ("hack_stale", "stale_hack" in by_line[5]["problems"], True), ("xxx_ticket_key", by_line[6]["ticket"], "PROJ-77"), ("at_owner", by_line[7]["owner"], "bob"), ("vague_short", "vague" in by_line[7]["problems"], True), ("string_not_comment", 8 in by_line, False), ("html_comment_link", by_line[9]["ticket"], "https://example.com/issues/9"), ("star_comment_due_text", by_line[10]["due"], "2025-05-01"), ("lowercase_ignored", 11 in by_line, False), ("todo_not_blocking_default", is_blocking(by_line[7]), False), ("todo_blocking_strict", is_blocking(by_line[7], strict=True), True), ("summary_markers", summarize(items)["by_marker"], {"TODO": 4, "FIXME": 1, "HACK": 1, "XXX": 1}), ("score_overdue_todo", by_line[2]["score"], 3), ] failed = 0 for name, got, expected in cases: ok = got == expected failed += not ok print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}") print(f"\n{len(cases) - failed}/{len(cases)} passed") return 1 if failed else 0
def main(argv=None): p = argparse.ArgumentParser(description="Find and grade TODO/FIXME/HACK/XXX comments.") p.add_argument("paths", nargs="*", default=[], help="files or directories to scan") p.add_argument("--today", help="YYYY-MM-DD for overdue checks (default: today)") p.add_argument("--ext", help="comma-separated extensions to include, e.g. .py,.ts") p.add_argument("--json", action="store_true") p.add_argument("--strict", action="store_true", help="exit 1 on any problem") p.add_argument("--self-test", action="store_true") a = p.parse_args(argv) if a.self_test: return self_test() if not a.paths: p.error("give files/directories to scan, or --self-test") today = dt.date.fromisoformat(a.today) if a.today else dt.date.today() exts = {e if e.startswith(".") else "." + e for e in a.ext.split(",")} if a.ext else DEFAULT_EXT items = [] for root in a.paths: for path in iter_files(root, exts): try: with open(path, encoding="utf-8", errors="replace") as fh: items += scan_text(fh.read(), path, today) except OSError as e: print(f"warn: {path}: {e}", file=sys.stderr) if a.json: print(json.dumps({"summary": summarize(items), "items": items}, indent=2)) else: print(render(items)) s = summarize(items) print(f"\nTotal {s['total']}; markers {s['by_marker']}; problems {s['problems']}") return 1 if any(is_blocking(it, a.strict) for it in items) else 0
if name == "main": sys.exit(main())
