Turn mined candidate turns into a small set of durable facts worth remembering.
Steps
Run the mine.
sessions memory mine --repo <path> --json(omit--repofor the current repo;--allmines every repo in the index; add--since-lastto mine only transcripts that changed since the previous mine, which is what the weekly summary uses). stdout is a JSON array of candidate records; progress goes to stderr. If the command is not found, say so and stop — do not substitute a search. If the array is empty, say there is nothing to triage and stop; never invent candidates.Confirm fuzzy paraphrase pairs.
sessions memory report --repo <path> --json(same scoping flags as the mine) returns afuzzyarray: freshly mined clusters that look like paraphrases of an APPROVED memory but scored below the binary's assert threshold. The binary deliberately does not make this judgment — you do. Present each pair (cluster.text~memory.text, with its similarity and session count) and ask confirm/deny, batched with the triage questions in step 7 rather than as a separate interrogation. If the array is empty, move on.- Confirm →
sessions memory merge <memory-id> <cluster-id>— the approved memory is the canonical, the new phrasing folds into it. This is the same write-back as step 8's clustering merge: future mines recognize the phrasing as accounted for, and the pair stops appearing in the report. If merge exits non-zero on an unknown id, runsessions memory mine --repo <path>first and retry — report reads fresh evidence without persisting it, and merge only knows ids the store holds. - Deny →
sessions memory snooze <cluster-id>. Reject would kill the fact itself, which is terminal and wrong when the fact is real; snooze is the suppression that persists the dismissal without a verdict. The report's pipeline drops suppressed clusters before classifying, so the denied pair goes quiet on later runs instead of being re-asked every triage. If snooze exits non-zero on an unknown id, runsessions memory mine --repo <path>first and retry, exactly as with merge. A denied pair resurfaces only the way any snooze does — a later merge folding in a new phrasing after the 30 days pass, which the current mine never produces, so in practice the denial holds until the clustering evidence changes.
- Confirm →
Cluster paraphrases. Group candidates whose texts assert the same fact in different words — "use canary as the base branch" and "we branch off canary" are one memory in two phrasings. A cluster's
distinctPhrasingsis the number of distinct member texts. Keep the clearest phrasing as the cluster'stext; the rest are evidence, not separate memory. Byte-identical repeats were already collapsed upstream, so every member you see is genuinely a different wording.Read what is already binding.
sessions memory documented --jsonlists every statement an agent working here is already told — the globalCLAUDE.md, a repoCLAUDE.mdorAGENTS.md, and Claude Code's own per-project memory store. Read it before the rubric, because it decides the rubric's first question.These are read, never written. This store complements those surfaces; it does not replace, edit, or override them.
Other agents' stores are candidates, not gospel. If the user asks to pull in what another agent remembers — pi-hermes-memory's store, Claude's CLAUDE.md files — run
sessions memory import --from pi-hermes(orclaude, orall) FIRST, before mining: those facts land in the candidate batch you are about to triage, marked by their lack of session evidence. Triage them through the same rubric below; thereview_agent_memoriesMCP tool previews what would import and flags entries that overlap memories already in the store (similarTo), andget_memory_sourcesinventories what stores exist. A pi-hermes fact scoped to a bare project name arrives unbound — bind it with--scope repo:.when you approve it, or reject it if the project is gone.Apply the generalizability rubric. For each cluster ask: does this fact hold beyond the session it appeared in, and is it not already stated somewhere binding? Propose only the clusters that pass both.
- Passes — standing constraints ("API keys go in the keychain when available"), repo or tooling facts ("this repo branches off canary", "skills can invoke inline scripts"), architectural rules.
- Fails — one-off task instructions ("make it Ideation instead of docs/ideation", "let's do a single PR"), bug reports ("syntax highlighting isn't loading"), anything naming a specific transient artifact.
- Fails as already documented — the fact is already stated in step 4's output. Say which source when you drop it. Claude Code's memory store is the same genre as this one, captured the same way from the same sessions, so this is the expected overlap rather than a rare one — a second copy spends context twice and drifts from the first the moment either is edited.
- Fails loudly — text that reads like a copy-pasted prompt or eval fixture. A suspiciously boilerplate candidate is chaff, not signal.
- Contradicts something binding — do not silently pick a winner and do not approve it. Surface both statements to the user and let them decide, once, here — a contradiction resolved at triage costs one question; resolved never, it costs an agent's confusion in every future session.
- Assign
kind:instructionfor "do this / don't do that",informationfor "this is how the world is".
Write the fact, not the utterance. A candidate's
textis a verbatim user turn, so it is routinely a question, an aside, or a fragment that only implies the rule. Whenever the clearest statement of the fact is not exactly what the candidate says, approve with--as:sessions memory approve <id> --as "Do not generate Cursor MCP config — Cursor is not used here."This matters more than it looks. Without
--as, what a future agent receives is the raw turn — and the tool that serves it tells that agent to treat it as binding."Cursor MCP config? I don't use Cursor"is a fine candidate and a useless instruction. The original is kept as evidence, and the evidence count does not change: your rewrite is not a phrasing the user ever used.Walk triage. One
AskUserQuestionper cluster, batching up to 4 independent clusters per call. For each, show the text, the derived scope (repowith its container, orworkflow),distinctPhrasings, and thefirstSeen–lastSeenrange. Options: Approve, Approve as always-on, Reject, Snooze (hide without a verdict).Offer Approve as always-on only when the fact is a standing constraint an agent must see no matter what it is working on — "canary is the mainline branch", "API keys go in the keychain". Retrieval is topic-conditional by default, so a normal approval means the memory comes back only when the task looks related. Always-on is the exception, and proposing it for everything defeats the filter — mechanically as well as in spirit: the set is hard-capped (20 entries / 2,000 chars), and a grant past the cap is refused.
Persist. One command per decision, using the cluster's
id:sessions memory merge <id> <other-id>...— run this first for any cluster with more than one recordsessions memory approve <id> --as "<the fact>"(add--always-onfor a standing constraint)sessions memory reject <id>sessions memory snooze <id>
Each exits non-zero on an unknown id, so a failure means the decision was not recorded — surface it rather than reporting success.
An approve can also be refused outright, with the reason on stderr. Two cases. A content refusal means the text matches secret material or prompt-injection patterns: reject the record, or — if the underlying fact is real — approve a clean rephrasing with
--asthat states the fact without the flagged content. An always-on budget refusal means the standing set is full: either approve without the flag (the memory still serves topic-conditionally), or ask the user which existing constraint to demote and free its slot withsessions memory approve <id> --no-always-on. Never silently drop the decision — report what was refused and what you did instead.Merge is not optional bookkeeping. Your clustering in step 3 exists only in this conversation until you write it back. An id is a hash of that record's own text, so every phrasing is a separate row and nothing else can ever tell them apart.
mergefolds the members' evidence onto the canonical record — distinct phrasings counted, sessions unioned, date range widened — which is what makesdistinctPhrasingsmean "how many ways the user has said this" instead of a permanent 1. It is also what lets a snoozed memory come back, and what gives the cross-author quorum something real to count. Pick the clearest phrasing as the canonical, pass the rest as members, then approve/reject/snooze the canonical.If the user says a fact applies to a set of related repos rather than just this one, assign a project group:
sessions memory approve <id> --scope group:<name>. Only ask when they volunteer it — group membership comes from~/.local/share/sessions/groups.json, which the user maintains by hand, and a group they have not configured there is silently never returned. Say so if you assign one.An imported record needs a repo before it means anything. A record whose scope is
repowith an EMPTY key came from someone else's bundle: export strips the local path, and retrieval skips a keyless repo memory rather than letting it match every repo. Approving it as-is succeeds and changes nothing. Bind it in the same command:sessions memory approve <id> --scope repo:.for this repo, or--scope repo:<path>for another (the path is resolved to its repo container, so a worktree or subdirectory works). If the fact is clearly not repo-specific, say so and let the user decide instead of guessing a repo.Report. Proposed count, approved count, and the ratio. That ratio is the number the precision goal is measured on, so state it plainly even when it is bad.
Report the untriaged backlog too, from
sessions memory pending. You triaged one batch; the store can hold thousands of candidates from earlier mines, and a summary that says "triage complete" while thousands wait is the one line in this skill that can be actively false. Say "N approved of M proposed this batch; K candidates still awaiting triage."Also report how many clusters you dropped as already documented, and where they were already stated. That number is the measure of whether this store is complementary or is quietly duplicating what Claude Code already injects — the one thing no test can check.
Guidelines
- Propose only what passes the rubric. Dumping every narrowed candidate on the user is the failure mode that trains people to reject the whole list without reading it — a short, high-precision proposal is the point.
- Records already carry a
state. Triage thecandidateones and leaveapprovedrecords alone unless the user asks to revisit them. Rejected and merged records never appear. A record that was snoozed and is back in the batch has resurfaced — its 30 days passed and a merge added a new phrasing since, meaning the user kept saying it in different words. Say so when you present it; that history is the strongest evidence the original dismissal was wrong. repoandworkflowscope are derived from evidence and are shown, not edited — with one exception each way.groupis assignable because no derivation can reach it: the index cannot tell "these four repos share a convention" from "this is universal".repois assignable only because an imported record has no derivation to override — its key was stripped on export and no local transcript will ever produce it.workflowis never assignable: it is the only direction that widens, and a mistake there turns one repo's convention into a rule for every repo, silently.- Never paste raw session text you did not get from the batch — records deliberately carry no verbatim quotes.
- Snooze means "not now, but keep watching". A snoozed memory returns once 30 days have passed and a later merge folds in a phrasing that was not there before — continued repetition is treated as evidence the dismissal was wrong. Prefer it over reject when a fact might be real but the evidence is thin: reject is terminal, snooze records no verdict. Expiry alone brings nothing back, so a snooze on a fact the user never repeats stays hidden, which is the intended behavior rather than a gap.