Imported from Terryc21/unforget (
SKILL.md). Install upstream withnpx skills add Terryc21/unforget. Copyright stays with the author (Apache-2.0).
unforget
Installed as a Claude Code plugin (current version: v1.0; plugin install available since v0.2). Manual install via
~/.claude/skills/unforget/(invoked as/skill unforget) still works as a v0.1 fallback.
A way of not losing sight or track of what is deferred.
Why this skill exists
Every developer defers things. The problem isn't the deferral. The problem is that deferred items end up scattered across:
- a
Deferred.mdat the repo root - date-prefixed plan files in some "deferred" folder
- audit-tool ledgers (radar-suite, ESLint TODO comments, etc.)
- Slack DMs to yourself
- comments in code (
// TODO: come back to this) - memory files for AI assistants
- paused plan files in
~/.claude/plans/
When the user asks "what's deferred?" months later, the answer requires walking every one of those surfaces. Items go stale. Some get fixed by accident. Some sit forever because nobody remembered them.
unforget collapses all deferral into ONE file (UNFORGET.md) with a structured format that:
- Forces the deferral question ("when does this ship?") via the Target column.
- Surfaces staleness via a built-in scan command that flags items past their age threshold.
- Standardizes the format so any developer reading any project's UNFORGET.md instantly recognizes the structure.
The pattern was extracted from a real Universal app (iOS, iPadOS, macOS) where deferred work had fragmented across five tracking surfaces. Consolidation freed roughly 3 hours of release-prep time per cycle.
Format at-a-glance
UNFORGET.md is a single markdown file with 4 sections, each containing a rating table whose width depends on the preset (10 columns for Standard, 9 for Compact / Continuous, 6 for Lean).
Sections: 1. Paused plans (P) ยท 2. Session spillover (S) ยท 3. Audit findings (A) ยท 4. User-reported / observed (U)
Columns (Standard preset): # | Target | Finding | Urgency | Risk: Fix | Risk: No Fix | ROI | Blast Radius | Fix Effort | Status
Target values: ๐ด THIS (blocks current release) ยท ๐ต NEXT (next post-release update) ยท ๐ก LATER (two cycles out) ยท โช SOMEDAY (no commitment)
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.
Full format spec lives in reference/format.md: column meanings, Status enum, detail-block format (closure pointer โ body โ verify-still-open recipe โ spawn links), Standard / Compact / Lean / Continuous presets, and anti-patterns. Read that file when writing or validating a row.
Open rows whose details cite specific file paths SHOULD carry a **Verify-still-open:** one-line recipe in the detail block โ a 10-second grep that confirms the row's premise still matches the current source. Rows decay independently of fixes (refactors move lines, parallel sessions ship silent fixes); the recipe makes that grep a structural checkpoint, not a habit. See reference/format.md ยง Verify-still-open recipe for the three-layer cascade.
Subcommand surface
| Subcommand | One-line purpose | Full spec |
|---|---|---|
/unforget init |
Bootstrap UNFORGET.md and survey existing deferral artifacts across the project | reference/init.md (with surface detail in reference/surfaces.md) |
/unforget add |
Capture a new deferral (defaults to Section 2 / Session spillover); 30s end-to-end | reference/commands.md |
/unforget edit |
Refine a row's columns; closure recommendations on --status=Fixed |
reference/commands.md |
/unforget import |
Re-run the surface survey after init (catches NEW artifacts) | reference/commands.md (surface detail in reference/surfaces.md) |
/unforget list |
Show current state, filterable by section / Target / Urgency / age / staleness; --view= (all/open/done/split/next) picks which rows, --group-by= (target/section/none) picks the grouping, --ledgers=/--all-ledgers unions registered sibling ledgers, --fresh re-runs the display-preference interview |
reference/commands.md |
/unforget show |
Synthesized current-state read for ONE row (Finding/Impact/Fix, no history); --full appends the raw Detail block; markdown baseline, optional interactive card view where available |
reference/commands.md |
/unforget scan |
Identify rows past their staleness threshold; read-only | reference/commands.md |
/unforget branch |
(format v2+) Atomically create a child ledger (header + parent pointer + registry entry, all-or-none) when work differs on the actor / lifespan / domain axis | reference/branching.md (summary in reference/commands.md) |
/unforget verify |
(format v2+) Integrity lint: contradictions, unproven "done", bloat, dangling detail-block pointers, stale recipes, registry drift; read-only; gates archive/promote |
reference/verify.md |
/unforget archive |
Move completed (Done/Fixed) rows out of the active tables into an archive file; lightweight, run anytime; holds back "Done-but-owed" rows | reference/commands.md |
/unforget promote |
Release-time ritual: verify ๐ด THIS rows fixed, promote ๐ต NEXT to ๐ด THIS | reference/promotion.md (with backups in same file) |
/unforget --version |
Print version, install path, supported format-version; install-verification | reference/commands.md |
Decision flowchart: which subcommand do I run?
- No UNFORGET.md exists in the project yet โ
/unforget init - You want to capture one new item, fast โ
/unforget add "<finding>" - You want to update an existing row's columns โ
/unforget edit <ID> - A new audit / plan / memory file appeared since init โ
/unforget import - The user just asked "what's deferred?" โ
/unforget list(or/unforget list --target=THISfor ship-blockers only) - You've picked one row to actually work on and want its current state, not its whole history โ
/unforget show <ID>(add--fullfor the complete raw history) - You want to find rows that have aged past their thresholds โ
/unforget scan - The user wants to change how
list/scandisplay by default, or says "run fresh" / "ask me what I want to see" โ/unforget list --fresh(depth-gated interview, saved per-project; seereference/commands.mdยง Display-preference interview) - Deferred work differs on actor (a different human acts on it) / lifespan (a sprint with its own discipline) / domain (a different repo or subject) โ
/unforget branch(but default to a row or section โ seereference/branching.md) - Completed rows have piled up and you want them out of the active view โ
/unforget archive(lightweight; use this between releases instead ofpromote) - You're about to ship a release โ
/unforget promote - You want to verify the install loaded correctly โ
/unforget --version - A row is being closed (
/unforget edit <ID> --status=Fixed) and you want the post-fix sweep โ seereference/promotion.mdยง post-fix-sweep
Companion files
This SKILL.md is intentionally thin. The full spec is split across reference/*.md files loaded on demand:
| File | What's in it | Loaded when |
|---|---|---|
reference/format.md |
Column definitions, Status / Target enums, detail-block format, presets, anti-patterns | Writing or validating a row |
reference/init.md |
Phases 1โ7 of the init walkthrough, success criteria | Running /unforget init |
reference/surfaces.md |
Six core surfaces, Surface 1b general doc scanning, redirect-pointer pre-check, memory-dir resolution, path encoding, meta-file pre-check, audit-tool format-aware parsing, cross-surface dedup, GitHub-issues four states, algorithm fallback | Running init or import, or auditing surface behavior |
reference/promotion.md |
Promote ritual, dry-run mechanics, post-fix-sweep workflow, backups and recovery | Running /unforget promote or marking a row Fixed |
reference/commands.md |
Per-subcommand specs for add, edit, import, list, show, scan, archive, --version (incl. --version's install-integrity + recall-trigger checks) |
Running any of those subcommands |
reference/status.md |
(format v2+) @status / @verified tokens: the status enum, the done-verified-requires-device/user rule, the tokenโnarration contradiction rule, archive invariant, provenance |
Reading/writing a row's status; running archive/list/edit |
reference/registry.md |
(format v2+) the registry: schema (global config + per-ledger), README-canonical rule (README wins over the .unforget.json cache), where it lives |
Resolving where ledgers live / reading persisted posture & policies |
reference/verify.md |
(format v2+) the verify/doctor integrity lint: the checks, read-only rule, archive/promote gating, enforceable verify-still-open recipe |
Running /unforget verify; before archive/promote |
reference/deferral-gate.md |
(format v2+) the deferral gate at add: the trivial tripwire, the "why not now?" allow-list, and the session defer/fix accounting that backs it |
Running /unforget add; showing the session readout on list |
reference/branching.md |
(format v2+) the branching model: the three axes (actor / lifespan / domain), the decision cascade, parent/child conventions, and the atomic branch command |
Deciding whether work earns a child ledger; running /unforget branch |
reference/skill-handoffs.md |
(format v2+) companion skill handoffs: the 5 functions, the global manifest, install-state detection by invocable name, frequency governance, the shipped-default disclosure | Firing a companion recommendation at a done/promote/verify transition |
scripts/*.py |
Deterministic helpers (surface scan, fuzzy dedup, path encoding, format-version check, backup prune, status-token parse, registry read/write, integrity verify, deferral gate + tally, atomic branch creation, recall-block writer, import drift detector, row-length check + lossless split, companion manifest + resolver, display-preference resolver). JSON in / JSON out. Standard library only. See scripts/README.md. |
Whenever the corresponding reference file delegates to a script |
Spec-substitution principle. This SKILL.md is the index, not the spec. When implementing or modifying any subcommand, Read the linked reference file before acting. The reference files are authoritative.
How to use unforget alongside CLAUDE.md / AGENTS.md
The skill works best when the project's main AI instructions file has a section that points at UNFORGET.md as the canonical deferral source. /unforget init offers to add this for you. Example block:
## Deferred Work Index
**Single source of truth:** `Documentation/Development/Deferred/UNFORGET.md`
Read this file when:
- The user asks "what's deferred?", "what's the backlog?", "prioritize my plans," or any variant.
- Before suggesting a release / submission, to check ๐ด THIS rows for unresolved blockers.
- When a task in the current session needs to be deferred, log a row here. Do NOT create a new tracking file unless the entry needs detail beyond one row.
**Format:** 10-column rating table per section. **Sections:** Paused plans / Session spillover / Audit findings / User-reported.
**Target column** is the release-cycle commitment: ๐ด THIS / ๐ต NEXT / ๐ก LATER / โช SOMEDAY.
Never log deferred items elsewhere. Memory files, plan files, and audit ledgers are detail stores; UNFORGET.md is the index.
This block is what makes the skill's recall trigger work. Without it, future AI sessions don't know to read UNFORGET.md when the user asks about deferred work.
Compatibility notes
- Non-Claude-Code use: UNFORGET.md is plain markdown. The format works fine in any editor, on GitHub, in Linear, etc. The slash commands require Claude Code, but the file itself is portable.
- Multi-user / team use: UNFORGET.md commits to git like any other markdown. Concurrent edits use standard merge resolution. Status changes between Open / In Progress / Fixed should be done atomically per row to minimize merge churn.
- Other AI assistants: The "Deferred Work Index" block in CLAUDE.md / AGENTS.md works for any AI that reads project instructions. Cursor, Copilot, Aider, etc. can all benefit from the recall trigger pattern.
- CI integration:
/unforget scanoutput is structured markdown. A simple GitHub Action can run the scan weekly and post the report to a Slack channel or open an issue. Thescripts/*.pyhelpers are standalone and can be invoked from CI without Claude Code. - Python 3.9+: the helper scripts under
scripts/use Python 3.9+ standard library only (no third-party deps). When Python is unavailable, eachreference/*.mdfile that delegates to a script keeps an "Algorithm fallback" paragraph the LLM can re-derive from. The fallback is functional but slower and non-deterministic; install Python 3.9+ for the canonical implementation.
Format-version contract
Every read operation (add, list, promote, scan, edit, import, verify) checks for an HTML comment marker of the form <!-- unforget-format: vN --> near the top of UNFORGET.md. The marker declares which version of the unforget file format the file conforms to. This skill (v2.0) supports formats v1 and v2. v2 adds the @status/@verified status tokens, the registry, the verify lint, the deferral gate, branching, the onboarding/recall-block wiring, the row-length discipline (bounded index rows + lossless splits), and the companion-skill handoffs (functionโmanifest, invocable-name detection); a v1 file has none of those and is read/written as a legacy ledger (tokens optional, never required). Three cases:
- Marker absent. The skill prompts: "this file may not be in unforget format; proceed anyway?" Default response is no. If the user proceeds, the skill operates as best it can without format guarantees, and recommends adding
<!-- unforget-format: v2 -->near the top of the file to silence the prompt on future reads. - Marker recognized (
v1orv2). The skill proceeds normally. Av1file is treated as a legacy ledger: the v2-only features (status tokens, registry,verifyerrors) simply don't apply; nothing is required or auto-added until the file is upgraded tov2. - Marker is a future version (
v3or higher). The skill prints: "this file declares unforget format vN, but this skill version supports up to v2. Operating in read-only mode; writes are refused." Read-only operations (list,scan,verify, andpromote --dry-run) still work. Write operations (add,edit,import, andpromotewithout--dry-run) refuse with a one-line error pointing to the version mismatch and recommending a skill upgrade.
Preferred implementation: delegate the marker read to python3 scripts/check_format_version.py <path-to-UNFORGET.md> (returns JSON). Algorithm fallback if Python is unavailable: read the first 30 lines of the file, grep for <!-- unforget-format: v (case sensitive), parse the version digit, compare against supported.
๐ Asking the user anything: the governing rule
Ask about the OUTCOME in the user's words. Never about the mechanism in the skill's words.
This governs every question this skill puts to a user โ the --fresh display interview, init's onboarding questions, a branch confirmation, an edit prompt, and any question added later. It is not a --fresh rule that happens to generalize; it is a skill-wide rule that --fresh happened to expose.
The reference files below are written for the implementer, so they name config keys (display_view, git_posture, archive_nudge_threshold) and flag values (all/open/split, maintained/manual/none). Those names are specification, not prompt copy. Translate every one before it reaches a user.
- Name what the user GETS, not what gets configured. "Only unfinished work, everything, or one 'do this next' pick" โ never "a view preset." A user should never have to model this skill's internals to answer this skill's own question. If an option can only be understood by someone who has read the reference file, it is not written yet.
- Ask about the result, not the effort. "What do you want the table to display?" โ never "How much do you want to set?" An effort-framed question makes the user budget time before knowing what they'd get, and forces every option to re-explain the subject from scratch.
- No interview bookkeeping in the prompt. No question counts in labels or descriptions. A count answers "how long is this," competing with "what am I choosing" at the moment of decision.
Origin (2026-08-13). A live --fresh run rendered reference/commands.md's key names straight into the prompt; the user's report was that it gave "not much context as to how to answer." The repair took three passes โ key names โ outcomes, then effort-framing โ outcome-framing, then removing question counts the second pass had added โ because each pass fixed only what was pointed at, with no stated principle to apply. This rule is here, in the index every session reads, so the next question is written right the first time rather than corrected after a user hits it.
Where the rule is applied today (v2.8.0): reference/commands.md ยง Display-preference interview (all seven --fresh questions) and reference/init.md (git posture, cadence, recall โ the three of its five that named mechanism; the file-path question was already outcome-framed and was deliberately left alone). Both files carry an **Ask as:** line per question giving the wording that reaches the user.
โ ๏ธ init deserves the most care of any interview in this skill โ it is the only one where every respondent is new by definition. A confused user in --fresh already has a ledger and can decline; a confused user in init never gets one. Apply this rule hardest there.
See reference/commands.md ยง The governing rule for EVERY question in this interview for the long form and worked examples.
Anti-patterns (summary)
Things this skill deliberately does NOT do: custom column reordering ยท custom rating scales ยท per-row column visibility ยท renaming core columns ยท multiple files ยท auto-deferring on the user's behalf.
See reference/format.md ยง Anti-patterns for why each is banned โ that file is the single source; this line is only the index.
Changelog
v2.8.0 โ every user-facing question now asks about outcomes, not mechanism (2026-08-13) ยท minor
Documentation and prompt-wording only โ no script, format, or behavior change. A v1 or v2 ledger is untouched; every command does exactly what it did in v2.7.0. What changed is what the skill says when it asks the user something, which is why this is minor rather than patch: it alters the interview a user actually sees.
- A new skill-wide governing rule (
SKILL.md ยง Asking the user anything): ask about the OUTCOME in the user's words, never about the mechanism in the skill's words. It governs every question this skill asks โ--fresh,init,branchconfirmations,editprompts, and anything added later. Placed in SKILL.md deliberately: the rule was first written insidereference/commands.md, which is only loaded when working onlist/add/edit, so a session runninginitnever read it. Three sub-rules: name what the user GETS (not what gets configured); ask about the result, not the effort; no interview bookkeeping (question counts) in the prompt. reference/commands.mdโ all seven--freshquestions now carry an**Ask as:**line with the wording that reaches the user, alongside the key names that remain as implementer spec. The gate question changed from "How much do you want to set?" to "What do you want the Unforget table to display?", tier labels dropped their question counts, andview preset/grouping/verbosity/multi-ledger union defaultwere translated to plain outcomes.reference/init.mdโ three of five onboarding questions given the same treatment: git posture โ "Who should be able to see these notes?", cadence preset โ options that lead with the shipping pattern rather than the preset name and column count, recall block โ "Should future AI sessions know these notes exist?" The file-path question was already outcome-framed and was deliberately left alone; the cadence question's stem was already right and is marked โ so a future editor doesn't "fix" what works. Recommends dropping Custom from cadence's spoken options โ a 12-column pool is not a question a first-time user can answer before seeing one row.- Why
initgets the strongest wording: it is the only interview in this skill where every respondent is new by definition. A confused user in--freshalready has a ledger and can decline; a confused user ininitnever gets one.
Origin (2026-08-13, one live session). A --fresh run rendered commands.md's key names
straight into the prompt; the user reported it gave "not much context as to how to answer." The
repair took three passes โ key names โ outcomes, then effort-framing โ outcome-framing, then
removing the question counts the second pass had added โ because each pass fixed only what was
pointed at, with no stated principle to apply. The rule exists so the next question is written
right the first time.
โ ๏ธ Deliberately NOT done: a sweep of other skills. Two skills sampled from the same
installation (tutorial-creator, ui-enhancer-radar) already follow this principle without ever
having been told it โ one even stores to an internal USER_EXPERIENCE key while never showing that
name to the user. The failure appears specific to a skill whose subject matter is its own
configuration, where leaking key names is the path of least resistance. A blanket rollout would
have been solving a problem with two counterexamples and no supporting evidence. The real
structural gap โ no mechanism propagates a convention across skills โ is noted here rather than
papered over; skill-reviewer is the plausible home for such a check.
v2.7.0 โ list --fresh display preferences; a registry-wiping write bug; spec-vs-code reconciliation (2026-08-13) ยท minor
Additive and backward compatible, but it closes a data-loss bug in registry.py and lands
two verify checks the v2.3.0/v2.6.0 changelog entries already claimed were shipped. The
through-line is the one this release exists to attack: a value stated in one place and cited
from another, with nothing reconciling them. Four instances were found in a single session โ
the changelog describing checks the code lacked, a test golden pinned five versions back, a
plugin manifest five releases stale, and a spec asserting a merge that did not exist.
- ๐
registry.py write --mergeโ fixes a registry-wiping partial write.writehad only replace semantics, so a caller saving ONE key rendered a block in which every other global key was(unset)and the Ledgers table was empty. Measured, not theorized: a{"global": {"display_view": "open"}}write against a live 9-key/3-ledger registry left 9 nulls and 0 registered ledgers โ precisely the stranded-ledger failure the registry exists to prevent, reachable by following the then-current spec prose literally.--mergeapplies PATCH semantics (keys absent from the payload keep their value; a payload omittingledgersleaves the table untouched) and is now MANDATORY for any partial write. Full-state writers (init,branch) may still use either. verifychar-budget hard threshold (the v2.3.0 entry, now actually implemented).--char-budget-hard(default 4x soft = 1600) escalates an over-budget cell fromwarntoerror, gatingarchive/promote. The installed script had every over-budget row atwarnregardless of size, so the documented escalation never fired: a real ledger's 3,058-char row (7.6x budget) passed the gate clean.check_rows's new parameter defaults to 4x soft, so existing 2-arg call sites keep working.verifydetail-pointercheck (the v2.6.0 entry, now actually implemented). Flags a row whose cell claimsโ detail block **<ID>**with no matching bullet under any### Detail -heading (warn). A Detail bullet with no pointing row is reported as informational advisory text, NOT counted towardwarn_count, per that entry's own spec./unforget list --freshโ the display-preference interview. Sets the saved default for--view/--group-by/--section/verbosity so plainlist/scancalls apply it silently. A depth gate fires first (Quick = 1 question ยท Standard = 4 ยท Thorough = +thresholds), so the interview's length is the user's choice rather than a fixed toll.--freshALWAYS re-interviews โ a deliberate exception to this skill's don't-nag discipline, since being asked again is the entire point of typing it. Every question past the gate carries an explicit "Keep current" option; a skipped answer is omitted from the write, never written as null (a null would CLEAR the prior value โ the opposite of skipping). Newscripts/display_prefs.py(resolve/framing/build-patch) owns the mechanics; question wording and judgment stay with the LLM, the same splitdefer_tally.pydraws. Precedence is fixed: explicit flag > saved preference > hardcoded default, with asourcesmap naming which layer produced each value. A flag passed alongside--freshrenders but is never saved, so a one-off filter cannot silently become a permanent default.- 5 tunables migrated into the registry (
archive_nudge_threshold,stale_days_this/next/ later/someday). They were specified as living in "a config block at the top of UNFORGET.md" โ but no reader for them was ever implemented (the onlyunforget-config:marker any script parses ismemory-dir), and no ledger was found carrying them, so the migration moved no data and broke no code path. A legacy in-file block is still honored, registry winning on conflict. --versionreconciles its own version declarations โ all FIVE of them. SKILL.md frontmatter,.claude-plugin/plugin.json, the newest changelog heading, the README's shields.io badge cache-buster, and the README's**Maturity:**bullet are now compared;--versionreportsversions_in_sync+declared_versions. The manifest had been stale at 2.1.0 while everything else read 2.6.0. Only sources that actually declare a version vote (a manual install with no manifest is not drift; an unparseable manifest or a README lacking both markers degrades rather than crashing), and drift is advisory, exit 0 โ it misreports what is installed but, unlike a missing companion file, does not break the router. The README patterns are anchored to the badge URL and the literal**Maturity:**bullet, so prose mentioningv1.0.3or "upgrading from 2.4.0" is not mistaken for a declaration. โ ๏ธ The first cut of this check covered only three sites; bumping to 2.7.0 still needed a hand-grep of the README to catch the other two โ a check that covers most declaration sites leaves the release manual, which is the failure it was built to prevent.
โ ๏ธ Known remaining gap: the version check compares strings across declaration sites; it
cannot catch a changelog entry that describes behavior the code does not implement. That was
the shape of the two verify gaps above, and it needs semantic comparison rather than string
equality. Unautomated on purpose โ noted here rather than left implicit.
v2.6.0 โ verify: catch a detail-block pointer that leads nowhere (2026-08-13) ยท minor
New check, warn severity, no new flag. Additive: existing verify behavior for every other
check is unchanged.
detail-pointerflags a row whose Finding or Status cell containsdetail block **<ID>**(either the canonicalโ see detail blockphrasingrow_budget.pywrites, or the shorterโ detail blockform) when no matching- **<ID>** -bullet exists under any### Detail - <section>heading in the file. The inverse (a bullet with no pointer) is reported informationally, not counted as a finding โ usually just a row written with full detail from the start.- Origin: found on the same real row this whole v2.2-2.5 arc traces back to. Stuffolio's
A65 row read
โ detail block **A65**, but no**A65**bullet existed anywhere in the file's Detail sections โ the row's entire 3,707-character history was still sitting in the table cell, the exact shape the char-budget check exists to catch, except the pointer made the row LOOK already-split when it wasn't. Nothing before this checked that a stated pointer actually resolves. - Now load-bearing for
/unforget show, not just cosmetic.show's Fix field reads the LAST dated entry in a row's Detail bullet as its source of truth (v2.5.0, above). A dangling pointer meansshowsilently degrades to "no detail history on file," quietly losing the richest part of its own output with no signal that anything was missing. This check is what makes that failure visible instead of silent. warn, noterrorโ deliberately, unlike char-budget's hard-threshold escalation. A dangling pointer doesn't itself prove the row's rating columns or@statustoken are wrong; gatingarchive/promoteon it would need real field data first, the same caution that kept the char-budget hard threshold at 4x rather than 1x.- No auto-fix. Unlike char-budget overflow (which
verify --fixsplits fresh), a dangling pointer's right remedy depends on WHY it's dangling โ deleted content, a moved section, a typo'd ID โ which a mechanical fixer can't distinguish.verifyreports it; a human decides. - Full spec:
reference/verify.mdยง Thedetail-pointercheck (ยง4f).
v2.5.0 โ /unforget show: one-row synthesis instead of the full history dump (2026-08-13) ยท minor
New subcommand. Read-only, additive, no change to on-disk format or any existing command.
/unforget show <ID>renders three fields for ONE row: Finding (current-state, not history), Impact (why it matters left as-is), Fix (what closes it, or the specific verification step still owed for adone-unverifiedrow). Deterministic extraction, not a per-call model summary: Finding/Impact come from the row's own table cells; Fix comes from the LAST dated entry in the Detail block, which the row-length discipline's ยง2b append rule already guarantees is the current state (history is appended, the cell's status is REPLACED to latest) โshowleans on that existing invariant rather than adding new logic to find "what's current." No caching; recomputed fresh every call, same anti-staleness principle as the char-budget and view-mode work above.--fullprints the synthesis, then the complete raw Detail-block history verbatim below it โ the escape hatch for when the full accreted narrative (a reversal, a postmortem) is actually wanted. Nothing is ever hidden from the file, only from the DEFAULT view; same non-negotiable as the row-length split's "moves history, never deletes it" rule, applied one level up.- Markdown is the baseline everywhere, per this skill's own stated portability goal ("works
fine in any editor, on GitHub, in Linear... other AI assistants" โ ยง Compatibility notes).
Nothing about the default
showoutput depends on any rendering capability beyond stdout. Carries its own Algorithm fallback (reference/commands.mdยง/unforget showยง Algorithm fallback), same as every other non-trivial command in this file โ table/string extraction only, no ranking or cross-row logic, so the fallback is a close mirror of the preferred path rather than a simplified approximation. - Interactive card presentation is optional and environment-gated, never a silent swap.
Where a richer surface exists (e.g. Claude's Artifact/widget rendering),
showmay OFFER a click-through card view of the same three fields โ explicitly opt-in, generated from the same deterministic extraction (no separate logic, no separate drift risk), and it degrades to nothing (not to an error) anywhere that capability is absent. - Deliberately does not touch
list's rating table. Comparing rows (Urgency/ROI/Risk across many) and reading one row deeply are different tasks โ the interactive view carries no rating columns and is not meant to answer "what's next" (that stayslist/--view=next's job). Extending this pattern to a multi-row interactivelistis a real design question flagged as explicitly out of scope for this version, not assumed as a natural follow-on. - Origin: a live demo built from Stuffolio's own open rows, prompted by "these summaries take up a lot of vertical space โ what if selecting a row is when a brief description displays, instead of the list itself." The follow-up question ("would we lose the 11-column table?") is what drew the comparison-vs-reading distinction this design rests on: no, because they were never the same job.
- Full spec:
reference/commands.mdยง/unforget show(including ยง Interactive presentation).
v2.4.0 โ list --ledgers= / --all-ledgers: opt-in cross-ledger reads (2026-08-13) ยท minor
Additive, backward compatible: /unforget list with no scope flag is single-ledger, unchanged.
--ledgers=<names>/--all-ledgersunion rows from sibling ledgers already declared in the registry (role/axis/parent/deathperreference/registry.md) โ no new registry field, and no globbing for stray*UNFORGET*.mdfiles. A name not present in the registry is an error, not a silent skip.- Default stays opt-in, by design decision, not just default caution. The alternative
(auto-union every discovered ledger file, narrow with a flag) was considered and rejected: it
would make
branch's side effect silently change tomorrow'slistoutput with no flag touched, and it would surface files the registry exists specifically to avoid losing track of or confusing with real ledgers. - Three safety levels, not one blanket "combine": reading (
--view=all/open/done/split) is a safe, unconditional union โ output gains a Ledger column whenever more than one ledger is in scope. Ranking (--view=next --all-ledgers) is axis-aware: anaxis:actorsibling (a different human's work, e.g. a project's TERRY-only ledger) or anaxis:lifespansibling (has adeathcondition โ meant to disappear) is never presented as an undifferentiated top pick; the source ledger is always named, and an actor-scoped top result gets an explicit best-non-actor-scoped alternative alongside it. Writes (archive/edit/promote) are entirely out of scope for these flags โ they keep operating on the one ledger they're pointed at, same as today. - Origin: a follow-up to the
--view=/--group-by=work above, prompted by "would a user choose or combine which ledgers to work from?" The axis-aware ranking rule specifically guards against the failure a blind cross-ledger--view=nextwould invite: surfacing a row scoped to a different actor or a dying sprint ledger as if it were a permanent, generally actionable "next," which would misrepresent exactly the separationbranch's three axes (reference/branching.mdยง2) were designed to preserve. - Algorithm fallback for
--ledgers=/--all-ledgersis covered in the SAME fallback paragraph as--view=/--group-by=(see v2.2.0 entry below) โ one combined recipe for all threelistextensions, not a separate one per flag. - Full spec:
reference/commands.mdยง Multi-ledger scope (under/unforget list).
v2.3.0 โ char-budget hard error + write-time budget offer at edit (2026-08-13) ยท minor
char-budgetescalates toerrorpast a hard threshold (default 4x the soft budget, 1600 chars; new--char-budget-hardflag). Previouslychar-budgetwaswarnat every size, so a row could sit at any length indefinitely without ever blockingarchive/promote. Origin: a real ledger (Stuffolio, 2026-08-13) carried a row at 3,707 chars โ 9x the 400 soft budget โ through repeated ship cycles; its accreted "RESOLVED" / "still owed" / "prior arc" history (never migrated to the detail block that already existed for it) directly caused a session to misread the row's current status. The lossless split (verify --fix,scripts/row_budget.py) already existed and already worked; what was missing was a severity that made using it mandatory before shipping, not optional. Applies to v1 (tokenless) ledgers too โ the failure this catches doesn't depend on@statustokens being present. Full spec:reference/verify.mdยง Char-budget severity escalation./unforget editnow offers the split at write time, not just reactively at the nextscan/verifyโ checked once per status-changing edit, right after the change is applied, only when the edit CROSSES the soft-budget threshold (not re-offered on every subsequent edit to a row already over budget). Advisory, same shape as the existing companion-skill handoff: easy to decline, never blocks the edit itself. Catches the bloat where it's actually created (one status change at a time) instead of only where it's later discovered. Full spec:reference/commands.mdยง Budget check at write time (under/unforget edit).
v2.2.0 โ list --view= / --group-by=: named row-selection modes, orthogonal grouping (2026-08-13) ยท minor
Additive, backward compatible: new opt-in flags, no change to the default list output or the
on-disk file format.
--view=<all|open|done|split|next>picks which rows show.allis today's unchanged default (one table, everything).open/doneare named equivalents of filtering to just the Open or Completed bucket โ the common-case spelling for "what's left" / "what shipped."splitrenders both as two headed tables in one output (with counts, so the reader doesn't count rows by hand).nextskips the table entirely and returns one recommended row plus a one-line reason, ranked by a composite of ship-risk (Target ร Urgency ร Risk:No-Fix), closest-to-done (adone-unverifiedrow needing only a verification step outranks one needing new code, all else equal), and ROI โ the dominant factor is named in the reason so the pick is inspectable, not a black box, and ties break toward lower Fix Effort.--group-by=<target|section|none>is the orthogonal axis: controls how the rows--viewselected are grouped/sorted, never which rows are included.target(default, unchanged) is today's ๐ด THIS โ ๐ต NEXT โ ๐ก LATER โ โช SOMEDAY grouping.sectiongroups by Paused Plans / Session Spillover / Audit Findings / User-Reported instead โ combined with--view=splitthis produces one Open/Completed pair per section.noneis a flat Urgency-sorted list for piping elsewhere.- Status classification (all
--viewmodes) is viaparse_status.py's existingarchivablefield โopen/in-progress/blocked/done-unverifiedcount as Open,done-verified/withdrawncount as Completed.done-unverifiedstaying in Open, not Completed, is the load-bearing rule across every mode โ code-written-but-not-proven is still open work by this skill's own status tiering, and--view=nextexplicitly flags when the top pick is a verification step rather than new code so it doesn't read as "start from scratch." Legacy tokenless rows use the same loose word-status mapping--statusalready applies; unclassifiable rows land in an Unparsed heading (splitmode) rather than being silently dropped into either bucket. - Origin: a live 61-row ledger, read start-to-finish by an agent asked for "what's open,"
under-reported by 18 rows on the first pass, then separately misread a row's current status
from its own history narration (a row that had gone open โ fixed โ regressed โ fixed again
read as still-open from the prose alone, even though its token was
done-verified). Re-deriving "open vs. done" by eye from one merged, sorted-by-Target table is exactly the failure mode@statustokens exist to prevent (seereference/status.md). The two-axis design (rather than a single--splitflag, the first cut of this feature) came from a follow-up ask: separate "which rows" from "how grouped" so open-only, done-only, combined, and a future grouping request don't each need their own bespoke flag. - Composes with existing filters (
--target=,--section=,--stale,--age=); a--view=<mode>combined with a bucket-picking--status=<value>is redundant, so--status=wins if both are passed. - Carries an Algorithm fallback (
reference/commands.mdยง/unforget listยง Algorithm fallback) โ every existing command spec in this file has one and these flags initially didn't; added so a Python-unavailable environment (or a human without this skill loaded) has a written recipe for--view/--group-by's logic, not just the base filterslistalready covered. - Storage untouched by design. UNFORGET.md stays one file, one table per section โ see
reference/commands.mdยง View modes for why splitting the file itself would work against this skill's "single source of truth" premise. - Full spec:
reference/commands.mdยง View modes and ยง Grouping (under/unforget list).
v2.1.0 โ quoted status tokens no longer hijack a row's status (2026-08-11) ยท minor
Bug fix, backward compatible, but a behavior change in the parser โ hence minor, not patch.
parse_status.status_cellnow scans last-cell-BACKWARD. It scanned first-forward for the first cell carrying an@status:token, and Finding precedes Status. A row that quoted a token illustratively โ rows documenting the format do this, and so does any row citing a sibling row's state โ had the QUOTED token silently become its status forlist,archive, and the release gate. Found on a live ledger 2026-08-11: anopenrow citing a closed sibling parsed asdone-verifiedand failed the gate. Backward scanning returns the real Status cell in every layout the format allows, including with the optional1-Star Riskcolumn appended (it carries no token). Regression-tested both directions.- New
quoted-status-tokenwarning (warn, not error). The parser fix keeps the tool correct, but a quoted token still corrupts thegrep -creading that ledger docs commonly prescribe for humans. Fires at write time and names the offending token. - Contradiction messages now point at the quote. When a row both contradicts and quotes a token, the bare "token says X but narration says Y" sent authors to edit their prose โ the innocent half. It now names the quote as the likely cause.
FILE_CITE_REno longer matches ordinary prose.[\w./-]+\.\w{1,5}countede.g,i.e, and decimals like0.50as file citations, inflatingstale-recipewarnings and training users to ignore the check. Now requires a path separator or a known source/doc extension. Measured on a 3-ledger installation: 33 โ 27 warnings on the worst file.
v2.0.3 โ contradiction false positives (2026-07-31) ยท patch
Bug fix only, backward compatible. The ยง1b contradiction check matched its phrase list as bare substrings, which fired on ordinary prose. Three classes found in the field:
"still open"matched a VERB phrase. "viewers can still open + view detail" โ a sentence about a UI affordance โ was read as "this row is still open," contradicting its owndonetoken. Now distinguishes the adjective (clause-final: "the issue is still open") from the verb (takes an object or conjunction: "still open the sheet", "still open + view"). Only the adjective contradicts."blocker"matched its own negation. "not a blocker" was read as "is a blocker." Negation-aware now (not/never/no longer/isn't/wasn'twithin two words)."unverified"matched the row's own@status:done-unverifiedtoken. The narration is stripped of@status:/@verified:tokens before scanning, so a token can no longer be read as prose about itself.
Matching is also word-bounded now, so a phrase inside a longer word no longer fires.
Why this mattered. A false contradiction sets archivable to False, so the row is held
out of archive indefinitely while a human is sent to reconcile a real sentence against a
conflict that never existed. On the source installation it produced a phantom 5th error over
a ledger whose true error count was 4.
8 regression cases added to tests/test_row_visibility.py (4 false-positive shapes, 4 real
contradictions that must still fire), verified to fail against the old matcher.
v2.0.2 โ release-gate false negative (2026-07-31) ยท patch
Bug fixes only, backward compatible. A first-ever verify run against a mature
three-ledger installation found that the row-id pattern was matching too little, and that
rows it missed were invisible to every check in the lint:
-
ROW_ID_REaccepted at most one leading letter and no suffix. Real ids skipped in the field:A48a/A48b(a finding split into sub-rows),MI-08(a hyphen-prefixed sibling ledger),**S12**(bold-wrapped). Consequence: two ๐ด THIS ship-blockers were excluded from the release gate, which reported 2 blockers over a ledger holding 4 โ and reported it as a clean number. An entire sibling ledger reportedrows_checked: 0while appearing healthy. Widened to an optional 1-3 letter prefix (optional hyphen), digits, optional letter suffix, optional bold. Strictly wider: every previously-matching row still matches, bare-numeric ids still work, headers and separators still correctly do not. -
New check 10,
cell-count. Flags a row whose cell count differs from its table's declared header width. The cause is nearly always an unescaped|in cell prose (agrep 'a\|b'recipe, a regex alternation), which silently shifts every positional column read past it โ a status token can land in a rating cell. Error severity. Width is tracked per-table, so a 10-column section and a 5-column sprint table coexist without false positives. -
Regression bench.
tests/test_row_visibility.py(23 assertions) covers the id grammar positively and negatively plus the cell-count check, builds its own fixture, and is wired intotests/run.sh. The shared fixture project exercised none of these id shapes, which is precisely why the bug survived to production.
v2.0.1 โ column-layout robustness (2026-07-26) ยท patch
Bug fixes only, no new features, backward compatible. A refreshed example that finally exercised the format's own optional/variable columns surfaced a family of positional table-cell reads that broke when the column layout wasn't the Standard 10:
- Status was read by position, not content.
parse_status.status_cell(and its copies inverify_ledger/row_budget) took the last table cell as Status โ so an appended1-Star Riskcolumn made the risk strip get read as the status, silently breakinglist/archive/verify(zero tokens found). Now the Status cell is located by the cell carrying the@statustoken; the three copies are consolidated into one. - Finding was read as a fixed index.
finding_cellusedcells[2], which is Urgency under the Compact preset (that preset drops the Target column). Now a single preset-aware locator (detects the Compact**๐ด THIS ยท โฆ**badge) that both call sites delegate to.target_is_thishardened the same way. - The
verify --fix/row_budget splitpath (build_index_row) read Finding/Status by fixed index; now by content, so a split of a 1-Star-column row preserves the risk strip and bounds the real Status. branchwrote a fixed 10-column pointer row. Nowbuild_pointer_rowderives the column set from the parent's actual header and places content by column name, so a Lean/Compact/Continuous/1-Star parent gets a correctly-shaped pointer row.
All found via /bug-echo on the first fix; each fix reproduced before and after, with
regression guards added to the test suite (proven to fail if the fix regresses). The
refreshed examples/UNFORGET.md now shows format v2 (real @status tokens, a
done-unverified owed row, a lossless split) plus the optional 1-Star Risk column.
v2.0.0 โ the format-v2 milestone (2026-07-26) ยท the eight-phase design build, complete
A milestone, NOT a breaking change. The major bump marks scope, not incompatibility: every v1
ledger keeps working untouched, no migration is forced, and the skill reads and writes both v1 and
v2. What earns the 2.0.0 is that this is a categorically more capable tool than v1.0 โ eight
phases (shipped incrementally as v1.1.0 through v1.6.0, now tagged together as v2.0.0) added the
whole format-v2 layer: structured @status/@verified
tokens (a row can't contradict itself; a "done" isn't done until it's verified), a registry
(where every ledger lives + git posture + policies), the verify integrity lint (gates
archive/promote), the deferral gate (trivial tripwire + why-not-now + session accounting), the
branch command (atomic child ledgers), onboarding wiring (a maintained CLAUDE.md recall
block + drift reconciliation), row-length discipline (bounded index rows + lossless splits), and
companion skill handoffs (below). Backward compatible throughout; a v1 (tokenless) ledger is
never blocked by any v2 check.
The final phase, companion skill handoffs: unforget recommends OTHER skills at earned ledger transitions โ function-based, not skill+URL hardcoded through trigger points, so a companion link rots in ONE place (the manifest), never twelve.
- Five fixed functions (
reference/skill-handoffs.md):post-fix-sibling-scan,ship-risk-scoring,audit-reverify,forward-bug-hunt,verify-against-reality. Each fires at a specific ledger transition and names the earned reason โ never a generic "you might like these skills" footer. - One global manifest (
~/.claude/unforget-companions.md,scripts/companions.py): function โ skill โ invoke โ url, the ONLY place a companion URL is written. Projects inherit it. Ships a default mapping the author's skills, disclosed at init (overridable in one place; unforget works with no manifest at all). - Install-state detection by INVOCABLE NAME, never a dir find (the one-star-risk lesson โ
one-star-riskis invocable but has no dir of that name). Three states: installed โ run the command, no URL; not-installed โ one soft pointer with the manifest URL; unset โ say so, invent no URL.verifygains a rot check for entries neither installed nor reachable. - Governance: at most once/function/session; a trivial close fires nothing; advisory, never blocking, and never a way to defer the scan (a handoff means do-it-now-while-context-is-hot).
- Reconciled the pre-existing inline
/radar-suite+/bug-echoclosure block (which hardcoded two URLs and detected installs by directory name) into this function/manifest system acrossedit,promote,deferral-gate, andverify.
With Phase 8 the v1.1 design build is complete โ all eight phases (status tokens, registry, verify lint, deferral gate, branching, onboarding, row-length, companion handoffs) shipped. Backward compatible throughout: every feature degrades cleanly on a v1 ledger.
v1.5.0 โ row-length discipline (2026-07-26) ยท format v2
Phase 7 of the v1.1 design build: the row-length rule that keeps a ledger Readable. A row is a one-line INDEX; history/context/verification narration belongs in a detail block, not fused into an ever-growing Finding or Status cell. The 2026-07-25 failure was a ~155KB ledger with multi-KB rows whose Reads truncated and misled the reader โ a bounded index prevents exactly that.
- The two-part row (
reference/format.mdยง Row-length discipline). The table row carries a compact index (a one-line finding summary + the@status/@verifiedtokens + a one-line status); the unbounded content lives in a### Detail - <section>bullet. History is appended to the block, never grown in the cell. scripts/row_budget.py.checkflags Finding/Status cells over the budget (default 400, registry-configurable viarow_char_budget).splitturns an over-budget row into a bounded index + a detail-block bullet holding the full original content verbatim โ and returnslossless:trueonly when every character is provably preserved, refusing otherwise. The hard rule: the budget MOVES history to the block, it NEVER deletes it.- Wired:
scangains the char-budget lint;verify --fixoffers the split for char-budget findings only, per row, with approval (the one integrity finding safe to auto-resolve because it's mechanical and lossless-verifiable).verifywith no flags stays read-only exactly as before.
Backward compatible: the rule flags legacy over-long rows but never blocks on them; a split is always offered, never forced, and only ever moves content โ a legacy ledger keeps working untouched.
v1.4.0 โ onboarding, registry wiring, and the maintained recall block (2026-07-26) ยท format v2
Phase 6 of the v1.1 design build: init/import now write and reconcile the two persisted
surfaces the whole system depends on โ the registry and the maintained recall block โ so
nothing the skill relies on lives only in memory (the through-line of the onboarding design). This
is the fix for the 2026-07-25 split-brain (ledgers stranded in a parallel tree) and stale-pointer
(a CLAUDE.md index that described an old layout) failures.
- Onboarding questions (
reference/init.md).initadds the git-posture question (split / committed / ignored โ split recommended, and the skill writes the.gitignorerules itself, incl. ignoring the ephemeral.unforget-session.jsonand.unforget.jsoncache), and upgrades the recall question to maintained / manual / none. It writes the registry + the two policy defaults (Policy 1 deferral, Policy 2 multi-axis) at the end. - The maintained recall block (
reference/init.md,scripts/recall_block.py). A marker-delimited Deferred Work Index in CLAUDE.md/AGENTS.md, rebuilt from the registry by init/import/branch so it can't rot โ rewriting only between its markers, never the user's content.branchnow updates it as a fourth atomic artifact (rolls back with the other three on any write failure). importdrift detection (scripts/import_drift.py). Reconciles the registry against reality โ registered-but-missing (error), found-but-unregistered (the stranded-parallel-tree check), posture-mismatch, and stale-recall. Read-only; reports, you fix.- Migration for already-messy projects (
reference/init.mdยง Phase 6b,reference/surfaces.mdยง non-standard locations).initASKS for out-of-repo ledger locations (rather than a disk-wide scan), proposes consolidation, and verifies byte-identical before removing any original โ the one-way-door discipline for not losing a ledger during a move.
Backward compatible: all of it is v2; a v1 ledger keeps working, and a project with no registry just gets the pre-v2 behavior (branch stays reachable via the parent pointer, no recall maintenance).
v1.3.0 โ branching + the branch command (2026-07-26) ยท format v2
Phase 5 of the v1.1 design build: the branching model and an atomic /unforget branch
command. The default is still NOT to branch โ most deferred work is a row or a section. A new
ledger is justified only when work differs from the parent on one of three axes.
- The three axes (
reference/branching.mdยง2): actor (a different human acts on it โ earns a file even at identical discipline, that's what aTERRY-UNFORGETis; a machine/ automation actor does NOT โ that's a Target value or tag), lifespan (a sprint โ earns a ledger only when paired with a different discipline like a cap/eviction, not a plain time-box), and domain (a different repo/subject). Plus the decision cascade (ยง3) and the two placement policies (ยง2.5). - The atomic
branchcommand (ยง8,scripts/branch_create.py). Creating a child does three things together, or none โ scaffold the child header (axis, discipline, parent back-pointer, death condition if lifespan), write the parent's single pointer row (never a copy of child rows), and register the child. A failure on any one rolls the others back โ no half-branched state. That structural atomicity makes the 2026-07-25 split-brain (a child the parent/registry lost track of) impossible. Guards refuse rather than half-create: a duplicate name, a lifespan child with no death condition, or an unconfirmed non-human actor. - Auto-suggest on a repeated pattern (ยง6).
add/importoffer a branch โ never branch unilaterally โ only when the cascade lands on "new ledger" for โฅ2 related items, naming the pattern seen. One item never triggers it. This is how an emerging track gets noticed instead of silently accumulating.
Backward compatible: branch writes v2 children and reads the registry; on a project with no
registry, register the parent first. The recall block still points at the canonical index; a
child is reachable via the parent's pointer row (the marker-delimited recall-block writer that
would add a per-child pointer line is Phase 6).
v1.2.0 โ deferral gate (2026-07-26) ยท format v2
Phase 4 of the v1.1 design build: the deferral gate, which fires at /unforget add โ the
moment work is about to become a deferred row. It targets deferral-laundering: a row looks
identical whether it was deferred for a good reason or because deferring was frictionless and
self-flattering. The gate makes deferral cost something and leave an auditable record.
- Trivial tripwire (
reference/deferral-gate.mdยง2). A would-be row that is Fix Effort = Trivial AND Blast Radius = โช 1 file is redirected to do it now โ scope doesn't gate it (out-of-scope trivial โ do it and log a one-line report). A trivial-but-destructive change (deletion, force-push, prod deploy) is the exception: it routes to needs-approval, never auto-done. Trivial โ safe. - "Why not now?" allow-list (ยง3). Everything that clears the tripwire must name one of four
deferral reasons โ
user-decision,scaffolding,scope,external-blockโ recorded in the row asDeferred because: <tag>so a later reader (orscan/verify) can check whether it held up. No valid reason โ do-now is the default, not a row. - Session defer/fix accounting (ยง4, the load-bearing backstop). A per-session tally surfaces on
listand at session end โ2 fixed inline ยท 7 deferred (reasons: โฆ). A defer-heavy ratio (default โฅ 3ร fixed) raises a gentle, advisory-never-blocking flag. The linguistic gate can be gamed per row; a ratio can't โ this half is why the gate is honest, not decorative. scantrivial-staleness cross-check (ยง4d). A Trivial row that has survived โฅN sessions un-done is flagged as a near-certain "should've just done it" โ how the p
*Truncated - read the full file at https://github.com/Terryc21/unforget/blob/8aba343f6b48b495f14095bd1b12b6e1