Skip to content

Commit 8d7cc04

Browse files
noelsaw1claude
andcommitted
refactor: replace MEMORY.md archiving with duplicate detection in post-flight
The old check_memory() archived MEMORY.md to PROJECT/1-INBOX/ which was misaligned with Claude Code's per-file auto-memory model. New check_memory_duplicates() scans ~/.claude/projects/*/memory/ for orphaned files, broken index links, duplicate topics, and missing frontmatter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c827862 commit 8d7cc04

5 files changed

Lines changed: 134 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1313
- Do not edit a version block that has already been committed and pushed
1414
-->
1515

16+
## [1.9.1] - 2026-04-09
17+
18+
### Changed
19+
- **`experimental/post-flight.sh` — replace MEMORY.md archiving with duplicate detection**`check_memory()` removed; new `check_memory_duplicates()` scans the Claude Code auto-memory directory (`~/.claude/projects/*/memory/`) for orphaned files not linked from MEMORY.md index, broken index links pointing to missing files, duplicate topics (3+ memory files sharing the same `type` frontmatter), and missing frontmatter. Reports findings without auto-fixing — consistent with the script's read-only default mode. The old archive-to-`PROJECT/1-INBOX/` behavior was misaligned with Claude Code's per-file memory model. MCP handler and tool description updated to reflect the new check. Event name changed from `check:memory` / `action:archive` to `check:memory-duplicates`.
20+
1621
## [1.9.0] - 2026-04-05
1722

1823
### Added

PROJECT/1-INBOX/P1-POST-FLIGHT.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,11 @@ post-flight.sh --hook ./agent-hook.sh --json-events \
175175
The hook receives events like:
176176
- `check:4x4` — 4X4.md state
177177
- `check:changelog` — CHANGELOG.md state
178-
- `check:memory`MEMORY.md state
178+
- `check:memory-duplicates`Claude Code memory duplicate scan results
179179
- `check:git` — git working tree state
180180
- `validate:build` — syntax check results
181181
- `suggest:commit` — proposed commit message
182182
- `action:commit` — actually committing (agent can gate this)
183-
- `action:archive` — archiving MEMORY.md
184183

185184
### 🤔 Still Dumb?
186185

@@ -207,7 +206,7 @@ The script has been drafted and placed at `experimental/post-flight.sh`.
207206
post-flight.sh
208207
```
209208
- ✓ Checks 4X4.md, CHANGELOG.md, MEMORY.md freshness
210-
-Archives MEMORY.md if present → `PROJECT/1-INBOX/MEMORY-<timestamp>.md`
209+
-Scans Claude Code memory for conflicted duplicates (orphans, broken links, duplicate topics)
211210
- ✓ Reports git state (dirty, untracked, branch info)
212211
- ✓ Runs quick build validation (PHP syntax check, etc.)
213212
-**NO commit, NO push** — just reporting
@@ -253,7 +252,7 @@ post-flight.sh --push --dry-run
253252

254253
1. **4X4.md** — Exists and accessible
255254
2. **CHANGELOG.md** — Exists and accessible
256-
3. **MEMORY.md**Archives to `PROJECT/1-INBOX/MEMORY-<timestamp>.md`
255+
3. **Memory Duplicates**Scans Claude Code auto-memory directory for orphans, broken links, duplicate topics, and missing frontmatter
257256
4. **Git State** — Current branch, modified files, untracked files
258257
5. **Build Validation** — PHP syntax check (if available)
259258

@@ -263,12 +262,14 @@ Auto-generated:
263262
docs: Session cleanup — updated docs and archived MEMORY.md
264263
```
265264

266-
#### **Archive Behavior**
267-
If MEMORY.md exists:
268-
```
269-
MEMORY.md → PROJECT/1-INBOX/MEMORY-20260405-150230.md
270-
```
271-
Keeps MEMORY.md fresh for each session without losing context.
265+
#### **Memory Duplicate Detection**
266+
Scans `~/.claude/projects/<project-hash>/memory/` for:
267+
- Orphaned `.md` files not linked from `MEMORY.md` index
268+
- Broken links in `MEMORY.md` pointing to missing files
269+
- Duplicate topics (3+ files with the same `type` frontmatter)
270+
- Missing frontmatter (`name`, `description`, `type`)
271+
272+
Reports findings only — does not auto-fix or archive.
272273

273274
### Integration with Agents
274275

experimental/post-flight.sh

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ set -euo pipefail
66
#
77
# Session cleanup & documentation synchronization for solo developers.
88
#
9-
# Ensures 4X4.md, CHANGELOG.md, MEMORY.md are synced, optionally commits and
10-
# pushes with a single confirmation prompt.
9+
# Checks 4X4.md, CHANGELOG.md freshness, scans Claude Code memory for
10+
# conflicted duplicates, optionally commits and pushes.
1111
#
1212
# Usage:
1313
# post-flight.sh [OPTIONS]
@@ -45,11 +45,10 @@ set -euo pipefail
4545
# Agent Hook Events:
4646
# check:4x4 '{"status":"ok|missing|stale|mismatch"}'
4747
# check:changelog '{"status":"ok|missing|stale"}'
48-
# check:memory '{"status":"fresh|missing|should_archive","path":"..."}'
48+
# check:memory-duplicates '{"status":"ok|found|no_dir","duplicates":N,"orphans":N,"broken":N}'
4949
# check:git '{"status":"clean|dirty|needs_push","branch":"...","files_changed":N}'
5050
# validate:build '{"status":"ok|error","message":"..."}'
5151
# suggest:commit '{"message":"...","files":["..."]}'
52-
# action:archive '{"from":"...","to":"..."}'
5352
# action:commit '{"message":"...","sha":"..."}'
5453
# action:push '{"remote":"...","branch":"...","url":"..."}'
5554
# complete '{"status":"success|failed","summary":"..."}'
@@ -143,24 +142,117 @@ check_changelog() {
143142
return 0
144143
}
145144

146-
check_memory() {
147-
if [ ! -f "$REPO_ROOT/MEMORY.md" ]; then
148-
emit_event "check:memory" '{"status":"missing"}' || return 1
149-
echo "${YELLOW}⚠ MEMORY.md not found (OK)${NC}"
145+
check_memory_duplicates() {
146+
# Scan Claude Code auto-memory directory for conflicted duplicates,
147+
# orphaned files, broken index links, and missing frontmatter.
148+
#
149+
# Memory layout:
150+
# ~/.claude/projects/<project-hash>/memory/MEMORY.md (index)
151+
# ~/.claude/projects/<project-hash>/memory/<name>.md (individual memories)
152+
153+
# Derive the Claude Code memory dir for the current repo root.
154+
# Claude Code slugifies the absolute path: / → -
155+
local slug
156+
slug=$(echo "$REPO_ROOT" | sed 's|/|-|g')
157+
local memory_dir="$HOME/.claude/projects/${slug}/memory"
158+
159+
if [ ! -d "$memory_dir" ]; then
160+
emit_event "check:memory-duplicates" '{"status":"no_dir","duplicates":0,"orphans":0,"broken":0}' || return 1
161+
echo "${YELLOW}⚠ No Claude Code memory directory found (OK)${NC}"
150162
return 0
151163
fi
152-
153-
local timestamp
154-
timestamp=$(date +%Y%m%d-%H%M%S)
155-
local archive_path="$REPO_ROOT/PROJECT/1-INBOX/MEMORY-${timestamp}.md"
156-
157-
if [ "$DRY_RUN" -eq 0 ]; then
158-
mkdir -p "$(dirname "$archive_path")"
159-
mv "$REPO_ROOT/MEMORY.md" "$archive_path"
160-
emit_event "action:archive" "{\"from\":\"MEMORY.md\",\"to\":\"$archive_path\"}" || return 1
161-
echo "${GREEN}✓ MEMORY.md archived → PROJECT/1-INBOX/MEMORY-${timestamp}.md${NC}"
164+
165+
local index_file="$memory_dir/MEMORY.md"
166+
local duplicates=0
167+
local orphans=0
168+
local broken_links=0
169+
local missing_frontmatter=0
170+
local issues=""
171+
172+
# Collect all .md files in the memory dir (excluding MEMORY.md index)
173+
local memory_files=()
174+
while IFS= read -r -d '' f; do
175+
memory_files+=("$f")
176+
done < <(find "$memory_dir" -maxdepth 1 -name "*.md" ! -name "MEMORY.md" -print0 2>/dev/null)
177+
178+
if [ ${#memory_files[@]} -eq 0 ] && [ ! -f "$index_file" ]; then
179+
emit_event "check:memory-duplicates" '{"status":"ok","duplicates":0,"orphans":0,"broken":0}' || return 1
180+
echo "${GREEN}✓ Memory: no files to check${NC}"
181+
return 0
182+
fi
183+
184+
# --- Check 1: Orphaned files (in dir but not linked from MEMORY.md) ---
185+
if [ -f "$index_file" ]; then
186+
for mf in "${memory_files[@]}"; do
187+
local basename
188+
basename=$(basename "$mf")
189+
if ! grep -qF "$basename" "$index_file" 2>/dev/null; then
190+
orphans=$((orphans + 1))
191+
issues="${issues} orphan: ${basename}\n"
192+
fi
193+
done
194+
195+
# --- Check 2: Broken links (referenced in MEMORY.md but file missing) ---
196+
while IFS= read -r linked; do
197+
if [ ! -f "$memory_dir/$linked" ]; then
198+
broken_links=$((broken_links + 1))
199+
issues="${issues} broken link: ${linked}\n"
200+
fi
201+
done < <(grep -oE '\([^)]+\.md\)' "$index_file" 2>/dev/null | tr -d '()' | sort -u)
162202
else
163-
echo "${GREEN}✓ MEMORY.md would be archived → PROJECT/1-INBOX/MEMORY-${timestamp}.md${NC}"
203+
# No index but files exist — all are orphans
204+
orphans=${#memory_files[@]}
205+
for mf in "${memory_files[@]}"; do
206+
issues="${issues} orphan (no index): $(basename "$mf")\n"
207+
done
208+
fi
209+
210+
# --- Check 3: Duplicate topics (same type + similar name/description) ---
211+
# Extract type values from frontmatter and look for duplicates within each type
212+
declare -A type_files
213+
for mf in "${memory_files[@]}"; do
214+
local ftype
215+
ftype=$(sed -n '/^---$/,/^---$/{ s/^type:[[:space:]]*//p; }' "$mf" 2>/dev/null | head -1)
216+
if [ -z "$ftype" ]; then
217+
missing_frontmatter=$((missing_frontmatter + 1))
218+
issues="${issues} missing frontmatter: $(basename "$mf")\n"
219+
continue
220+
fi
221+
local bn
222+
bn=$(basename "$mf")
223+
if [ -n "${type_files[$ftype]+x}" ]; then
224+
type_files[$ftype]="${type_files[$ftype]}|$bn"
225+
else
226+
type_files[$ftype]="$bn"
227+
fi
228+
done
229+
230+
# Flag types with 3+ files as potential duplicates (some overlap is normal)
231+
for ftype in "${!type_files[@]}"; do
232+
local count
233+
count=$(echo "${type_files[$ftype]}" | tr '|' '\n' | wc -l | tr -d ' ')
234+
if [ "$count" -ge 3 ]; then
235+
duplicates=$((duplicates + 1))
236+
local file_list
237+
file_list=$(echo "${type_files[$ftype]}" | tr '|' ', ')
238+
issues="${issues} possible duplicates (type=$ftype, ${count} files): ${file_list}\n"
239+
fi
240+
done
241+
242+
# --- Report ---
243+
local total=$((duplicates + orphans + broken_links + missing_frontmatter))
244+
245+
emit_event "check:memory-duplicates" "{\"status\":\"$([ "$total" -gt 0 ] && echo found || echo ok)\",\"duplicates\":$duplicates,\"orphans\":$orphans,\"broken\":$broken_links,\"missing_frontmatter\":$missing_frontmatter}" || return 1
246+
247+
if [ "$total" -eq 0 ]; then
248+
echo "${GREEN}✓ Memory: ${#memory_files[@]} files, no duplicates or conflicts${NC}"
249+
else
250+
echo "${YELLOW}⚠ Memory: ${total} issue(s) found (${#memory_files[@]} files checked)${NC}"
251+
if [ -n "$issues" ]; then
252+
echo -e "$issues" | while IFS= read -r line; do
253+
[ -n "$line" ] && echo " ${YELLOW}${line}${NC}"
254+
done
255+
fi
164256
fi
165257
return 0
166258
}
@@ -202,7 +294,7 @@ suggest_commit_message() {
202294
local files_changed
203295
files_changed=$(git diff --name-only 2>/dev/null | tr '\n' ',' | sed 's/,$//')
204296

205-
local message="docs: Session cleanup — updated docs and archived MEMORY.md"
297+
local message="docs: Session cleanup — updated docs"
206298

207299
emit_event "suggest:commit" "{\"message\":\"$message\",\"files\":\"$files_changed\"}" || return 1
208300
return 0
@@ -282,7 +374,7 @@ validate_build() {
282374
do_commit() {
283375
cd "$REPO_ROOT" || fail "Cannot cd to repo root"
284376

285-
local message="docs: Session cleanup — updated docs and archived MEMORY.md"
377+
local message="docs: Session cleanup — updated docs"
286378

287379
if [ "$DRY_RUN" -eq 0 ]; then
288380
git add -A
@@ -338,7 +430,7 @@ main() {
338430

339431
check_4x4 || true
340432
check_changelog || true
341-
check_memory || true
433+
check_memory_duplicates || true
342434
check_git_state || true
343435

344436
echo ""

tools/mcp-server/src/handlers/post-flight.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ export function createPostFlightHandlers(deps: PostFlightHandlerDeps) {
9090
status: line.includes("✓") ? "ok" : line.includes("⚠") ? "stale" : "error",
9191
message: line,
9292
});
93-
} else if (line.includes("MEMORY.md")) {
93+
} else if (line.includes("Memory:")) {
9494
checks.push({
95-
check: "MEMORY.md",
96-
status: line.includes("archived") ? "ok" : "ok",
95+
check: "memory-duplicates",
96+
status: line.includes("") ? "ok" : line.includes("⚠") ? "stale" : "error",
9797
message: line,
9898
});
9999
} else if (line.includes("Build validation") || line.includes("syntax check") || line.includes("npm build") || line.includes("composer validate") || line.includes("No build validators")) {

tools/mcp-server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,7 @@ export function createServer() {
863863
"post_flight_session_cleanup",
864864
{
865865
description:
866-
"Solo developer post-flight session cleanup — ensures 4X4.md, CHANGELOG.md, and MEMORY.md are synced. Optionally commits and pushes with confirmation. Archives MEMORY.md to PROJECT/1-INBOX/ for clean sessions. Runs build validation.",
866+
"Solo developer post-flight session cleanup — checks 4X4.md and CHANGELOG.md freshness, scans Claude Code memory for conflicted duplicates (orphans, broken links, duplicate topics). Optionally commits and pushes with confirmation. Runs build validation.",
867867
inputSchema: {
868868
mode: z.enum(["report", "commit", "push"]).default("report").describe("report (default, no git actions), commit (with confirmation), or push (commit + push with confirmations)"),
869869
dryRun: z.boolean().default(false).describe("Show what would happen without executing"),

0 commit comments

Comments
 (0)