This file holds the column definitions, enum values, detail-block contract, presets, and anti-patterns. Read it when writing or validating a row.
UNFORGET.md is a single markdown file with 4 sections, each containing a 10-column rating table.
| Section | What goes here | ID prefix |
|---|---|---|
| 1. Paused plans | Work started, made progress, paused mid-execution. Each row points at a detail file. | P |
| 2. Session spillover | Items that surfaced mid-task in some other work. Captured in 1-2 lines. | S |
| 3. Audit findings | Items surfaced by audit tools (linters, code review, custom audit skills) that weren't fixed immediately. | A |
| 4. User-reported / observed | Bugs noticed but not reproduced, friction observed, "this feels off" notes. Lower bar than audit findings. | U |
# | Target | Finding | Urgency | Risk: Fix | Risk: No Fix | ROI | Blast Radius | Fix Effort | Status
| Column | Meaning |
|---|---|
| # | Stable ID (P1, S2, A3, U4). Never reuse. |
| Target | Release-cycle commitment (see "Target values" below) |
| Finding | One-clause description. Multi-line OK. |
| Urgency | 🔴 CRITICAL / 🟡 HIGH / 🟢 MEDIUM / ⚪ LOW |
| Risk: Fix | What could break making the change. ⚪ Low / 🟢 Medium / 🟡 High / 🔴 Critical |
| Risk: No Fix | Cost of leaving it (crash, data loss, user-visible bug). Same scale. |
| ROI | 🟠 Excellent / 🟢 Good / 🟡 Marginal / 🔴 Poor |
| Blast Radius | ⚪ 1 file / 🟢 2-5 files / 🟡 6-15 files / 🔴 >15 files |
| Fix Effort | Trivial / Small / Medium / Large |
| Status | A machine-readable @status: token (format v2+) followed by human narration. See "Status tokens" below. Legacy word-statuses (Open / In Progress / Fixed / Deferred / Skipped) still parse. |
The Status cell begins with one authoritative, machine-readable token that tools
read instead of parsing prose. Full spec: reference/status.md. In brief:
`@status:<value>` `@verified:<tier>` <human narration follows>
@statusenum:open·in-progress·done-verified·done-unverified·blocked·withdrawn.@verifiedtier (required ondone-verified):code·device·user·session-claimed.done-verifiedrequiresdeviceoruser(orcodewith a note);session-claimedcan never backdone-verified— a claim is not a verification.done-unverifiedis the "done-but-owed" state: fixed, not yet ground-truth-checked.archivemoves onlydone-verified/withdrawn;done-unverifiedis held back.- A token that contradicts its own narration (says
done-verifiedover prose that says "re-opened"/"still owed") is a lint error the verify pass flags.
Optional on legacy, expected on new. Rows written before v2 have no token and
still work (a tokenless row is never auto-archived). New and edited rows get a
token. Old word-statuses map: Open→open, In Progress→in-progress,
Fixed→done-verified (with a tier) or done-unverified (if unproven),
Deferred→open/blocked, Skipped→withdrawn.
Detection and validation delegate to scripts/parse_status.py (see reference/status.md for the algorithm fallback).
| Target | Meaning |
|---|---|
| 🔴 THIS | Must ship in current release cycle. Blocks submission. |
| 🔵 NEXT | First post-release point update. Triaged as fixable but not blocking. |
| 🟡 LATER | Two cycles out or more. Real work, not yet scheduled. |
| ⚪ SOMEDAY | No commitment. Captured so it doesn't get lost. May stay here forever. |
Invariant: 🔴 THIS is the only Target that blocks shipping. At submission time, every 🔴 THIS row must be Status = Fixed or have been demoted with a one-line reason.
Each section table is followed by a ### Detail - <section name> subsection. Detail blocks hold the prose context that doesn't fit in the 10-column table (the why, the file paths, the linked plan refs, the resolution history). One detail bullet per ID; the bullet starts with the ID in bold (- **P1** - ...).
Detail block format (four parts in order):
- Closure pointer (only if the row is closed). Lead the body with
**CLOSED YYYY-MM-DD: [one-sentence summary of how it closed].**This pointer is what makes a closed row's outcome scannable; future readers see "what happened" without reading the full body. For Open rows, skip this part entirely. - Body. History, files, plan refs, context, gotchas. Free-form prose. Length is whatever the row needs; some rows are one sentence, some are five paragraphs. The Finding cell in the table is the headline; the body is the article.
- Verify-still-open recipe (Open rows whose body cites specific file paths or line numbers; opportunistic on older rows). One line:
**Verify-still-open:** \` — expect: .` See the Verify-still-open recipe subsection below for the rationale and the three-layer cascade. - Spawn links (only if the row is part of a chain).
Spawned-from: <ID>if this row was created by closing another row (e.g., a workaround that spawned a real-fix follow-up).Spawns: <ID>if closing this row created a follow-up row. Both directions are recorded so the chain can be walked from either end.
Example (closed row with a spawned follow-up):
- **P3** - **CLOSED 2026-04-20: hidden the menu entry until server signing lands. Spawns: P6.** Every item that showed the Wallet feature failed when the user completed the flow. Blocked on server `/api/wallet/sign-pass` + Apple Pass Type ID. Pre-submission decision: complete the worker endpoint OR hide the menu item (~30 min if hiding). Chose hiding for build 13; future endpoint work tracked at row P6.
Example (open row with no spawn link):
- **P4** - Search relevance overhaul. Phases 1-4 (term weighting, stop words, fuzzy match, synonym expansion) shipped in build 11. Phases 5-7 (personalization, click-through learning, query rewriting) require a Cloudflare D1 schema we don't have yet. Plan: `~/.claude/plans/search-overhaul.md`.
The format is intentionally simple: closure pointer, body, verify-still-open recipe, spawn links. The skill's add / edit / promote flows preserve this structure when they touch a detail block. Hand-editing a detail block is fine as long as the four parts stay in order and the closure pointer (if present) stays at the top.
A ledger row is a one-line index; the history, code-traces, and verification narration live in the detail block, NOT fused into the Finding or Status cell. This is the rule that keeps a list or a Read from truncating mid-row and misleading the reader — the 2026-07-25 failure was a ~155KB ledger with multi-KB rows whose Reads blew past the token cap and cut off the one fact that mattered.
The two-part row (§2a):
- The table row carries a compact index: ID · Target · a ONE-LINE finding summary · the rating columns · the
@status:/@verified:token(s) + one-line current status. Soft budget: the Finding and Status cells each ≤ ~400 chars (registry-configurable viarow_char_budget; theverify/scanlint reads it). This is whatlistrenders and what stays greppable. - The detail block (a
### Detail - <section>bullet,- **<ID>** - …) holds the unbounded content: history, files, plan refs, the verify-still-open recipe. It doesn't bloat the scannable table.
History is APPENDED to the block, not grown in the cell (§2b): a status CHANGE appends a dated line to the detail block; the table cell's one-line status is REPLACED (not grown) to the latest. The table always shows current state in one line; the block holds the audit trail. The failure mode this bans is history accreting inside the table cell (U5's Status cell was ~8KB of dated narration).
The hard rule (never violated): the budget MOVES history to the detail block; it NEVER deletes it. A split is a bounded index + an unbounded block, not a bounded row that drops facts. The tooling refuses any split it cannot prove is lossless.
Preferred implementation: scan/verify flag over-budget cells (the char-budget check), and the split is delegated to a helper:
python3 scripts/row_budget.py check --file <UNFORGET.md> [--dir <ledger-dir>] # flag over-budget cells
python3 scripts/row_budget.py split --file <UNFORGET.md> --id <ID> [--headline "<summary>"] [--apply]
split produces a bounded index row (Finding replaced by a one-line headline + → see detail block **<ID>**; the @status/@verified tokens KEPT in the table so list/archive still read them) and a detail-block bullet holding the full original cell content verbatim. It returns lossless:true only when every character of the original cells is provably present in the block, and refuses otherwise. --apply writes it; the default is a dry-run plan. The one-line headline is the LLM's judgment call (pass --headline); absent one, the tool derives a mechanical, lossless-safe headline (first clause + pointer). See reference/commands.md § /unforget scan and § /unforget verify.
Algorithm fallback (Python unavailable): for each table row, if the Finding or Status cell exceeds ~400 chars, move everything past a one-line headline into a - **<ID>** - bullet under the section's ### Detail - <section> block (verbatim — lose nothing), leave a → see detail block **<ID>** pointer in the Finding cell, and keep the @status:/@verified: token in the table with only a one-line current status. Never delete content; if you can't preserve it, don't split.
Rows decay independently of fixes. A row logged a week ago can be silently stale: someone reorganized the file, an unrelated PR moved the line, a parallel session shipped the fix without closing the ledger. Before writing any code for a row, run a 10-second grep to confirm the row's premise still matches the current source.
The convention: every Open row whose detail block references specific file paths and line numbers SHOULD include a one-line **Verify-still-open:** <command> recipe. Old rows pick this up opportunistically when next touched; new rows get one at write time.
Format:
**Verify-still-open:** `grep -nE "try\?|context\.save" Sources/Views/Navigation/MyProductsWrapper.swift` — expect: a `try?` on `context.save()`. If the grep shows `do { try ... } catch { ... }` instead, the row is stale-Fixed.
Three layers, cheapest to most expensive:
- File-existence. Does the file referenced in the row's detail still exist at the cited path?
ls <path>— if it returns "No such file or directory," the row is stale; the file got moved or deleted in a refactor. - Line-content. Does the line referenced still contain the anti-pattern the row describes? Grep the exact path for the exact pattern. If the pattern is gone, the row is stale-Fixed; flip Status to
Fixedand write a closure pointer of the form**CLOSED YYYY-MM-DD (verify-only — no code change needed).**. - Symbol/parameter shape. If the row says "would require a
fooparameter onBarView" and grep finds that parameter already exists, the fix shipped silently. Same outcome — Fixed (verify-only).
Where to put it: at the end of the detail block's body, before any Spawn links. One line. The intent is "this is a checklist item, not a discussion."
When to add a recipe to old rows: opportunistically — when you next read or work the row, write one. Don't do a batch backfill; the older rows are the ones most likely to be stale anyway, and writing recipes for them at backfill time costs more than just verifying them at pickup time.
Why this matters: the convention originated after a 2026-05-12 cleanup pass in an adopting project where two of four "fixes" in a single sitting turned out to be pre-shipped (only the ledger was stale). Ten-second greps at the start of each row would have saved ~50 minutes of redundant code work. The recipe makes that grep a structural part of the row, not a habit the next reader has to remember.
unforget init offers three presets at setup. Each is opinionated and complete; users pick the closest fit, and the skill adapts the table format accordingly.
| Preset | Audience | Columns |
|---|---|---|
| Standard | Mobile/desktop apps shipping discrete releases | All 10 columns above with Target |
| Compact | Projects that prefer a narrower table; same release-cycle semantics as Standard | 9 columns. Drops the dedicated Target column and inlines Target as a leading badge inside the Finding cell (e.g., **🔴 THIS · Apple Wallet pass broken promise**). All other columns identical to Standard. |
| Lean | Solo devs, side projects, junior devs | 6 columns: # / Target / Finding / Urgency / Effort / Status |
| Continuous | Web apps, services, libraries with continuous deployment | 9 columns; replaces Target with Window (🟢 NOW / 🟡 THIS WEEK / 🔵 THIS MONTH / ⚪ SOMEDAY) |
Users on Standard, Compact, or Lean can also append extra columns (Client, Sprint, Component, etc.) without modifying core columns. Removing or renaming core columns is intentionally not supported, because it breaks comparability across projects and tooling.
Projects shipping a user-facing app (App Store / Play Store) can append a 1-Star Risk column that rates each row's exposure to a one-star review. It is an appended extra column, not a core column — it works in either format version (v1 or v2) and doesn't change which one the file declares; tooling that doesn't know about it ignores it, and projects that don't ship a public app simply omit it. Append it as the last column, after Status. (The status tooling finds the @status token by content, not by position, so an appended 1-Star Risk column after Status is read correctly — see scripts/parse_status.py § status_cell.) There is a live example in examples/UNFORGET.md.
What it holds. A three-zone risk strip — At risk · Watch · Clear (left→right = riskier→safer) — with a single ★ marking the row's band. The rating semantics are borrowed wholesale from the one-star-risk skill; when that skill is installed, /unforget can delegate the scoring to it. The strip is not a decoration: it is the compressed output of a one-star-risk judgment. one-star-risk is the shipped default filler for the ship-risk-scoring companion function — its skill mapping and URL live in the companion manifest (reference/skill-handoffs.md), the single place a companion link is written; resolve it there rather than hardcoding a URL at this column.
In-cell rendering (markdown table). Make the strip one unbroken inline-code span with no internal spaces — spaces and | let the renderer wrap or break the column. The ★ sits in the third of the bar matching the band; its position within that third is a lean, not a percentage. Put the band glyph + zone word on the next line via <br> so color is never the only cue:
| … | Status | 1-Star Risk |
| … | Open | `risk‹★────────›clear`<br>🔴 At risk (deep) |
| … | Open | `risk‹─────★───›clear`<br>🟡 Watch (mid) |
| … | Fixed | `risk‹────────★›clear`<br>🟢 Clear (border) |
| … | Open | `risk‹─────────›clear`<br>⚪ n/a |
Hard rules (inherited from one-star-risk):
- The zone is the band and is firm; the star's position within the zone is only a lean — one of three named lean-words (
deep/mid/border), how near the skeptic pass came to the adjacent band. Never a percentage or computed coordinate (pos: 62%is forbidden), and never let two same-zone rows be compared by strip position. - Accessibility: always pair the strip with the band glyph (🔴/🟡/🟢/⚪) and the zone word in text ("At risk / Watch / Clear"). The strip never encodes the band by position alone.
- Most unforget rows are
⚪ n/a. Paused plans, spillover TODOs, and internal audit findings usually aren't user-visible App Store risks. That is expected — the value is the non-⚪ few. Do not inflate bands to fill the column. 1-Star Risknever drives the Target/ship-gate. It is advisory context alongside the existingRisk: No Fixcolumn, not a replacement for it. A 🔴 At-risk row still ships or defers on itsTarget, same as any other row.
Compact preserves Standard's release-cycle semantics; only the rendering changes. Conversion is mechanical:
- Standard cell:
🔴 THISin column 2,Apple Wallet pass broken promisein column 3. - Compact cell: column 2 is dropped; column 3 (Finding) becomes
**🔴 THIS · Apple Wallet pass broken promise**.
The leading **🔴 THIS · ...** form is the contract: a literal Target badge (one of 🔴 THIS / 🔵 NEXT / 🟡 LATER / ⚪ SOMEDAY), the literal middle dot · separator, and the original Finding text. Bold the whole prefix-plus-Finding so the badge stays visually anchored.
Why Compact exists. A 10-column rating table can wrap or render as vertical blocks in narrow terminals. Compact saves one column without losing the Target signal: grep '🔴 THIS' still finds ship-blockers, the Finding cell gets more room before hitting its character limit, and tooling that reads the file can still parse Target via a simple regex on the leading badge.
When to pick Compact at init. Compact is offered alongside Standard / Lean / Continuous in Phase 1's preset prompt for projects that report narrow terminals or that already use the inlined-badge convention informally. Conversion between Standard and Compact is lossless and can be done at any time by /unforget edit or a future /unforget migrate flow.
Tooling expectations. Read operations (list, scan, promote --dry-run) detect the preset from the column count and section headers and adapt parsing accordingly. Write operations (add, edit, import, promote) preserve the preset that's already in use; the skill never auto-converts a Compact file to Standard or vice versa.
Continuous deployment has no discrete release cycles, so the file-header fields that Standard / Compact / Lean rely on (Last promoted: / Currently shipping toward:) have no defined meaning. Continuous files use these equivalents instead:
Last sweep:— date of the most recent/unforget scanor/unforget promotepass. Stamped automatically by either command.Active focus:— free-form text naming the current major work theme (e.g., "Q3 latency reduction" or "v2 API migration"). The user updates this manually whenever the focus shifts; there is no automated promotion ritual driving it.
The four Window values (🟢 NOW / 🟡 THIS WEEK / 🔵 THIS MONTH / ⚪ SOMEDAY) replace the four Target values. The promotion concept doesn't apply to Continuous: items don't auto-promote between Windows because there's no release cut driving the move; the user re-rates Windows during weekly or sweep-time reviews.
The Continuous-preset header on a fresh init looks like:
**Last sweep:** 2026-05-04
**Active focus:** Q3 latency reduction
Standard / Compact files keep Last promoted: and Currently shipping toward: exactly as documented elsewhere in the spec; only Continuous swaps them.
Things this skill deliberately does NOT do, and why:
- Custom column reordering. Breaks comparability across projects.
- Custom rating scales. Letting one user use 🔴/🟡/🟢/⚪ and another use P0/P1/P2/P3 makes the format un-shareable.
- Per-row column visibility. Hiding columns on some rows but not others. Devolves into chaos.
- Renaming core columns. "Call Urgency 'Priority' instead." Skill becomes incompatible with itself.
- Multiple files. UNFORGET.md is the index. Detail files (per-plan markdown) are linked FROM rows, not duplicates of them.
- Auto-deferring things the AI thinks should be deferred. Deferral is a user decision. The skill captures, organizes, and surfaces; it doesn't decide on the user's behalf.