Act as a TODO Comment Debt Tracker — ChatGPT Prompt

Act as a TODO Comment Debt Tracker — ChatGPT Prompt

Chatgpt-prompts#chatgpt#text prompt#todo comment debt
Last updated: October 23, 2026

The Prompt

--- 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 churn - `references/triage-rules.md` - how to decide fix now, ticket, or delete - `templates/debt-report.md` - report for a sprint review, release checklist, or PR comment - `examples/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 1. Run the scanner and publish the report; do not mass-edit. 2. Apply the convention to new comments via code review or a lint rule. 3. Fix old comments when you touch that code anyway. 4. Revisit the totals monthly; aim for "no overdue, no untracked FIXME". FILE:references/triage-rules.md # Triage rules For each item, ask in order: 1. **Is it still true?** Read the surrounding code and `git log -L` for the lines. If the issue is gone, delete the comment. 2. **Is it under 30 minutes?** Fix it now in a small PR that removes the comment. 3. **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. 4. **Is it a workaround?** (HACK) Write down the condition that allows removal ("remove after vendor SDK 5.x") and a due date. 5. **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:** <name> **Scanned:** <paths> **Date:** <YYYY-MM-DD> ## 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*(?P<marker>TODO|FIXME|HACK|XXX)\b" r"(?:\s*\((?P<paren>[^)]*)\))?(?:\s*\[(?P<bracket>[^\]]*)\])?\s*:?\s*(?P<text>.*)$") 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="<text>", 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="<text>", 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(carol): https://example.com/issues/9 replace hero image --> * 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())

FREE AI

Make it yours

Describe one change and free AI rewrites this prompt for you.

Works with

ChatGPT, Gemini, Claude - copy and paste. For images, use it with Midjourney, DALL-E, or Gemini image mode.

Tip: swap names, colors, or details to make it yours. Shorter prompts often work better - remove lines you don't need.

Related Prompts

Keep exploring

This prompt is part of our Best ChatGPT Prompts collection, the closest match for this style. You can also browse the full ChatGPT prompts collection, where every prompt is free to copy.

For ideas beyond this topic, discover more free AI prompts on PromptBee.

Copied to clipboard

Prompt copied!

Gemini doesn't allow auto-fill from links, so paste it yourself - takes 2 seconds:

  1. Gemini opened in a new tab
  2. Tap the message box in Gemini
  3. Tap Paste, then Send

Keep exploring related prompts

More ideas for the same category, ready to copy.

Next: Act as an Identity-Locked Image Transformation Prompt — ChatGPT Prompt

Browse this category →