From session-orchestrator
Reviews, consolidates, and prunes Claude Code project memory files. Supports dry-run and atomic apply modes for safe batch maintenance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/session-orchestrator:memory-cleanupsonnetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implements the 4-phase memory consolidation process modelled after Claude Code's Auto Dream feature. Run after major refactors, framework migrations, or every 5+ sessions in a repo.
Implements the 4-phase memory consolidation process modelled after Claude Code's Auto Dream feature. Run after major refactors, framework migrations, or every 5+ sessions in a repo.
The memory system lives at ~/.claude/projects/<encoded-cwd>/memory/ and consists of:
MEMORY.md — index file (must stay under 200 lines; lines after 200 are truncated by the harness).name, description, metadata.type).The four memory types are user, feedback, project, reference (see global auto memory instructions for semantics). This skill never invents new types.
This skill accepts two optional flags. Default (no flag) runs the interactive 4-phase consolidation defined below.
| Flag | Behavior |
|---|---|
--dry-run | Run Phases 1-3 read-only; instead of mutating MEMORY.md / topic files, write a unified-diff proposal to .orchestrator/pending-dream.md (atomic). Exit 0. |
--apply-pending | Read .orchestrator/pending-dream.md; refuse if older than 14 days; apply diff; delete pending file; print auto-dream applied: -<X> lines, +<Y> entries. Exit 0. |
Flags are mutually exclusive — passing both is an error. Absence of both = legacy interactive mode (Phases 1-4 below).
--dry-run flow:
```diff block (or as a complete replacement body when full rewrite is simpler).writePendingDream({ repoRoot, diff, sourceSession, memoryLinesBefore, proposedLinesAfter }) from scripts/lib/auto-dream.mjs.pending-dream written: <N> lines proposed (or no consolidation needed (MEMORY.md is healthy) when the plan is empty).--apply-pending flow:
applyPendingDream({ repoRoot, memoryDir }) from scripts/lib/auto-dream.mjs.applied: true → print auto-dream applied: -<linesBefore-linesAfter> lines, +<entries> consolidated entries and exit 0.applied: false, reason: 'missing' → print no pending dream to apply and exit 1.applied: false, reason: 'stale' → print pending dream is stale (>14d), re-run --dry-run and exit 1.Both flag-driven flows delegate atomicity and staleness enforcement to scripts/lib/auto-dream.mjs. The interactive Phases 1-4 below remain unchanged.
Understand the current memory state before making changes.
List all files in the memory directory:
ls -la ~/.claude/projects/*/memory/ 2>/dev/null | grep "$(basename "$(pwd)")"
Or directly list the project's memory dir (the path is in the auto memory system instructions).
Read MEMORY.md (the index file) — note its line count:
wc -l <memory-dir>/MEMORY.md
Skim each topic file referenced in the index. Build a mental map:
Goal: Improve existing files, never create duplicates.
Find what's changed since the last consolidation.
Git history — recent commits give the timeline for "did this fact change?":
git log --oneline -20
Stale references — for each file/function/symbol mentioned in memory, verify it still exists:
grep -rn "specific-file-or-function" src/ lib/ scripts/ 2>/dev/null
Relative dates — find temporal references that need conversion:
grep -rni "yesterday\|today\|tomorrow\|last week\|this week\|gestern\|heute\|morgen\|letzte woche" <memory-dir>/
Version drift — package versions stored in memory vs reality:
cat package.json | grep '"version"'
Test/issue-count drift — claims like "5001 passed" or "8 open issues" age fast. Cross-check with current state if mentioned.
Apply maintenance operations to memory files.
If multiple files or entries describe the same thing, combine them into one authoritative entry. Update inbound [[wiki-link]]s accordingly.
"Yesterday we decided X" → "On 2026-03-24 we decided X". Always use ISO date format (YYYY-MM-DD). This rule applies on write, but cleanup catches what slipped through.
git log --diff-filter=R surfaces renames).decisions.md-style note that the decision was reversed, with the reason — don't silently overwrite).If two memory entries conflict, check the codebase to determine which is current. Delete the outdated entry. Never leave contradictions.
Keep MEMORY.md clean and under the 200-line limit.
Check line count:
wc -l <memory-dir>/MEMORY.md
If over 180 lines, extract detailed content into topic files. Suggested naming:
project_sessions.md or session-YYYY-MM-DD-<slug>.mdreference_decisions.mdreference_packages.md{type}_{topic}.mdTopic file frontmatter (required):
---
name: descriptive-name
description: one-line description for relevance matching
metadata:
type: project # one of: user | feedback | project | reference
---
Update the index:
MEMORY.md is an index, not a dump.- [Title](file.md) — hook entries.Final verification:
wc -l <memory-dir>/MEMORY.md # must be < 200
# verify all index links resolve to existing files
grep -oP '\]\(([^)]+\.md)\)' <memory-dir>/MEMORY.md | sed 's/](\(.*\))/\1/' | while read f; do
[ -f "<memory-dir>/$f" ] || echo "BROKEN: $f"
done
Skip if
persistence: falsein Session Config. Silent no-op if no Auto-promoted sibling worktrees exist or none are stale.
Detect stale Auto-promoted worktrees (older than stale-branch-days from Session Config, default 7 days) and offer them for batch-removal alongside the other housekeeping prune actions from Phase 4. This sub-phase is additive — it appends candidates to the existing prune flow rather than introducing a separate independent confirmation cycle.
Auto-promoted worktrees follow the layout <parentDir>/<repoName>-<sessionId>/, where <sessionId> is a semantic session-id (per parseSessionId() from scripts/lib/session-id.mjs). Random-suffix worktrees (UUID-format) are NOT auto-promoted and MUST be ignored.
Authoritative impl:
scripts/lib/memory-cleanup/worktree-sweep.mjs—listAutoPromotedWorktrees(repoRoot, mainCheckoutRoot, opts). Import and call; do NOT re-implement from this doc.Algorithm: split
git worktree list --porcelainon the blank-line delimiter; for eachworktreeentry, skip the main checkout, then require the basename to start with<repoName>-and the suffix to parse as asemanticsession-id viaparseSessionId()(UUID / random suffixes excluded). Each match yields{ wtPath, sessionId, branch }. Any git failure →[](conservative no-op). Git invocation is via the injection-safeopts.execFileFn(defaultexecFileSyncwith an args array — #577 HARDEN-001).
A worktree is stale iff mtime(worktree-dir) < now - stale-branch-days × 86400 × 1000. Read stale-branch-days from Session Config (default 7):
Authoritative impl:
scripts/lib/memory-cleanup/worktree-sweep.mjs—isWorktreeStale(wtPath, staleBranchDays). Import and call; do NOT re-implement from this doc.Algorithm:
statSync(wtPath); stale iffDate.now() - stat.mtimeMs > staleBranchDays × 24 × 60 × 60 × 1000. A missing or unreadable path →false(conservative no-op).
After Phase 4 (Prune & Index) compiles its list of items to offer for removal, ALSO include any stale auto-promoted worktrees. The user sees them in the same AUQ ("batch-decide" per PRD §3 P3 Gherkin row 4):
import { execFileSync } from 'node:child_process';
import { listAutoPromotedWorktrees, isWorktreeStale } from '$PLUGIN_ROOT/scripts/lib/memory-cleanup/worktree-sweep.mjs';
import { discoverActiveSessions } from '$PLUGIN_ROOT/scripts/lib/session-discovery.mjs';
const staleBranchDays = $CONFIG['stale-branch-days'] ?? 7;
const candidates = listAutoPromotedWorktrees(process.cwd(), mainCheckoutRoot);
const stale = candidates.filter((c) => isWorktreeStale(c.wtPath, staleBranchDays));
if (stale.length === 0) {
// Silent no-op — no stale worktrees to offer.
} else {
// #580-HARDEN-002: cross-check candidates against LIVE peer sessions before offering removal.
// A stale-by-mtime worktree may still be the checkout of an active parallel session — removing
// it would disrupt that session (PSA-002 overlap). Flag any such candidate so the AUQ can warn.
const active = await discoverActiveSessions(mainCheckoutRoot);
const livePaths = new Set(active.map((s) => s.worktreePath));
const annotated = stale.map((wt) => ({ ...wt, activePeer: livePaths.has(wt.wtPath) }));
// Add to the existing housekeeping prune AUQ. Each stale worktree becomes one option line:
// Format: `[ ] Stale auto-promoted worktree: <basename> (age <N>d) — remove via 'git worktree remove --force'`
// If `activePeer` is true, prefix the option with "⚠ ACTIVE PEER SESSION" and make Behalten the default.
//
// If memory-cleanup currently presents per-item AUQs (one question per prune candidate),
// append one question per stale worktree.
//
// If memory-cleanup uses a single multiSelect AUQ for the whole batch,
// add stale worktrees as additional options.
//
// Operator selects which to remove; coordinator runs (arg-array, no shell — #577 HARDEN-001):
// execFileSync('git', ['-C', mainCheckoutRoot, 'worktree', 'remove', '--force', wtPath])
// for each selected. WARN line per removal.
}
When the candidate's wtPath matches a LIVE peer session (wt.activePeer === true, from discoverActiveSessions() above), the AUQ MUST surface a ⚠ ACTIVE PEER SESSION warning and keep Behalten as the safe default — removing a live session's worktree mid-flight is a PSA-002 overlap that can corrupt another session's working state.
const peerWarning = wt.activePeer
? ' ⚠ ACTIVE PEER SESSION — removing may disrupt a live session.'
: '';
AskUserQuestion({
questions: [{
question: `Stale auto-promoted worktree found: ${path.basename(wt.wtPath)} (age ${ageDays}d, branch=${wt.branch}).${peerWarning} Remove?`,
header: "Stale-Worktree",
multiSelect: false,
options: [
{
label: "Behalten (Recommended)",
description: wt.activePeer
? "Keep this worktree — a LIVE peer session is using it. Removal would disrupt the active session."
: "Keep this worktree. Re-evaluate at next /memory-cleanup run.",
},
{
label: "Entfernen",
description: wt.activePeer
? `⚠ ACTIVE PEER SESSION detected. Only choose this if you have confirmed no live session needs it, then run 'git worktree remove --force ${wt.wtPath}'.`
: `Run 'git worktree remove --force ${wt.wtPath}'.`,
},
],
}],
});
Per .claude/rules/parallel-sessions.md PSA-003: every git worktree remove is destructive — operator must explicitly authorize. The AUQ above satisfies this. Never auto-remove without user confirmation, even for stale worktrees. The --force flag is acceptable here only because the AUQ already secured explicit per-worktree consent; never apply --force to worktrees the operator did not select.
docs/prd/2026-05-26-parallel-aware-sessions.md §3 P3 Gherkin row 4 + §3.A P3 EARS state-driven clausestale-branch-days config: scripts/lib/config.mjs:138 (default: 7)parseSessionId() from scripts/lib/session-id.mjslistAutoPromotedWorktrees() + isWorktreeStale() from scripts/lib/memory-cleanup/worktree-sweep.mjsdiscoverActiveSessions(repoRoot) from scripts/lib/session-discovery.mjs — flags stale candidates whose wtPath matches an active session so the AUQ warns before removal.claude/rules/parallel-sessions.mdAfter completing all four phases, report:
MEMORY.md line count (before → after).AskUserQuestion rather than guessing).user, feedback, project, reference) is load-bearing for the relevance heuristic.MEMORY.md if line-count is already healthy. Re-ordering for its own sake creates churn without value.npx claudepluginhub kanevry/session-orchestrator --plugin session-orchestratorAudits and selectively forgets stored Claude Code memories. Use when memory is large/uncurated, project state has shifted, or retrieval quality degraded.
Consolidates Claude Code's auto-memory across sessions using a four-phase cycle (Orient, Gather Signal, Consolidate, Prune Index) with seven operators. Activates via session hooks or manual `/dream` command.
Manually reviews and prunes stale project_* memory entries older than 30 days. Keeps MEMORY.md under the 200-line auto-load limit without auto-deleting.