Skip to content

Latest commit

 

History

History
223 lines (148 loc) · 20.2 KB

File metadata and controls

223 lines (148 loc) · 20.2 KB

unforget — Format reference

This file holds the column definitions, enum values, detail-block contract, presets, and anti-patterns. Read it when writing or validating a row.


The format

UNFORGET.md is a single markdown file with 4 sections, each containing a 10-column rating table.

Sections

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

Columns (Standard preset, 10-col)

# | 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.

Status tokens (format v2+)

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>
  • @status enum: open · in-progress · done-verified · done-unverified · blocked · withdrawn.
  • @verified tier (required on done-verified): code · device · user · session-claimed.
  • done-verified requires device or user (or code with a note); session-claimed can never back done-verified — a claim is not a verification.
  • done-unverified is the "done-but-owed" state: fixed, not yet ground-truth-checked. archive moves only done-verified/withdrawn; done-unverified is held back.
  • A token that contradicts its own narration (says done-verified over 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 values

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.

Detail blocks

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):

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Row-length discipline (format v2+): the table row is a bounded INDEX

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 via row_char_budget; the verify/scan lint reads it). This is what list renders 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.

Verify-still-open recipe (before working a row)

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:

  1. 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.
  2. 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 Fixed and write a closure pointer of the form **CLOSED YYYY-MM-DD (verify-only — no code change needed).**.
  3. Symbol/parameter shape. If the row says "would require a foo parameter on BarView" 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.

Presets

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.

Optional column: 1-Star Risk

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 Risk never drives the Target/ship-gate. It is advisory context alongside the existing Risk: No Fix column, not a replacement for it. A 🔴 At-risk row still ships or defers on its Target, same as any other row.

Compact preset detail

Compact preserves Standard's release-cycle semantics; only the rendering changes. Conversion is mechanical:

  • Standard cell: 🔴 THIS in column 2, Apple Wallet pass broken promise in 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 preset detail

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 scan or /unforget promote pass. 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.


Anti-patterns

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.