@lh8ppl/claude-memory-kit 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/bin/cmk-auto-extract.mjs +13 -0
- package/bin/cmk-compress-session.mjs +31 -17
- package/bin/cmk-inject-context.mjs +12 -2
- package/bin/cmk-weekly-curate.mjs +14 -2
- package/package.json +3 -2
- package/src/audit-log.mjs +6 -0
- package/src/auto-drain.mjs +59 -0
- package/src/auto-extract.mjs +117 -6
- package/src/auto-persona.mjs +544 -0
- package/src/bullet-lookup.mjs +59 -0
- package/src/capture-turn.mjs +54 -0
- package/src/compress-session.mjs +6 -8
- package/src/compressor.mjs +19 -4
- package/src/conflict-queue.mjs +8 -1
- package/src/daily-distill.mjs +19 -11
- package/src/doctor.mjs +74 -23
- package/src/forget.mjs +14 -0
- package/src/graduate-session.mjs +65 -0
- package/src/graduation.mjs +179 -0
- package/src/inject-context.mjs +206 -59
- package/src/install.mjs +52 -7
- package/src/lessons-promote.mjs +137 -0
- package/src/memory-write.mjs +2 -2
- package/src/native-memory.mjs +98 -0
- package/src/persona-portability.mjs +253 -0
- package/src/provenance.mjs +23 -5
- package/src/read-hook-stdin.mjs +47 -0
- package/src/register-crons.mjs +17 -8
- package/src/scratchpad.mjs +247 -19
- package/src/session-end-tasks.mjs +127 -0
- package/src/settings-hooks.mjs +33 -3
- package/src/subcommands.mjs +339 -16
- package/src/weekly-curate.mjs +53 -6
- package/src/write-fact.mjs +14 -0
- package/template/.claude/skills/memory-write/SKILL.md +47 -88
- package/template/.gitignore.fragment +6 -0
- package/template/CLAUDE.md.template +15 -9
- package/template/local/machine-paths.md.template +1 -12
- package/template/local/overrides.md.template +1 -11
- package/template/project/MEMORY.md.template +5 -26
- package/template/project/SOUL.md.template +1 -10
- package/template/user/fragments/INDEX.md.template +1 -1
- package/template/.claude/hooks/pre-tool-memory.js +0 -78
- package/template/.claude/hooks/transcript-capture.js +0 -69
- package/template/.claude/settings.json +0 -27
- package/template/support/scripts/auto-extract-memory.sh +0 -102
- package/template/support/scripts/refresh-distill-timestamp.py +0 -35
- package/template/support/scripts/register-crons.py +0 -242
- package/template/support/scripts/run-daily-distill.sh +0 -67
- package/template/support/scripts/run-weekly-curate.sh +0 -58
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
register-crons.py — reads cron/jobs/*.md and registers the active jobs with
|
|
4
|
-
the host scheduler (Task Scheduler on Windows, crontab on Unix).
|
|
5
|
-
|
|
6
|
-
Idempotent: re-running re-registers any missing tasks without disturbing existing ones.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python scripts/register-crons.py [--dry-run] [--unregister NAME]
|
|
10
|
-
|
|
11
|
-
The task name prefix defaults to the project directory's basename
|
|
12
|
-
(slugified). Override with the env var CMK_TASK_PREFIX, e.g.:
|
|
13
|
-
|
|
14
|
-
CMK_TASK_PREFIX=ytslide- python scripts/register-crons.py
|
|
15
|
-
|
|
16
|
-
Job file format (YAML frontmatter):
|
|
17
|
-
---
|
|
18
|
-
name: Daily Memory Distillation
|
|
19
|
-
time: '23:00'
|
|
20
|
-
days: daily | mon | tue | wed | thu | fri | sat | sun
|
|
21
|
-
active: 'true'
|
|
22
|
-
job_type: timestamp_refresh | shell_command | claude_prompt
|
|
23
|
-
command: 'bash scripts/run-daily-distill.sh'
|
|
24
|
-
working_directory: '${CLAUDE_PROJECT_DIR}'
|
|
25
|
-
---
|
|
26
|
-
[task body / prompt]
|
|
27
|
-
"""
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import argparse
|
|
31
|
-
import os
|
|
32
|
-
import platform
|
|
33
|
-
import re
|
|
34
|
-
import subprocess
|
|
35
|
-
import sys
|
|
36
|
-
from pathlib import Path
|
|
37
|
-
|
|
38
|
-
try:
|
|
39
|
-
import yaml
|
|
40
|
-
except ImportError:
|
|
41
|
-
print("ERROR: pyyaml not installed. Run: python -m pip install pyyaml", file=sys.stderr)
|
|
42
|
-
sys.exit(1)
|
|
43
|
-
|
|
44
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
45
|
-
JOBS_DIR = REPO_ROOT / "cron" / "jobs"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _default_prefix() -> str:
|
|
49
|
-
slug = re.sub(r"[^a-z0-9]+", "-", REPO_ROOT.name.lower()).strip("-")
|
|
50
|
-
return f"{slug}-" if slug else "cmk-"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
TASK_NAME_PREFIX = os.environ.get("CMK_TASK_PREFIX", _default_prefix())
|
|
54
|
-
|
|
55
|
-
DAY_MAP = {"daily": "DAILY", "mon": "MON", "tue": "TUE", "wed": "WED",
|
|
56
|
-
"thu": "THU", "fri": "FRI", "sat": "SAT", "sun": "SUN"}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def slugify(name: str) -> str:
|
|
60
|
-
return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def parse_job_file(path: Path) -> dict | None:
|
|
64
|
-
text = path.read_text(encoding="utf-8")
|
|
65
|
-
m = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text, re.DOTALL)
|
|
66
|
-
if not m:
|
|
67
|
-
print(f"WARN: {path.name} has no YAML frontmatter, skipping")
|
|
68
|
-
return None
|
|
69
|
-
front = yaml.safe_load(m.group(1)) or {}
|
|
70
|
-
front["__body__"] = m.group(2).strip()
|
|
71
|
-
front["__source__"] = path.name
|
|
72
|
-
return front
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _windows_bash_path() -> str | None:
|
|
76
|
-
"""Find Git Bash on Windows. Task Scheduler runs `cmd /c` with System32
|
|
77
|
-
high on PATH, so a bare `bash` command resolves to Windows' WSL bash
|
|
78
|
-
launcher (which fails if no WSL distro is configured)."""
|
|
79
|
-
candidates = [
|
|
80
|
-
Path(r"C:\Program Files\Git\usr\bin\bash.exe"),
|
|
81
|
-
Path(r"C:\Program Files\Git\bin\bash.exe"),
|
|
82
|
-
Path(r"C:\Program Files (x86)\Git\usr\bin\bash.exe"),
|
|
83
|
-
]
|
|
84
|
-
for p in candidates:
|
|
85
|
-
if p.exists():
|
|
86
|
-
return str(p)
|
|
87
|
-
return None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def build_task_command(job: dict) -> str | None:
|
|
91
|
-
jt = job.get("job_type")
|
|
92
|
-
wd = job.get("working_directory", "").replace("${CLAUDE_PROJECT_DIR}", str(REPO_ROOT))
|
|
93
|
-
if jt == "shell_command":
|
|
94
|
-
cmd = job.get("command", "")
|
|
95
|
-
if not cmd:
|
|
96
|
-
return None
|
|
97
|
-
if platform.system() == "Windows":
|
|
98
|
-
if cmd.startswith("bash "):
|
|
99
|
-
gitbash = _windows_bash_path()
|
|
100
|
-
if gitbash:
|
|
101
|
-
cmd = f'"{gitbash}" {cmd[5:]}'
|
|
102
|
-
if wd:
|
|
103
|
-
return f'cmd /c "cd /d \"{wd}\" && {cmd}"'
|
|
104
|
-
return f'cmd /c "{cmd}"'
|
|
105
|
-
if wd:
|
|
106
|
-
return f'sh -c "cd \"{wd}\" && {cmd}"'
|
|
107
|
-
return cmd
|
|
108
|
-
if jt == "timestamp_refresh":
|
|
109
|
-
helper = REPO_ROOT / "scripts" / "refresh-distill-timestamp.py"
|
|
110
|
-
py = "python" if platform.system() == "Windows" else "python3"
|
|
111
|
-
return f'{py} "{helper}"'
|
|
112
|
-
if jt == "claude_prompt":
|
|
113
|
-
msg = f"Job {job.get('name')!r} is registered but its body needs Claude CLI to run."
|
|
114
|
-
if platform.system() == "Windows":
|
|
115
|
-
return f'cmd /c "echo {msg}"'
|
|
116
|
-
return f'sh -c "echo {msg}"'
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def register_windows(job: dict, dry_run: bool) -> tuple[bool, str]:
|
|
121
|
-
name = TASK_NAME_PREFIX + slugify(job["name"])
|
|
122
|
-
time = job.get("time", "")
|
|
123
|
-
days = job.get("days", "daily")
|
|
124
|
-
cmd = build_task_command(job)
|
|
125
|
-
if not cmd:
|
|
126
|
-
return False, "no command for this job_type"
|
|
127
|
-
|
|
128
|
-
if days == "daily":
|
|
129
|
-
schedule = ["/sc", "daily"]
|
|
130
|
-
elif days in DAY_MAP:
|
|
131
|
-
schedule = ["/sc", "weekly", "/d", DAY_MAP[days]]
|
|
132
|
-
else:
|
|
133
|
-
return False, f"unknown days value: {days!r}"
|
|
134
|
-
|
|
135
|
-
args = [
|
|
136
|
-
"schtasks", "/create", "/tn", name,
|
|
137
|
-
"/tr", cmd,
|
|
138
|
-
"/st", time,
|
|
139
|
-
*schedule,
|
|
140
|
-
"/f",
|
|
141
|
-
]
|
|
142
|
-
if dry_run:
|
|
143
|
-
return True, "DRY-RUN: " + " ".join(args)
|
|
144
|
-
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
|
145
|
-
if result.returncode != 0:
|
|
146
|
-
return False, f"schtasks failed: {result.stderr.strip() or result.stdout.strip()}"
|
|
147
|
-
return True, f"registered as {name}"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def register_unix(job: dict, dry_run: bool) -> tuple[bool, str]:
|
|
151
|
-
name = TASK_NAME_PREFIX + slugify(job["name"])
|
|
152
|
-
time = job.get("time", "")
|
|
153
|
-
days = job.get("days", "daily")
|
|
154
|
-
cmd = build_task_command(job)
|
|
155
|
-
if not cmd:
|
|
156
|
-
return False, "no command for this job_type"
|
|
157
|
-
|
|
158
|
-
if not re.match(r"^\d{2}:\d{2}$", time):
|
|
159
|
-
return False, f"bad time format: {time!r}"
|
|
160
|
-
hh, mm = time.split(":")
|
|
161
|
-
|
|
162
|
-
if days == "daily":
|
|
163
|
-
day_field = "*"
|
|
164
|
-
elif days in DAY_MAP:
|
|
165
|
-
day_field = {"mon": "1", "tue": "2", "wed": "3", "thu": "4",
|
|
166
|
-
"fri": "5", "sat": "6", "sun": "0"}[days]
|
|
167
|
-
else:
|
|
168
|
-
return False, f"unknown days value: {days!r}"
|
|
169
|
-
|
|
170
|
-
cron_line = f"{mm} {hh} * * {day_field} {cmd} # {name}"
|
|
171
|
-
if dry_run:
|
|
172
|
-
return True, "DRY-RUN: would append to crontab: " + cron_line
|
|
173
|
-
|
|
174
|
-
existing = subprocess.run(["crontab", "-l"], capture_output=True, text=True).stdout
|
|
175
|
-
if name in existing:
|
|
176
|
-
return True, f"{name} already in crontab"
|
|
177
|
-
new = existing.rstrip() + "\n" + cron_line + "\n"
|
|
178
|
-
proc = subprocess.run(["crontab", "-"], input=new, text=True, capture_output=True)
|
|
179
|
-
if proc.returncode != 0:
|
|
180
|
-
return False, f"crontab failed: {proc.stderr.strip()}"
|
|
181
|
-
return True, f"appended {name}"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def main() -> int:
|
|
185
|
-
p = argparse.ArgumentParser()
|
|
186
|
-
p.add_argument("--dry-run", action="store_true", help="Print what would happen without changing anything")
|
|
187
|
-
p.add_argument("--unregister", help="Remove a task by name (with or without prefix)")
|
|
188
|
-
args = p.parse_args()
|
|
189
|
-
|
|
190
|
-
if args.unregister:
|
|
191
|
-
name = args.unregister if args.unregister.startswith(TASK_NAME_PREFIX) else TASK_NAME_PREFIX + slugify(args.unregister)
|
|
192
|
-
if platform.system() == "Windows":
|
|
193
|
-
r = subprocess.run(["schtasks", "/delete", "/tn", name, "/f"], capture_output=True, text=True)
|
|
194
|
-
print(r.stdout or r.stderr)
|
|
195
|
-
return r.returncode
|
|
196
|
-
existing = subprocess.run(["crontab", "-l"], capture_output=True, text=True).stdout
|
|
197
|
-
new = "\n".join(line for line in existing.splitlines() if name not in line) + "\n"
|
|
198
|
-
subprocess.run(["crontab", "-"], input=new, text=True)
|
|
199
|
-
print(f"Removed lines matching {name}")
|
|
200
|
-
return 0
|
|
201
|
-
|
|
202
|
-
jobs = sorted(JOBS_DIR.glob("*.md"))
|
|
203
|
-
if not jobs:
|
|
204
|
-
print(f"No job files found in {JOBS_DIR}")
|
|
205
|
-
return 0
|
|
206
|
-
|
|
207
|
-
print(f"Found {len(jobs)} job file(s) in {JOBS_DIR}")
|
|
208
|
-
print(f"Platform: {platform.system()}; using {'schtasks' if platform.system() == 'Windows' else 'crontab'}")
|
|
209
|
-
print(f"Task name prefix: {TASK_NAME_PREFIX}")
|
|
210
|
-
print()
|
|
211
|
-
|
|
212
|
-
n_ok = 0
|
|
213
|
-
n_skip = 0
|
|
214
|
-
n_fail = 0
|
|
215
|
-
for path in jobs:
|
|
216
|
-
job = parse_job_file(path)
|
|
217
|
-
if not job:
|
|
218
|
-
continue
|
|
219
|
-
name = job.get("name", "(unnamed)")
|
|
220
|
-
active = str(job.get("active", "")).lower() == "true"
|
|
221
|
-
if not active:
|
|
222
|
-
print(f"SKIP {name} (active: false)")
|
|
223
|
-
n_skip += 1
|
|
224
|
-
continue
|
|
225
|
-
if platform.system() == "Windows":
|
|
226
|
-
ok, msg = register_windows(job, args.dry_run)
|
|
227
|
-
else:
|
|
228
|
-
ok, msg = register_unix(job, args.dry_run)
|
|
229
|
-
marker = "OK" if ok else "FAIL"
|
|
230
|
-
print(f"{marker:6s} {name}: {msg}")
|
|
231
|
-
if ok:
|
|
232
|
-
n_ok += 1
|
|
233
|
-
else:
|
|
234
|
-
n_fail += 1
|
|
235
|
-
|
|
236
|
-
print()
|
|
237
|
-
print(f"Done. registered: {n_ok}, skipped: {n_skip}, failed: {n_fail}")
|
|
238
|
-
return 0 if n_fail == 0 else 1
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if __name__ == "__main__":
|
|
242
|
-
sys.exit(main())
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Daily memory distill — invokes Claude headlessly with a prompt that reads
|
|
4
|
-
# today's session log and extracts durable facts into context/MEMORY.md.
|
|
5
|
-
#
|
|
6
|
-
# Wired by cron/jobs/daily-memory-distill.md → schtasks/cron at 23:00 daily.
|
|
7
|
-
# Logs to context/sessions/{today}.md under a "## Automated distill" heading
|
|
8
|
-
# so the work is traceable.
|
|
9
|
-
|
|
10
|
-
# When run from Task Scheduler / cron / launchd, PATH may not include the
|
|
11
|
-
# directories where bash builtins or Claude CLI live. Set up explicitly.
|
|
12
|
-
case ":$PATH:" in
|
|
13
|
-
*":/usr/bin:"*) ;;
|
|
14
|
-
*) export PATH="/usr/bin:/usr/local/bin:/opt/homebrew/bin:/c/Program Files/Git/usr/bin:$PATH" ;;
|
|
15
|
-
esac
|
|
16
|
-
|
|
17
|
-
set -euo pipefail
|
|
18
|
-
|
|
19
|
-
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
20
|
-
cd "$REPO_ROOT"
|
|
21
|
-
|
|
22
|
-
TODAY=$(date +%Y-%m-%d)
|
|
23
|
-
SESSION_LOG="context/sessions/${TODAY}.md"
|
|
24
|
-
|
|
25
|
-
if [ ! -f "$SESSION_LOG" ]; then
|
|
26
|
-
echo "[$(date -Iseconds)] daily-distill: no session log at $SESSION_LOG; nothing to distill" >&2
|
|
27
|
-
# Still refresh the timestamp so HC-3 stays green.
|
|
28
|
-
python "${REPO_ROOT}/scripts/refresh-distill-timestamp.py" || true
|
|
29
|
-
exit 0
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
PROMPT=$(cat <<EOF
|
|
33
|
-
You are running as a scheduled background task at $(date -Iseconds) for this project. No human is watching. Be concise and idempotent.
|
|
34
|
-
|
|
35
|
-
GOAL: distill durable facts from today's session log into the bounded scratchpad.
|
|
36
|
-
|
|
37
|
-
STEPS:
|
|
38
|
-
1. Read \`${SESSION_LOG}\`. This is today's session log with deliverables, decisions, and open threads.
|
|
39
|
-
2. Read \`context/MEMORY.md\` to see what's already captured (active threads, environment notes, pending decisions).
|
|
40
|
-
3. Read \`context/memory/INDEX.md\` to see what's already in the granular archive.
|
|
41
|
-
4. For each meaningful item in the session log:
|
|
42
|
-
- If it's transient working state (current discussion, today's iteration) → if not already in MEMORY.md "Active Threads", add a one-line bullet there.
|
|
43
|
-
- If it's a durable typed fact (user preference learned, project decision made, feedback rule, external reference) → create or update a granular file at \`context/memory/<type>_<slug>.md\` with frontmatter + Why + How to apply. Add a one-line entry to INDEX.md. Don't duplicate in MEMORY.md.
|
|
44
|
-
- If it's an environment change (new tool installed, path discovered) → MEMORY.md "Environment Notes".
|
|
45
|
-
- If it's a decision the user still needs to make → MEMORY.md "Pending Decisions".
|
|
46
|
-
5. Before writing MEMORY.md, run \`wc -c context/MEMORY.md\`. If over 2500 chars, FIRST consolidate by merging similar bullets and dropping stale ones (anything older than 14 days that has no current relevance). Then add the new content.
|
|
47
|
-
6. Update the \`<!-- Last distilled: YYYY-MM-DD -->\` line at the top of MEMORY.md to ${TODAY}.
|
|
48
|
-
7. Append a one-line entry to \`${SESSION_LOG}\` under a new "## Automated distill" heading describing what changed.
|
|
49
|
-
|
|
50
|
-
CONSTRAINTS:
|
|
51
|
-
- Use the Read, Edit, and Bash tools only. Do NOT use Write to overwrite MEMORY.md — use Edit so the metadata comment is preserved.
|
|
52
|
-
- Do NOT create new files outside context/memory/ or modify anything outside context/.
|
|
53
|
-
- Do NOT commit, push, or run git operations.
|
|
54
|
-
- If the session log has nothing distillable, still update the timestamp on MEMORY.md so HC-3 stays green, and append a "## Automated distill" note saying "nothing new to distill."
|
|
55
|
-
- Keep total output under 3 paragraphs.
|
|
56
|
-
EOF
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
echo "$PROMPT" | claude --print \
|
|
60
|
-
--add-dir "$REPO_ROOT" \
|
|
61
|
-
--allowed-tools "Read" "Edit" "Bash(wc *)" "Bash(date *)" \
|
|
62
|
-
--output-format text \
|
|
63
|
-
2>&1 | tee -a "${REPO_ROOT}/context/sessions/${TODAY}.distill.log"
|
|
64
|
-
|
|
65
|
-
EXIT=${PIPESTATUS[0]}
|
|
66
|
-
echo "[$(date -Iseconds)] daily-distill: exit=$EXIT" >&2
|
|
67
|
-
exit "$EXIT"
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Weekly memory curator — invokes Claude headlessly with a prompt that
|
|
4
|
-
# prunes, merges, and consolidates entries in context/MEMORY.md.
|
|
5
|
-
#
|
|
6
|
-
# Wired by cron/jobs/weekly-memory-curator.md → schtasks/cron at Sun 09:00.
|
|
7
|
-
|
|
8
|
-
case ":$PATH:" in
|
|
9
|
-
*":/usr/bin:"*) ;;
|
|
10
|
-
*) export PATH="/usr/bin:/usr/local/bin:/opt/homebrew/bin:/c/Program Files/Git/usr/bin:$PATH" ;;
|
|
11
|
-
esac
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
16
|
-
cd "$REPO_ROOT"
|
|
17
|
-
|
|
18
|
-
TODAY=$(date +%Y-%m-%d)
|
|
19
|
-
SESSION_LOG="context/sessions/${TODAY}.md"
|
|
20
|
-
|
|
21
|
-
PROMPT=$(cat <<EOF
|
|
22
|
-
You are running as a scheduled weekly background task at $(date -Iseconds) for this project. No human is watching. Be conservative — if unsure whether something should be kept, KEEP IT.
|
|
23
|
-
|
|
24
|
-
GOAL: prune, merge, and consolidate \`context/MEMORY.md\` so it stays informative and well under the 2500-char cap.
|
|
25
|
-
|
|
26
|
-
STEPS:
|
|
27
|
-
1. Read \`context/MEMORY.md\` end to end.
|
|
28
|
-
2. Read \`context/memory/INDEX.md\` and at least the most recent 3 session logs (\`ls -t context/sessions/ | head -3\`) for context on what's currently relevant.
|
|
29
|
-
3. For each entry in MEMORY.md "Active Threads":
|
|
30
|
-
- Is it still being worked on? Check session logs. If resolved → REMOVE.
|
|
31
|
-
- Are there duplicates? → MERGE into one.
|
|
32
|
-
4. For each "Environment Note":
|
|
33
|
-
- Is it still accurate? Drop only if clearly stale.
|
|
34
|
-
5. For each "Pending Decision":
|
|
35
|
-
- Has it been resolved (look for matching deliverables in recent session logs)? → REMOVE.
|
|
36
|
-
- Has new context arrived? → UPDATE the bullet to reflect current state.
|
|
37
|
-
6. After curating, run \`wc -c context/MEMORY.md\` and report the new size.
|
|
38
|
-
7. Update the \`<!-- Last health check: YYYY-MM-DD -->\` line to ${TODAY}.
|
|
39
|
-
8. If today doesn't have a session log yet, create \`${SESSION_LOG}\` with a "## Session — automated curation" heading. Append a summary of what was changed (e.g., "Removed 2 resolved threads, merged 3 environment-notes duplicates, new size 1840/2500 chars.").
|
|
40
|
-
|
|
41
|
-
CONSTRAINTS:
|
|
42
|
-
- Use Read, Edit, and Bash(wc *), Bash(ls *), Bash(date *) only.
|
|
43
|
-
- NEVER add new content during curation — this is a cleanup pass, not a writing pass. If you find something missing, NOTE it in the session log but don't add to MEMORY.md.
|
|
44
|
-
- Do NOT touch granular archive files in context/memory/<type>_*.md unless they're obviously orphaned (no INDEX entry AND no references in MEMORY.md AND no recent session log mention).
|
|
45
|
-
- Do NOT commit, push, or run git operations.
|
|
46
|
-
- Keep total output under 5 lines: summary of changes only.
|
|
47
|
-
EOF
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
echo "$PROMPT" | claude --print \
|
|
51
|
-
--add-dir "$REPO_ROOT" \
|
|
52
|
-
--allowed-tools "Read" "Edit" "Bash(wc *)" "Bash(ls *)" "Bash(date *)" \
|
|
53
|
-
--output-format text \
|
|
54
|
-
2>&1 | tee -a "${REPO_ROOT}/context/sessions/${TODAY}.curate.log"
|
|
55
|
-
|
|
56
|
-
EXIT=${PIPESTATUS[0]}
|
|
57
|
-
echo "[$(date -Iseconds)] weekly-curate: exit=$EXIT" >&2
|
|
58
|
-
exit "$EXIT"
|