Imported from hamr0/liteagents (
packages/ampcode/skills/remember/SKILL.md). Install upstream withnpx skills add hamr0/liteagents --skill remember. Copyright stays with the author.
Run friction analysis, then consolidate session stashes + friction antigens into a single project-local MEMORY.md, and inject into AGENT.md. Friction runs automatically (best-effort) — there is no separate /friction command. A docs reconcile check runs at the end, detect-only.
Guardrails
- Favor straightforward, minimal implementations first and add complexity only when requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Precision over recall for hot memory. A false antigen loaded into
@MEMORY.mdsteers every future session. When unsure, do not promote — leave it to recurrence (a ledgerobservingentry at 2 sessions, nothing at 1). - Mid-tier model, not hardcoded. Steps 2/3/4a delegate to your tool's balanced default tier — judgment-capable, cheaper and faster than your top reasoning tier. Not the cheapest/fastest tier: on judgment work it measurably degrades (misclassification rates several times higher). Never hardcode a vendor-specific model name.
- Batch stashes, don't fan out. Step 2 gives each extraction agent up to 5 stashes and uses as few agents as possible (3 stashes → 1 agent, 7 → 2). One agent reading several sessions sees the same lesson recur and writes it once; one agent per stash writes it once per stash and leaves the merge to catch the duplicates. If more than one agent is needed, run them concurrently.
- Hot memory is short snippets, not prose. A fact is one line, target 160 characters, hard stop 180, stating a rule the agent should follow next time. Events, history, and narrative are not facts.
What it does
Reads all raw material (.amp/stash/*.md + .amp/remember/friction/antigen_clusters.json), extracts durable facts, episodes, and behavioral antigens into a single .amp/remember/MEMORY.md, then injects a managed memory section into AGENT.md.
Steps
-
Run friction first (best-effort — friction analyzes ALL your usage, not just this repo)
Friction's signal is global: recurring corrections and frustrations across every project are behavioral lessons worth keeping everywhere. So point it at the tool's global sessions root (all projects), not a per-project directory.
- Locate
friction.cjs— it is bundled next to this command atremember/friction.cjs(the same directory asremember.md, whether installed or run from the package). If it exists nowhere, skip to step 1 (stash-only) and tell the user friction.cjs is missing. - Check for a newer liteagents (best-effort, one line, never blocking) — bundled
beside
friction.cjsasremember/version-check.cjs. Call it by its absolute path, exactly as step 7 callsdocs-builder.cjs: the cwd here is the target repo, not this package, so a cwd-relative path fails everywhere except the liteagents repo itself.
If that path does not exist, use the directory you just resolved fornode ~/.config/amp/skills/remember/version-check.cjsfriction.cjs— the two ship side by side, so that directory is correct for a non-default install and when running from a checkout, where the path above would point at the installed copy instead of the one under test. It prints one advice line if the installed version is behind the registry, and prints nothing otherwise. It exits 0 on every path, caches the registry answer for 24h, and is bounded to ~2s, so it cannot stall this run. If it prints a line, relay it verbatim in your final report; never act on it and never run the install yourself. - If the script is missing from both locations, say so — one line, same rule as step 7's "applicable but could not run". A failed check (offline, registry down, timeout) stays silent by design: it is a once-a-day nudge, not a result anyone is waiting on. A missing script means the install is incomplete, which is worth a word.
- Resolve the global sessions root — probe this list top-to-bottom, use the first that
exists and contains
.jsonlfiles directly, or one level down in per-project subdirectories (friction.cjs scans exactly those two levels, not a deep recursive walk). Never prompt the user.# ── Add your own global sessions root at the TOP so it is checked first ── ~/.claude/projects/ # Claude Code ~/.factory/projects/ # Droid / Factory ~/.config/amp/projects/ # Amp ~/.config/opencode/projects/ # opencode ~/.codex/sessions/ # Codex CLI (use $CODEX_HOME/sessions/ if set) ~/.gemini/antigravity-cli/brain/ # AntigravityNote:
friction.cjsparses Claude Code's session schema. The Codex/Antigravity roots will resolve but yield no signals until friction learns their formats — open an issue to request one: https://github.com/hamr0/liteagents/issues - Run
node <friction.cjs> "<resolved-root>". friction writes its output to.amp/remember/friction/in the current project. - On any miss — loud, never silent. If no root resolves, or friction errors, or it
finds no usable sessions, print this and continue with stash-only consolidation:
⚠️ Friction didn't run — no sessions found. To enable it, open this command file (
remember.md) and add your tool's global sessions root to the TOP of the probe list in step 0, then re-run/remember. Consolidating stashes only this time.
- Locate
-
Gather sources
-
Legacy layout migration (one-time, loud). Older versions used
.amp/memory/and.amp/friction/. If either exists: move the pipeline files.amp/memory/{MEMORY.md,ledger.json,.processed}→.amp/remember/, and discard the old.amp/friction/contents entirely — friction regenerates all of its output fresh every run (step 0 has already rebuilt it in.amp/remember/friction/by the time migration runs; stale copies carry no unique information and moving them would overwrite fresh output). Move only those pipeline files — anything else in.amp/memory/(e.g. user-owned rule files) stays where it is. Remove the old dirs only if empty, update the managed MEMORY section in AGENT.md to the new reference (step 5), and tell the user exactly what moved. -
Sync
AGENT_RULES.mdfrom the installed template — run the bundled script, which does the whole decision itself. Call it by absolute path, for the same reason asversion-check.cjsin step 0: the cwd is the target repo, not this package.node ~/.config/amp/skills/remember/sync-rules.cjsIt compares
.amp/remember/AGENT_RULES.mdagainst the template shipped beside it and takes one of three actions: absent — copies it in; identical — does nothing at all, no write and no output; differs — moves the old body toAGENT_RULES.md.bakand copies the new one in, reporting both. Relay whatever it prints in the step-8 report; it is silent when nothing changed.This replaced a bootstrap-once rule that never refreshed, which left a measured 35 repos many releases behind. The rules doc is a shipped standards document, so it is kept current rather than frozen on first write — nothing is destroyed, because a differing body is always preserved in the backup first.
The comparison is a byte compare done by the script, never by you: a model-performed copy can re-wrap a line or drop a trailing newline, and the file would then differ forever, backing up on every single run.
-
Read all
.amp/stash/*.mdfiles in the current project -
Read friction output written in step 0:
.amp/remember/friction/antigen_clusters.json(preferred) or.amp/remember/friction/antigen_review.md(fallback). On the fallback path, step 4c does NO counting — merge quotes into matching entries only; never changesessions,last_seen, orrecurred_while_hot(the fallback carries nosession_ids, so identity matching cannot run on it). -
Read existing
.amp/remember/MEMORY.mdif it exists — create dir if missing -
Read processed manifest at
.amp/remember/.processed— skip already-processed stashes -
No unprocessed stashes → skip steps 2-3 (extraction and the Facts rewrite) entirely — facts are never rewritten with zero new input, not even to clear existing length-gate debt on
.amp/remember/MEMORY.md. Steps 4-5 (friction → ledger count → Antigens render) are stash-independent and still run whenever friction produced output (see step 4's own guard). If there is also no friction output, report "nothing to consolidate" and stop after step 1 — but run step 5'sstub-check.cjsbefore you stop. The stub shape does not depend on there being anything to consolidate, and skipping it on quiet runs is exactly how a repo with nothing to remember stays broken forever.sync-rules.cjsalready ran above, for the same reason.
-
-
Extract from unprocessed stashes (up to 5 stashes per agent, as few agents as possible — see Guardrails)
- Each agent reads its batch of stashes together and calls the mid-tier model (see Guardrails) to extract:
- FACTS (one line each, target 160 chars, hard stop 180): stable preferences, decisions, corrections, explicit "remember this". A fact is a rule that changes future behaviour, written as the current truth — not an event that happened, not its history. A lesson that recurs across the batch is written once.
- EPISODE (one per stash, 3-5 bullets): what was the goal, what was tried, outcome, lesson
- SKIP: code details, file paths, errors, mechanical steps, LLM responses
- Collect all new facts and episodes
- Each agent reads its batch of stashes together and calls the mid-tier model (see Guardrails) to extract:
-
Merge into MEMORY.md
- Read existing
.amp/remember/MEMORY.mdand parse its sections (## Facts, ## Episodes, ## Antigens) - Facts section — rewrite and compress, every run. Call the mid-tier model with the
existing facts + the new facts + the lessons of any episodes aging out (below), and have
it return the whole section rewritten, not the old list with lines added:
- New replaces old; contradictions keep the new version; duplicates fold into one line.
- Shorten every fact that can be shorter. Target 160 chars, one line, current truth only — no "supersedes", no version history, no narrative. The output should normally be shorter than the input.
- Facts are never append-only: an old fact that a new one refines is rewritten in place.
- Pre-write length gate — runs BEFORE MEMORY.md is written, not after. A check that
only runs after the write (step 8) can merely describe damage already on disk; the gate
has to sit inside the merge, before anything hits the file. This applies to every
line in the draft Facts section, including lines carried over unchanged from the
previous MEMORY.md — the whole section is rewritten every run (see "Shorten every fact
that can be shorter" above), so every line is this run's output. "Not introduced this
run" is not a reason to skip a line. After producing the draft, check every line's
length: 161-180 chars passes silently. Over 180 MUST be shortened and re-checked. The
ONLY exemption is mechanical: a line whose single longest backtick-quoted literal is
itself longer than 100 characters (a path, command, or exact phrasing that genuinely
cannot be split) — that line is kept verbatim and listed as an exemption in the step-8
report. No other reason exempts a line — not established formatting, not dense by
convention, not pre-existing, not load-bearing detail. A line that's long because it holds
several sentences is shortened by splitting it into several facts or dropping the
history — never exempted. Only a draft that passes the gate (or has its overruns
exempted under the 100-char backtick rule) is written to
.amp/remember/MEMORY.md.
- Episodes section: append new episode entries, keep only the 10 most recent. Dedup before appending: if a new episode covers the same work as one already in the section (same goal or same session's work under different wording — judge by content, not title), merge the new detail into the existing entry instead of appending a second copy. Re-processing a stash whose episode is already filed must not create a near-duplicate pair. Every older episode is folded, then deleted: its lesson becomes a fact (handed to the rewrite above); the narrative is removed. No archive — git has the history. Specify the operation once. The keep-10 rule is the rule; the set to remove is derived from it, never supplied alongside it as a second list. Given both, an agent applies both and removes their union — observed in the field: a run told to keep 10 and handed a 5-entry delete list removed 7, and the 2 extras were never folded, so one lesson left memory with nothing carrying it. No episode is removed whose lesson has not been folded into a fact first, and the two sets must match: state the count before, the count after, and name each episode removed. Removed-but-not-folded is a defect to report, not a tidy-up.
- Antigens section: only update from friction output (step 4)
- Write merged result to
.amp/remember/MEMORY.mdin the format under step 5.
- Read existing
-
Distill friction into antigens (only if friction output exists)
Friction has already done the heavy part: it scanned the raw session logs, kept only observed user reactions (corrections, curses, repeated stops), pooled them per session, and lexically pre-grouped the obvious repeats. Each cluster carries
theme,suggested_artifact,confidence,severity,sessions(recurrence count),projects,signals,contexts(the verbatim user quotes),preceding(the agent action + result just before the reaction — the trigger), andself_suspect(friction's guess that the user was correcting themselves). You work from these short quotes — never re-read the session logs. Friction's lexical grouping and flags are hints, not the verdict.-
Read
.amp/remember/friction/antigen_clusters.json. -
4a. Classify (the LLM classifies only — no merging, no arithmetic; counting and rendering are mechanical, see 4c and step 5). Call the mid-tier model once per cluster batch with each cluster's
contexts,preceding,errors,self_suspect,projects,sessions,top_keywords, and the ledger's existing entries (id,class_hints,rule,evidence.quotes). For EACH cluster, output exactly one label — nothing else:drop— self-directed correction, agent's own prose captured as context, or a real reaction too short/ambiguous to name a specific mistake (self_suspectand an emptypreceding— no agent action — are strong self-directed cues). Don't force a match on one overlapping word.- an existing ledger id (
ag-NNN) — only if the cluster is narrowly the SAME mistake class as that entry'sclass_hints+rule+evidence.quotes, not just similar sentiment. State the entry's specific claim precisely in the prompt (a generic one-liner rule is not enough to bound the match — see 4c Open item 2) and give the classifier a negative example, not just the positive claim, e.g. for ag-001 (validate, don't assert): "did you test it?" matches; "we're burning money, why is it failing?" does NOT — cost/outcome complaints are not validation claims. new:<theme>— a real, agent-directed mistake matching no existing entry.<theme>is NOT freeform LLM prose: derive it mechanically from the cluster's owntop_keywords[0]andtop_keywords[1](lowercase, hyphen-joined). This alone raised measured 5-run exact-label agreement from 0.884 to ~0.97-0.99 by removing wording variance as a source of disagreement — the remaining variance is genuine classification disagreement (drop vs. new:, or which existing id), not paraphrase noise. Also output arule: one line stating the behavioral rule this cluster's evidence supports, same do/don't imperative style as an existing ledger entry'srule(e.g. "Never say work is validated... without an actual run behind it"). This is the only LLM-authored field here —<theme>naming stays mechanical.friction.cjs countrequires it whenever the cluster's ownsessions >= 2(it will create a ledger entry); below that it's unused.
Output is
{cluster_index: label}fordrop/ag-NNN; fornew:<theme>, output{cluster_index: {label: "new:<theme>", rule: "<one-line rule>"}}. -
4b. Route + tier by recurrence. For each cluster and each same-label group (the clusters 4a gave the same label) — its tier comes from the distinct- conversation count
friction.cjs count(4c) computes for it, the union of the group's hashes deduped against the ledger:suggested_artifact: antigen(recurring + severe) or a same-label group → an antigen (a "do/don't" behavioral rule), with its verbatim evidence quotes.suggested_artifact: fact(recurring + mild) → a Fact.suggested_artifact: episodethat did not land in a recurring group → not an Episode, and at 1 session not written anywhere. The Episodes section is stash-fed and capped at 10; friction's one-offs are cross-project and arrive by the dozen, so filing them there would flush the stash episodes. Nothing is lost: friction re-scans every session log on every run, so the cluster re-surfaces until it recurs — and at 2 sessions it gets its home, a ledgerobservingentry (4c), which step 5 renders under Low Confidence.suggested_artifactis friction's structural proposal, not a filing decision; the filing rule is this list.- Confidence by distinct-session recurrence:
- High (5+ sessions) → loaded hot via
@MEMORY.md - Medium (3-4 sessions) → recorded under Antigens, not loaded hot
- Low (<3 sessions) → ledger
observingonly at 2 sessions; nothing at 1
- High (5+ sessions) → loaded hot via
-
Recurrence tiers bind everything, including same-label groups: grouping consolidates evidence, it never elevates it — a same-label group's tier comes from its combined distinct-session count (e.g. a 2-session same-label group is still Low → ledger
observingonly, not an antigen entry in MEMORY.md). -
Never auto-promote. Only High-confidence (5+ sessions) antigens load hot. A single dramatic correction is recorded nowhere yet, not an antigen.
-
4c. Count (
.amp/remember/ledger.json, the evidence trail linking each rule to the mistake it targets and whether it is working — replaces the old evidence-merge arithmetic; a script now does every count, not the LLM). This is a literal command you run, not a description you reason from. The LLM's job ended at 4a — do not hand-compute session counts, do not decide by inspection which entries changed, even if you are confident you can do it correctly. Createledger.jsonas{"version": 1, "entries": []}if missing. It is JSON for exact matching — bookkeeping only, never injected into context as guidance (MEMORY.md is what gets read; the ledger is what gets checked).Entry shape:
{ "id": "ag-001", "class": "claimed-done-not-verified", "class_hints": ["says pushed but", "none got it"], "status": "observing|hot|rejected|escalated", "rule": "<current phrasing>", "attempts": [{ "n": 1, "rule": "<phrasing>", "adopted": "YYYY-MM-DD", "outcome": "active|failed" }], "evidence": { "sessions": 0, "session_ids": [{ "id": "<project-label>/<MMDD-HHMM>-<hash>", "seen": "YYYY-MM-DD" }], "projects": [], "quotes": [], "last_seen": "YYYY-MM-DD" }, "recurred_while_hot": 0, "history": [{ "date": "YYYY-MM-DD", "event": "<transition>" }] }Immediately after 4a produces
labels.json, run these exact commands as real shell invocations, in order. First:node <path-to-friction.cjs> migrate-attempts .amp/remember/ledger.json .amp/remember/ledger.jsonThis records any hand-drifted
ruletext as a new attempt so I7 (rule== the last attempt'srule) holds before counting runs; it is a no-op on an already-consistent ledger, so always run it regardless of whether drift is suspected. Then:node <path-to-friction.cjs> count <labels.json> <ledger.json> <clusters.json> <today's-date> <ledger.json>.new .amp/remember/friction/count_report.jsonThen overwrite
ledger.jsonwith<ledger.json>.new's contents (e.g.mv ledger.json.new ledger.json). Do not stop afterlabels.json— producing labels is 4a, not the deliverable of this step.friction.cjs count <labels.json> <ledger.json> [clusters.json] [runDate] [outLedgerPath] [reportPath]is deterministic, no LLM involved, so it can't drift between repos or runs; the count report is also written to.amp/remember/friction/count_report.jsonfor step 8 to read back. It implements, mechanically, everything the old prose reasoning here used to require by hand:- Session identity — the trailing 8-char hash of a session id (stable across
project-label renames) — with the same fork/resume canonicalization friction.cjs
already applies before clusters are emitted (sessions sharing >=1 message
uuidcollapse to one canonical id; do not re-derive fork identity yourself). - Migration (one-time, grandfathered): SEED, DO NOT COUNT — an entry whose
session_idsis empty going in AND carries no prior "identity migration" history line has that line's first match seedsession_idsfrom this run's matches WITHOUT incrementingsessions/last_seen/recurred_while_hot. The migration-fill sub-case —session_idsstill empty but an "identity migration" line already exists — fillssession_idson the first post-migration match, still without incrementing; counting resumes only oncesession_idsis non-empty. - Counting is per CLUSTER INDEX, never per hash and never once per label-group.
For each cluster index in a matched group: if none of that cluster's own hashes are
already in the entry's stored set, it is one genuinely new conversation —
sessions+= 1 (never by hash count),last_seenrefreshed, and — if the entry ishotand the session's own date is on/after the current attempt'sadopteddate (the adopted-date gate: a mistake that predates the rule's current phrasing isn't a phrasing failure of it) —recurred_while_hot+= 1. A gated-out (predates-adopted) new conversation still counts as evidence (sessions, hash) but not towardrecurred_while_hot. A hash already present is a re-scan of an already-counted conversation: only missing alias hashes are added, nothing else changes. - Promotes
observing→hotatsessions >= 5(a fresh ledger on a project with mature global evidence can hit the hot case on its very first run: new entry bornhotdirectly ifsessions >= 5on arrival). - No match, cluster
sessions== 1 → writes nothing. The ledger tracks recurrence; a single occurrence has none to track yet. Friction re-scans every session log every run, so a later run matches it back to 2+ sessions and seeds it then — this does not change matching against an EXISTING entry, which is recurrence regardless of the matching cluster's own session count. A match is not an increment. Whether it counts as a new conversation is decided in 4c byfriction.cjs count, which is a no-op when that session hash is already stored — so several matches against one entry routinely produce zero increments, and that is correct, not a miscount. - For
new:<theme>groups with no ledger match: distinct conversations = distinct cluster indices in the group (within one classify batch, no two cluster indices share a session hash).sessions < 2→ writes nothing.sessions >= 2→ new entry,statusfollows the same >=5-hot / else-observing rule.
Open item 1 —
new:label collisions: resolved (Guard B).new:clusters never merge in-batch, regardless of whether two cluster indices share the samenew:string — eachnew:cluster withsessions >= 2creates its own ledger entry, and a genuine recurrence of the same new mistake is matched on a later run byclass_hints, like any other entry. Measured against the alternative (merge same-labelednew:clusters when theirtop_keywordsoverlap by >=1): a synthetic new entry was correctly re-matched by a fresh classifier on 5/5 runs under Guard B, while the keyword-overlap guard wrongly merged two real, distinct mistakes on real data (clusters 21/23 — unrelated mistakes sharing the generic keyword "fucking validate").friction.cjs countimplements Guard B: everynew:-labeled cluster stands alone.Open item 2 — a generic one-line
ruleunder-specifies the class for matching: the ledger'sclass_hints+rulealone can be too broad for the LLM classifier (4a) to reliably tell two different mistakes apart, as the "fucking validate" false-merge case above shows. Consider requiring a short negative example ("NOT X, even though it sounds similar") on ledger entries whoseclass_hintsare single generic words/phrases.After
friction.cjs countreturns, resume the parts it does not do:- Escalation. For any
hotentry the count run left withrecurred_while_hot >= 2: the phrasing failed. Mark the current attemptoutcome: "failed", draft attempt n+1 — it must differ from every prior attempt's text in this entry (failed attempts are the rejected-edit buffer: never re-propose one verbatim) — replacerule, update MEMORY.md's Antigens section (step 5), resetrecurred_while_hotto 0. If 2 attempts have already failed and the antigen persists →status: "escalated": remove the rule from MEMORY.md's hot section, record a Fact instead ("persistent failure mode: — no phrasing reduces it"), and flag it in the step-8 report. Flag, don't act — the user decides: enforcement (a hook, where the tool has them) or accepted limit. escalated/rejectedentries (rejected = user veto) never get a new attempt proposed, count run or not.
Decay (observing only). Antigens are the fastest-decaying artifact and, until now, had no exit. This is meaningful because of the identity fix above — without it,
last_seenwould refresh on every re-scan and nothing would ever go stale. After the matching pass, sweep everyobservingentry: if itslast_seenis older than 8 weeks (matching the ~7-week transcript retention — evidence that old can no longer be re-verified against the source logs), setstatus: "expired"and append a history line "expired — no new evidence in 8+ weeks". The ledger entry is kept, never deleted (append-only doctrine) — it just stops rendering into MEMORY.md. If a later run's new session hash matches anexpiredentry'sclass_hints, merge the evidence and setstatusback to"observing"with history "reactivated".hotentries never expire by age — a loaded rule that stops recurring is the rule working, not staleness; a hot entry leaves hot only via the existingrecurred_while_hotescalation path above.escalated/rejectedare untouched by decay.Consistency: MEMORY.md's Antigens section is the render; the ledger is the record — after 4c every hot antigen in MEMORY.md has a matching
hotledger entry. Mutations are append-friendly: merge evidence and change status, never delete entries or history lines. If ledger.json is malformed, say so loudly, move it aside asledger.json.bad-<date>, and start fresh — never silently overwrite. - Session identity — the trailing 8-char hash of a session id (stable across
project-label renames) — with the same fork/resume canonicalization friction.cjs
already applies before clusters are emitted (sessions sharing >=1 message
-
-
Inject memory + rules references into AGENT.md
-
Compose the section between
<!-- MEMORY:START -->and<!-- MEMORY:END -->markers:<!-- MEMORY:START --> @.amp/remember/MEMORY.md <!-- MEMORY:END --> -
The reference is an explicit path (
@.amp/remember/MEMORY.md) —@-references resolve relative to the file containing them, so a bare@MEMORY.mdin the project root would point at a nonexistent root-level file. Claude loads the full file directly, so no inline duplication is needed -
If
.amp/remember/AGENT_RULES.mdexists (bootstrapped in step 1), compose a second, independent section between<!-- AGENT_RULES:START -->and<!-- AGENT_RULES:END -->markers. Unlike MEMORY.md above, the file itself is never@-referenced — an@-reference hot-loads all ~300 lines into every session, and it is a standards guide to consult when designing/building something new, not hot context. The section carries a path pointer plus exactly two inline rules: the ones that change what you TYPE, which you cannot look up because you do not know you need them. Everything else stays behind the pointer. Write the section verbatim, rules first:<!-- AGENT_RULES:START --> **One writer per piece of state.** One function assigns each field; everything else calls it. Grep who writes it before you write it — and if a write can land from a callback, thread, or lifecycle, the reader must tell stale from fresh. **Surgical changes only.** Touch what the task requires. Dead code, nits, bugs you pass: if it's inside or affects the code you're already changing and the fix changes no behavior, fix it and say so — otherwise report it and say what it costs to leave it. A problem you don't fix goes in the report, never in a comment. Standards guide (read when designing/building something new, not hot context): .amp/remember/AGENT_RULES.md <!-- AGENT_RULES:END --> -
Each marker pair is independent: if AGENT.md lacks a given pair, append it at the end; if a given pair already exists, replace its content in place; if no AGENT.md exists, create one containing whichever section(s) apply.
-
An existing AGENT_RULES pair is left alone — bootstrap once, never overwrite. The block above is what to write when creating it, not a template to re-impose every run. Users trim this section deliberately (a pointer-only variant is common), and rewriting it silently re-adds text they removed, on every single run, forever. Observed in the field: a run restored the inline rules into a AGENT.md whose owner had cut them, and the edit had to be reverted by hand. Note this no longer matches how
AGENT_RULES.mditself is handled:sync-rules.cjsrefreshes that file every run, because it is a shipped standards document with a backup behind it. This section is prose the user owns, with nothing behind it — so it stays bootstrap-once. -
If an existing pair is present but its path pointer is missing or wrong, that is load-bearing: report it and stop, do not silently rewrite the section around it.
-
Then assert the stub SHAPE mechanically — run the bundled script by absolute path, for the same reason as steps 0 and 1:
node ~/.config/amp/skills/remember/stub-check.cjsIt edits only inside the marker pairs, and only the mechanism: a MEMORY include that is not
@.amp/remember/MEMORY.mdis repaired, and an@-include ofAGENT_RULES.mdis demoted to a plain pointer. Prose inside the blocks is user-owned and is never touched, which is why the bootstrap-once rule above still holds. It will not repoint a MEMORY include at a file that does not exist — an un-migrated.amp/memory/repo has a live MEMORY.md at the old path, and breaking a working include to satisfy a naming convention is worse than reporting it. Silent when the shape is already current; relay whatever it prints in the step-8 report.Measured 2026-09-03: 21 of 37 local repos still carried the pre-v2.19
@-include ofAGENT_RULES.md, hot-loading ~300 lines into every session. A shape rule checked by asking you to look is a rule that drifts back; this one is a byte-level assertion done by the script, never by you.
# Project Memory > Auto-generated by /remember. Do not edit manually. ## Facts - [one-line rule, target 160 chars, hard stop 180] ## Episodes ### YYYY-MM-DD - [title] - [bullet narrative] ## Antigens [rendered — see below, do not hand-write]The
## Antigenssection is rendered, not hand-written — run this too, as a literal command, do not hand-write it even to match the format shown above. After 4c has overwrittenledger.json, run:node <path-to-friction.cjs> render <ledger.json>Take that command's stdout verbatim and replace MEMORY.md's entire
## Antigenssection with it (from the## Antigensline up to, but not including, the next##heading, or to end of file if Antigens is the last section — it usually is). This step's output IS the correctness check for itself: after replacing,node <path-to-friction.cjs> check <ledger.json> <MEMORY.md>must reportI6-new: EQUAL— if it doesn't, something was edited by hand instead of pasted from the script's stdout; redo it from the script's stdout exactly, never patch MEMORY.md manually to make it match.friction.cjs renderprints the section byte-for-byte, no LLM paraphrase, no manual template filling:## Antigens ### High Confidence (loaded — applies every session) - [behavioral rule] (evidence: [N] sessions, [P] projects — "[quote1]", "[quote2]") — ag-NNN ### Medium Confidence (observing — not loaded) - [behavioral rule] (evidence: [N] sessions) — ag-NNN ### Low Confidence (needs more data) - [pattern] (evidence: [N] sessions) — ag-NNNTiers: High =
status == "hot" && sessions >= 5(the only tier that prints quotes — the first 2 ofevidence.quotes, verbatim, capped at 2 — andevidence.projects.length). Medium =status == "observing" && 3 <= sessions <= 4. Low =status == "observing" && sessions == 2.expired/escalated/rejected/sessions < 2never render, same as before — an entry markedexpiredby the decay rule (4c) is skipped here even though it stays inledger.json. A legacy 1-sessionobservingentry (see 4b) stays in the ledger and grows or expires like any other, but is not rendered. An empty tier prints- (none — <why>), one line, never an empty section. -
-
Update processed manifest
- Append paths of newly processed stashes to
.amp/remember/.processed
- Append paths of newly processed stashes to
-
Docs reconcile check + auto re-index (best-effort, crash-isolated like step 0)
/remembernever reconciles doc CONTENT, never writes frontmatter, never edits a page — the only write here is the generateddocs/index.mditself, via the same deterministicindex-flatscript/docs-builderalready uses, never a model call. Wrapped so any failure here can never block the memory write that already happened in steps 3-6.- Locate
docs-builder.cjs— it ships in thedocs-builder/directory that sits beside this one (underskills/on Claude Code and Amp, undercommands/on Droid and opencode), exactly asfriction.cjsships beside this file. Call it by its absolute path in the command below — the cwd here is the target repo, not this package, so a cwd-relative path fails everywhere except the liteagents repo itself. - Not applicable, stay silent: if the project has no
docs/directory, skip without saying anything. Most projects have no doc corpus and a nudge every run is noise. - Applicable but could not run — say so, loudly: if
docs/exists but the script is missing,gitfails, or the command errors, print one line explaining why the check was skipped. Never fail silently. docs/exists but nodocs/.docs-builder/directory: docs-builder has never run here — print one line telling the user to run/docs-builder reorgto organize and index the corpus. Do NOT tell them to runledgerinstead:ledgeronly stamps whatever is currently on disk as the baseline, so on an unsorted pile it would record the mess as correct andduewould then report NOT due forever.docs/.docs-builder/ledger.jsonexists: rundueand pass through its verdict, as below.- Otherwise run it and pass through its verdict:
node <docs-builder.cjs> dueduecomparesdocs/against the SHA stamped indocs/.docs-builder/ledger.jsonusinggit diff --numstat -M, classifying each doc as new / moved / moved+changed / changed (with the line delta and rough percentage) / deleted. It is due at >=5 changed docs — the same derived-not-counted shape as/stash's nudge. - If
dueprints "no ledger yet" (nodocs/.docs-builder/ledger.jsonto compare against), do NOT relay it — print the same/docs-builder reorgline as the no-docs/.docs-builder/case above, for the same reason:ledgerwould stamp an unsorted pile as correct. - Auto re-index — script only, no model, in addition to the DUE advisory below, not a
replacement for it. If
due's output was NOTdocs unchanged since <sha>. NOT due.(i.e. it printed a row table -- any new/moved/moved+changed/changed/deleted doc, whether or not the >=5 threshold below was crossed), the index has drifted and self-heals right here, unconditionally:
Same scriptnode <docs-builder.cjs> index-flat/docs-builder reorgalready calls, run standalone — no model call, no interview, nothing moves. Note in the step-8 report thatdocs/index.md(anddocs/log.md, ifindex-flattouched it) were regenerated, so they are included alongside whatever step 3-6 already changed when this run is committed. - If DUE (the row count crossed the >=5 threshold), ALSO end with one line:
docs: 7 changed since 991f72d3 — run /docs-builder reorg
- Locate
-
Report to user — print it AND write the same content to
.amp/remember/report.md(overwritten each run; the ledger keeps history — the report is just the latest snapshot)- Number of stashes processed
- Facts count (before → after the rewrite) plus how many existing lines were merged or shortened. At steady state — lines already ≤160, no near-duplicates — a run that grows by exactly its new facts and shortens nothing is correct; say so rather than forcing merges to hit a number. The bound on facts is the write-bar at entrance, not a count.
- Mechanical length check — run, don't estimate. This confirms the step-3 gate rather
than being the first check to catch an overrun. It implements the SAME mechanical
exemption as the gate (a line whose longest backtick literal exceeds 100 chars is not
flagged), so this count and the gate's count can never disagree:
Print every line it returns and the count. Zero is the target; non-zero means a gate miss — every remaining overrun already had its chance to be exempted (100-char backtick literal) inside the step-3 gate, so anything printed here should not exist.awk ' /^## Facts/{f=1} /^## Episodes/{f=0} f && /^- / && length($0)>180 { line=$0; maxlen=0 while (match(line, /`[^`]*`/)) { seglen = RLENGTH-2 if (seglen > maxlen) maxlen = seglen line = substr(line, RSTART+RLENGTH) } if (maxlen <= 100) print }' .amp/remember/MEMORY.md - Episodes count (new, kept hot, folded + deleted)
- Antigens count by confidence tier, with how many newly promoted to hot — sourced
from
.amp/remember/friction/count_report.json(4c's count report), not recomputed by hand - Ledger lines — one per non-observing entry: id, short rule, status, recurrences since
adoption. Highlight rephrased (RECURRED) and ESCALATED entries; escalations need a
user decision, e.g.:
ledger: ag-001 "verify live after publish" hot, 0 recurrences since 2026-07-10 ledger: ag-003 "don't commit per change" RECURRED while hot (2/2) → rephrased, attempt 2 ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept? - Relay verbatim whatever
version-check.cjs(step 0),sync-rules.cjs(step 1), andstub-check.cjs(step 5) printed. Never re-word or summarize them: they are the record of a file that was written or a version gap, and a paraphrase of "your body was backed up to AGENT_RULES.md.bak" can lose the filename the user needs. - Each is silent when nothing changed, so silence is the normal case and there is nothing to invent — never report an action that produced no output.
- Never a silent write: if any of the three wrote or moved a file and you did not relay its line, that is a defect.
- If step 7 ran the auto re-index, say so and name the regenerated files
(
docs/index.md, plusdocs/log.mdif touched) so they are staged with this run - Confirm MEMORY.md and AGENT.md updated
File locations (all project-local — two dirs: /stash owns .amp/stash/, /remember owns .amp/remember/)
- Stash files:
.amp/stash/*.md - Memory file:
.amp/remember/MEMORY.md(single source of truth, referenced as@.amp/remember/MEMORY.md) - Rules template:
.amp/remember/AGENT_RULES.md(refreshed from the bundled package template every/rememberrun bysync-rules.cjs; a differing body is backed up first, not silently overwritten — referenced by a plain path pointer, not@-referenced — see step 5) - Rules backup:
.amp/remember/AGENT_RULES.md.bak(written bysync-rules.cjsonly when the existing body differs from the template; a single file, overwritten each time it fires — not timestamped) - Antigen ledger:
.amp/remember/ledger.json(per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot) - Consolidation report:
.amp/remember/report.md(latest step-8 report, overwritten each run) - Processed manifest:
.amp/remember/.processed - Docs ledger (READ ONLY from here — owned by
/docs-builder):docs/.docs-builder/ledger.json - Friction output (transient, regenerated each run):
.amp/remember/friction/—antigen_clusters.json(preferred input),antigen_review.md(fallback), plus raw analysis files - Output:
AGENT.md(managed MEMORY section, plus an AGENT_RULES section once bootstrapped)