CFN Tech Debt
Purpose: Close the "later means never" gap. Every deliberate shortcut gets an inline cfn: comment naming its ceiling AND the trigger to revisit it. This skill greps those markers into a ledger and flags any marker with no trigger as a rot risk.
Pairs with cfn-arch (the build ladder decides when a shortcut is justified) and cfn-dry-review (finds accidental over-engineering; this tracks deliberate under-engineering).
The cfn: Marker Convention
Mark a deliberate simplification inline. The comment names what is limited and what should trigger an upgrade:
cfn: <ceiling>[,|;] <upgrade trigger>
A comma OR a semicolon separates ceiling from trigger — both are accepted. (Semicolon is in practice the dominant style.) The harvest splits on whichever comes first.
- Valid (names ceiling AND trigger):
# cfn: global lock, per-account locks if throughput matters// cfn: O(n^2) scan, switch to index when list > 1k rows// cfn: fixed retry/backoff at MVP; make per-error-class if a path needs more.// cfn: mirrors app.session_state CHECK; bump only if the migration CHECK moves.
- Invalid:
no-trigger(states a limit but no trigger to revisit; these silently rot):// cfn: this is the naive version# cfn: hardcoded for now
A marker may wrap across following comment lines; the trigger may live on line 2+:
/** cfn: process-local dedup; swap for a store-returned is_new flag
* when intake runs multi-instance. */
Rule: if you write a cfn: marker, name the trigger. A shortcut with no trigger is a leak, not a decision.
Inputs
$1:--persist(optional): write the ledger todocs/TECH_DEBT.md. Default: report to stdout only, change nothing.
Outputs
- stdout: one row per marker, grouped:
<file>:<line>: ceiling: <ceiling>. upgrade: <trigger|NONE>. - Summary line:
<N> markers, <M> with no trigger.(orNo cfn: debt. Clean ledger.) - Machine ledger (always written when
jqis present):.cfn-cache/tech-debt-ledger.json. Gitignored, project-scoped. Shape:{generated, total, no_trigger, markers:[{file, line, ceiling, upgrade_trigger|null, has_trigger}]}. This is the feedcfn-megaplanreads during scoping (see below). - With
--persist:docs/TECH_DEBT.md(human-facing markdown) - exit code: 0 = success
Feeds cfn-megaplan (the loop)
The JSON ledger closes the "later means never" loop. cfn-megaplan reads .cfn-cache/tech-debt-ledger.json during its Step 0 scope check and surfaces open debt in the files/area being planned as candidate backlog entries. Deliberate shortcuts become visible at the next planning pass instead of rotting. The harvester writes; megaplan only reads. Run this skill after implementation so the ledger stays current for the next plan.
Usage
# Report only (default)
$HOME/.claude/skills/cfn-tech-debt/harvest.sh
# Write/refresh the ledger file
$HOME/.claude/skills/cfn-tech-debt/harvest.sh --persist
How Harvest Works
Match: grep -rlE '(#|//|--|/[*]|^[[:space:]]*[*]).*cfn:' picks the candidate files; awk then re-scans each one. (File-based, not grep -n line-based, because the parser must look ahead at continuation lines — see Parse.) A marker counts when cfn: appears anywhere inside a comment, not only immediately after the comment token. All of these are real markers and all are harvested:
// cfn: global lock, per-account locks if throughput matters <- adjacent
// same seam. cfn: log + ignore at MVP; MP4 wires the handler. <- mid-comment
* cfn: mirrors deriveCostUsd's token math, unify on a third use <- JSDoc continuation
/** Backoff (ms) for attempt N. cfn: fixed base 100ms, tune if the pooler flaps. */
The earlier pattern ((#|//|--|/\*) ?cfn:) required the marker to sit adjacent to the comment token and silently dropped ~45% of real debt — whole files never reached the ledger. The rule is: prefer over-capturing a real marker to silently dropping one.
Scanning files (rather than grep -n output) also recovers markers in source files that grep classifies as binary — one stray NUL byte anywhere in a .ts file makes line-based grep -rn skip the whole file's content silently. grep -rl still lists it, and awk reads it regardless.
Parse: the marker body is everything after cfn:, joined across continuation comment lines — a wrapped marker keeps its trigger:
- Continuations must be comment lines in the same style as the marker line (
//→//, JSDoc*→*,#→#,--→--). - The join stops at the first non-comment line, a blank comment line, the closing
*/, a line that declares its owncfn:marker, or afterMAX_CONT(8) lines. - Only a pure comment line can start a wrapped marker. A trailing
const x = 1; // cfn: ...is parsed from its own line only, so an unrelated comment underneath it is never absorbed.
The joined body is then split on the first comma OR semicolon, whichever comes first → ceiling + upgrade trigger. Neither present → upgrade: NONE (the no-trigger rot flag). Both fields are truncated at 160 chars (MAX_FIELD) — the backstop against a runaway multi-line join swallowing the ledger with a paragraph of prose. A clipped (...-suffixed) trigger may hide the real upgrade condition further down the comment; read the source before judging such a row.
Reference exclusion (false-positive guard): prose that merely points at a marker living elsewhere is not itself a marker, and harvesting it manufactures a phantom row (and phantom rot) for a shortcut already counted at its real declaration site. A cfn: is treated as a reference, not a declaration, when:
- it is quoted (
`cfn:`,"cfn:",'cfn:') or directly preceded bysee— e.g.// Intentionally per-process (see cfn: note). - the word right after
cfn:ismarkerornote— a real marker's body opens with its ceiling, never those words — e.g.-- (documented cfn: marker in src/config.ts, not this file's concern)
Limits, honestly: this is a lexical rule. A reference phrased another way ("per the cfn: above") is still harvested, and a real marker that opened with a quote or with the literal word "note" would be dropped. Both are rare, and the bias stays over-capture rather than silently drop — only this narrow reference class is carved out. Test names like describe('... cfn: marker fold') are already excluded by the comment-opener requirement (no comment token on the line).
Exclusions:
- Dirs:
node_modules,.git,dist,dist-run,build,.next,coverage,worktrees,.artifacts,.backups(CFN's own pre-edit backups), and.cfn-cache. Build output and backups hold stale copies of source markers, which double-count the ledger..cfn-cacheholds this skill's own JSON ledger plus other skills' cached review diffs (.cfn-cache/diffs/*.diff) — harvesting a diff of the codebase re-harvests the codebase with the diff file as the "source", producing pure garbage rows. - File types:
*.md,*.mdx,*.markdown,*.rst,*.txt. Planning docs, specs, READMEs and this skill's owndocs/TECH_DEBT.mdonly quote marker text in prose; none is shipped code that can carry debt. Prose is excluded by extension, not by directory (planning/is not excluded as a path) so that real code living under a docs directory still reaches the ledger.
The harvest reads and reports by default. --persist is the only thing that writes a tracked file (docs/TECH_DEBT.md); the gitignored .cfn-cache/tech-debt-ledger.json is always refreshed.
Anti-Patterns
- Writing a
cfn:marker with no trigger (creates rot the ledger will flag) - Using
cfn:to excuse skipped validation, error handling, security, or accessibility (those are never legitimate shortcuts, seecode-quality.mdDefinition of Done) - Treating the ledger as a backlog substitute: high-impact
no-triggerrows should graduate tocfn-project-managementbacklog items
Related
cfn-arch: the build ladder that decides when a shortcut is justifiedcfn-dry-review: finds accidental over-engineering (opposite axis)cfn-project-management: promote rot-risk markers to backlogcfn-megaplan(reads.cfn-cache/tech-debt-ledger.jsonat scope time so open debt in the planned area surfaces as backlog candidates)