get-claudia 1.55.21 → 1.57.0
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/CHANGELOG.md +79 -0
- package/bin/index.js +213 -5
- package/bin/manifest-lib.js +245 -0
- package/memory-daemon/claudia_memory/daemon/health.py +1 -1
- package/memory-daemon/claudia_memory/daemon/scheduler.py +1 -1
- package/memory-daemon/claudia_memory/mcp/server.py +132 -123
- package/memory-daemon/claudia_memory/services/consolidate.py +1 -1
- package/memory-daemon/claudia_memory/services/remember.py +1 -1
- package/package.json +6 -2
- package/template-v2/.claude/hooks/__pycache__/post-tool-capture.cpython-313.pyc +0 -0
- package/template-v2/.claude/hooks/__pycache__/session-health-check.cpython-313.pyc +0 -0
- package/template-v2/.claude/hooks/__pycache__/user-prompt-capture.cpython-313.pyc +0 -0
- package/template-v2/.claude/hooks/hooks.json +11 -11
- package/template-v2/.claude/hooks/post-tool-capture.py +110 -10
- package/template-v2/.claude/hooks/pre-compact.py +4 -4
- package/template-v2/.claude/hooks/pre-compact.sh +1 -1
- package/template-v2/.claude/hooks/session-health-check.py +52 -4
- package/template-v2/.claude/hooks/session-summary.py +399 -0
- package/template-v2/.claude/hooks/user-prompt-capture.py +123 -0
- package/template-v2/.claude/manifest.json +73 -0
- package/template-v2/.claude/rules/claudia-principles.md +2 -2
- package/template-v2/.claude/rules/memory-availability.md +3 -3
- package/template-v2/.claude/rules/memory-commitment.md +92 -0
- package/template-v2/.claude/settings.local.json +26 -0
- package/template-v2/.claude/skills/capture-meeting/SKILL.md +6 -6
- package/template-v2/.claude/skills/capture-meeting/evals/basic.yaml +1 -1
- package/template-v2/.claude/skills/deep-context/SKILL.md +7 -7
- package/template-v2/.claude/skills/meditate/SKILL.md +10 -10
- package/template-v2/.claude/skills/meditate/evals/basic.yaml +1 -1
- package/template-v2/.claude/skills/meeting-prep/SKILL.md +3 -3
- package/template-v2/.claude/skills/memory-health/SKILL.md +1 -1
- package/template-v2/.claude/skills/memory-manager.md +85 -85
- package/template-v2/.claude/skills/morning-brief/SKILL.md +10 -10
- package/template-v2/.claude/skills/research/SKILL.md +2 -2
- package/template-v2/.claude/skills/skill-index.json +1 -1
- package/template-v2/CLAUDE.md +6 -6
- package/template-v2/.claude/hooks/__pycache__/pre-compact.cpython-313.pyc +0 -0
- package/template-v2/gitignore +0 -35
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a daily session summary markdown file from observations.jsonl.
|
|
3
|
+
|
|
4
|
+
Designed to run from a SessionEnd hook OR manually for retrospective summaries.
|
|
5
|
+
|
|
6
|
+
Output path: ~/.claudia/sessions/YYYY-MM-DD/NN-slug.md
|
|
7
|
+
Plus an INDEX.md per day, auto-regenerated.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
session-summary.py # uses CLAUDE_SESSION_ID env var or stdin JSON
|
|
11
|
+
session-summary.py <session_id> # explicit session id
|
|
12
|
+
session-summary.py --rebuild-index # regenerate today's INDEX.md only
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from collections import Counter
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
OBS_FILE = Path.home() / ".claudia" / "observations.jsonl"
|
|
25
|
+
SESSIONS_DIR = Path.home() / ".claudia" / "sessions"
|
|
26
|
+
|
|
27
|
+
# Words to filter out when deriving topic slugs
|
|
28
|
+
STOPWORDS = {
|
|
29
|
+
"the", "a", "an", "and", "or", "but", "if", "of", "to", "for", "with",
|
|
30
|
+
"in", "on", "at", "by", "is", "are", "was", "were", "be", "been", "being",
|
|
31
|
+
"i", "you", "we", "us", "they", "this", "that", "these", "those",
|
|
32
|
+
"have", "has", "had", "do", "does", "did", "can", "could", "will",
|
|
33
|
+
"would", "should", "may", "might", "must", "let", "let's", "ok", "okay",
|
|
34
|
+
"yes", "no", "now", "go", "ahead", "please", "thanks", "today",
|
|
35
|
+
"use", "make", "get", "see", "look", "want",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_observations(session_id: str = None) -> list[dict]:
|
|
40
|
+
"""Load observations from the JSONL file, optionally filtered by session_id."""
|
|
41
|
+
if not OBS_FILE.exists():
|
|
42
|
+
return []
|
|
43
|
+
obs = []
|
|
44
|
+
try:
|
|
45
|
+
with open(OBS_FILE, "r", encoding="utf-8") as f:
|
|
46
|
+
for line in f:
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if not line:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
o = json.loads(line)
|
|
52
|
+
if session_id and o.get("session_id") != session_id:
|
|
53
|
+
continue
|
|
54
|
+
obs.append(o)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
continue
|
|
57
|
+
except OSError:
|
|
58
|
+
return []
|
|
59
|
+
return obs
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def first_user_prompt(transcript_path: str) -> str | None:
|
|
63
|
+
"""Try to extract the first user prompt from a transcript file (JSONL of turns)."""
|
|
64
|
+
if not transcript_path or not Path(transcript_path).exists():
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
with open(transcript_path, "r", encoding="utf-8") as f:
|
|
68
|
+
for line in f:
|
|
69
|
+
try:
|
|
70
|
+
turn = json.loads(line)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
continue
|
|
73
|
+
role = turn.get("role") or turn.get("type")
|
|
74
|
+
if role == "user":
|
|
75
|
+
content = turn.get("content") or turn.get("text") or ""
|
|
76
|
+
if isinstance(content, list):
|
|
77
|
+
for block in content:
|
|
78
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
79
|
+
return block.get("text", "")[:500]
|
|
80
|
+
elif isinstance(content, str):
|
|
81
|
+
return content[:500]
|
|
82
|
+
except OSError:
|
|
83
|
+
return None
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def derive_topic_slug(observations: list[dict], first_prompt: str | None) -> str:
|
|
88
|
+
"""Derive a 2-4 word topic slug from observations and first prompt."""
|
|
89
|
+
text_pool = []
|
|
90
|
+
|
|
91
|
+
if first_prompt:
|
|
92
|
+
text_pool.append(first_prompt)
|
|
93
|
+
|
|
94
|
+
for o in observations:
|
|
95
|
+
if o.get("file_path"):
|
|
96
|
+
parts = Path(o["file_path"]).parts
|
|
97
|
+
if len(parts) >= 2:
|
|
98
|
+
text_pool.append(parts[-2])
|
|
99
|
+
|
|
100
|
+
text_blob = " ".join(text_pool).lower()
|
|
101
|
+
words = re.findall(r"[a-z]{3,}", text_blob)
|
|
102
|
+
words = [w for w in words if w not in STOPWORDS]
|
|
103
|
+
|
|
104
|
+
if not words:
|
|
105
|
+
return "session"
|
|
106
|
+
|
|
107
|
+
counter = Counter(words)
|
|
108
|
+
top_words = [w for w, _ in counter.most_common(4)]
|
|
109
|
+
slug = "-".join(top_words[:3]) if top_words else "session"
|
|
110
|
+
return slug[:60]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def session_window(observations: list[dict]) -> tuple[float, float]:
|
|
114
|
+
"""Return (start_ts, end_ts) from observation timestamps."""
|
|
115
|
+
if not observations:
|
|
116
|
+
return (time.time(), time.time())
|
|
117
|
+
timestamps = [o.get("ts", 0) for o in observations if o.get("ts")]
|
|
118
|
+
if not timestamps:
|
|
119
|
+
return (time.time(), time.time())
|
|
120
|
+
return (min(timestamps), max(timestamps))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def files_touched(observations: list[dict]) -> list[str]:
|
|
124
|
+
"""Return unique file paths touched in this session, in order of first appearance."""
|
|
125
|
+
seen = []
|
|
126
|
+
seen_set = set()
|
|
127
|
+
for o in observations:
|
|
128
|
+
fp = o.get("file_path")
|
|
129
|
+
if fp and fp not in seen_set:
|
|
130
|
+
seen.append(fp)
|
|
131
|
+
seen_set.add(fp)
|
|
132
|
+
return seen
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def external_actions(observations: list[dict]) -> list[dict]:
|
|
136
|
+
"""Return observations flagged with external_action."""
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
"ts": o.get("ts"),
|
|
140
|
+
"tool": o.get("tool"),
|
|
141
|
+
"action": o.get("external_action"),
|
|
142
|
+
"input": o.get("input", "")[:200],
|
|
143
|
+
}
|
|
144
|
+
for o in observations if o.get("external_action")
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def memory_entries_in_window(start_ts: float, end_ts: float) -> list[dict]:
|
|
149
|
+
"""Best-effort fetch of memory entries created during the session window.
|
|
150
|
+
|
|
151
|
+
Relies on the claudia-memory daemon's HTTP endpoint if available.
|
|
152
|
+
Returns empty list if not reachable. The daemon does not currently
|
|
153
|
+
expose a /recent_memories endpoint; this is a placeholder for a
|
|
154
|
+
future enhancement.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
import urllib.request
|
|
158
|
+
url = f"http://localhost:3848/recent_memories?since={start_ts}&until={end_ts}"
|
|
159
|
+
req = urllib.request.Request(url)
|
|
160
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
161
|
+
return json.loads(resp.read().decode("utf-8")).get("memories", [])
|
|
162
|
+
except Exception:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def next_session_number(date_dir: Path) -> int:
|
|
167
|
+
"""Return the next sequential session number for the day."""
|
|
168
|
+
if not date_dir.exists():
|
|
169
|
+
return 1
|
|
170
|
+
existing = []
|
|
171
|
+
for f in date_dir.glob("[0-9][0-9]-*.md"):
|
|
172
|
+
match = re.match(r"^(\d{2})-", f.name)
|
|
173
|
+
if match:
|
|
174
|
+
existing.append(int(match.group(1)))
|
|
175
|
+
return (max(existing) + 1) if existing else 1
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_summary(
|
|
179
|
+
session_id: str,
|
|
180
|
+
date_str: str,
|
|
181
|
+
session_num: int,
|
|
182
|
+
topic_slug: str,
|
|
183
|
+
start_ts: float,
|
|
184
|
+
end_ts: float,
|
|
185
|
+
files: list[str],
|
|
186
|
+
actions: list[dict],
|
|
187
|
+
memories: list[dict],
|
|
188
|
+
first_prompt: str | None,
|
|
189
|
+
transcript_path: str | None,
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Render the session summary markdown."""
|
|
192
|
+
duration_min = max(1, round((end_ts - start_ts) / 60))
|
|
193
|
+
started = datetime.fromtimestamp(start_ts, tz=timezone.utc).astimezone()
|
|
194
|
+
ended = datetime.fromtimestamp(end_ts, tz=timezone.utc).astimezone()
|
|
195
|
+
|
|
196
|
+
lines = [
|
|
197
|
+
f"# Session {session_num:02d} — {topic_slug.replace('-', ' ').title()}",
|
|
198
|
+
"",
|
|
199
|
+
f"**Date:** {date_str}",
|
|
200
|
+
f"**Started:** {started.strftime('%H:%M %Z')}",
|
|
201
|
+
f"**Ended:** {ended.strftime('%H:%M %Z')}",
|
|
202
|
+
f"**Duration:** ~{duration_min} min",
|
|
203
|
+
f"**Session ID:** `{session_id}`",
|
|
204
|
+
"",
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
if first_prompt:
|
|
208
|
+
lines += [
|
|
209
|
+
"## Opening prompt",
|
|
210
|
+
"",
|
|
211
|
+
"> " + first_prompt.strip().split("\n")[0][:300],
|
|
212
|
+
"",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
lines += ["## Files touched", ""]
|
|
216
|
+
if files:
|
|
217
|
+
for f in files:
|
|
218
|
+
lines.append(f"- `{f}`")
|
|
219
|
+
else:
|
|
220
|
+
lines.append("- (none)")
|
|
221
|
+
lines.append("")
|
|
222
|
+
|
|
223
|
+
lines += ["## External actions", ""]
|
|
224
|
+
if actions:
|
|
225
|
+
for a in actions:
|
|
226
|
+
lines.append(f"- **{a.get('action')}** via `{a.get('tool')}` — `{a.get('input')[:120]}`")
|
|
227
|
+
else:
|
|
228
|
+
lines.append("- (none)")
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
lines += ["## Memory entries created", ""]
|
|
232
|
+
if memories:
|
|
233
|
+
for m in memories[:30]:
|
|
234
|
+
mid = m.get("id") or m.get("memory_id") or "?"
|
|
235
|
+
content = (m.get("content") or "")[:200]
|
|
236
|
+
lines.append(f"- `mem-{mid}` — {content}")
|
|
237
|
+
else:
|
|
238
|
+
lines.append("- (none captured — memory daemon may not expose recent_memories endpoint)")
|
|
239
|
+
lines.append("")
|
|
240
|
+
|
|
241
|
+
if transcript_path:
|
|
242
|
+
lines += [
|
|
243
|
+
"## Find this again",
|
|
244
|
+
"",
|
|
245
|
+
f"- Transcript: `{transcript_path}`",
|
|
246
|
+
f"- Memory query: `\"{topic_slug.replace('-', ' ')}\"`",
|
|
247
|
+
"",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
lines += [
|
|
251
|
+
"---",
|
|
252
|
+
f"*Auto-generated by session-summary.py at {datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z')}*",
|
|
253
|
+
"",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return "\n".join(lines)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def regenerate_index(date_dir: Path) -> None:
|
|
260
|
+
"""Regenerate INDEX.md for a given day's folder."""
|
|
261
|
+
if not date_dir.exists():
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
sessions = []
|
|
265
|
+
for f in sorted(date_dir.glob("[0-9][0-9]-*.md")):
|
|
266
|
+
first_lines = f.read_text(encoding="utf-8").split("\n")[:6]
|
|
267
|
+
title = first_lines[0].lstrip("# ").strip() if first_lines else f.name
|
|
268
|
+
started = ""
|
|
269
|
+
for line in first_lines:
|
|
270
|
+
if line.startswith("**Started:**"):
|
|
271
|
+
started = line.replace("**Started:**", "").strip()
|
|
272
|
+
break
|
|
273
|
+
sessions.append((f.name, title, started))
|
|
274
|
+
|
|
275
|
+
date_str = date_dir.name
|
|
276
|
+
lines = [
|
|
277
|
+
f"# Sessions — {date_str}",
|
|
278
|
+
"",
|
|
279
|
+
f"*{len(sessions)} session(s) captured.*",
|
|
280
|
+
"",
|
|
281
|
+
"| # | Topic | Started | File |",
|
|
282
|
+
"|---|-------|---------|------|",
|
|
283
|
+
]
|
|
284
|
+
for fname, title, started in sessions:
|
|
285
|
+
match = re.match(r"^(\d{2})-", fname)
|
|
286
|
+
num = match.group(1) if match else "??"
|
|
287
|
+
clean_title = title.split("—", 1)[1].strip() if "—" in title else title
|
|
288
|
+
lines.append(f"| {num} | {clean_title} | {started} | [`{fname}`](./{fname}) |")
|
|
289
|
+
lines += [
|
|
290
|
+
"",
|
|
291
|
+
f"*INDEX regenerated {datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %Z')}*",
|
|
292
|
+
"",
|
|
293
|
+
]
|
|
294
|
+
(date_dir / "INDEX.md").write_text("\n".join(lines), encoding="utf-8")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main():
|
|
298
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--rebuild-index":
|
|
299
|
+
date_str = sys.argv[2] if len(sys.argv) > 2 else datetime.now().astimezone().strftime("%Y-%m-%d")
|
|
300
|
+
regenerate_index(SESSIONS_DIR / date_str)
|
|
301
|
+
print(json.dumps({"action": "rebuild-index", "date": date_str}))
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
session_id = ""
|
|
305
|
+
transcript_path = ""
|
|
306
|
+
|
|
307
|
+
if len(sys.argv) > 1:
|
|
308
|
+
session_id = sys.argv[1]
|
|
309
|
+
else:
|
|
310
|
+
session_id = os.environ.get("CLAUDE_SESSION_ID", "")
|
|
311
|
+
# Try stdin (SessionEnd hook contract)
|
|
312
|
+
if not session_id and not sys.stdin.isatty():
|
|
313
|
+
try:
|
|
314
|
+
raw = sys.stdin.read()
|
|
315
|
+
if raw.strip():
|
|
316
|
+
payload = json.loads(raw)
|
|
317
|
+
session_id = payload.get("session_id", "")
|
|
318
|
+
transcript_path = payload.get("transcript_path", "")
|
|
319
|
+
except (json.JSONDecodeError, OSError):
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
if not session_id:
|
|
323
|
+
print(json.dumps({"error": "no session_id provided"}))
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
observations = load_observations(session_id)
|
|
327
|
+
if not observations:
|
|
328
|
+
print(json.dumps({"warning": "no observations for session", "session_id": session_id}))
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
start_ts, end_ts = session_window(observations)
|
|
332
|
+
date_str = datetime.fromtimestamp(start_ts, tz=timezone.utc).astimezone().strftime("%Y-%m-%d")
|
|
333
|
+
date_dir = SESSIONS_DIR / date_str
|
|
334
|
+
date_dir.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
|
|
336
|
+
first_prompt = first_user_prompt(transcript_path) if transcript_path else None
|
|
337
|
+
topic_slug = derive_topic_slug(observations, first_prompt)
|
|
338
|
+
|
|
339
|
+
# Check if a summary for this session already exists; overwrite with latest data
|
|
340
|
+
existing_file = None
|
|
341
|
+
session_num = next_session_number(date_dir)
|
|
342
|
+
for existing in date_dir.glob("[0-9][0-9]-*.md"):
|
|
343
|
+
try:
|
|
344
|
+
content = existing.read_text(encoding="utf-8")
|
|
345
|
+
if f"`{session_id}`" in content:
|
|
346
|
+
existing_file = existing
|
|
347
|
+
match = re.match(r"^(\d{2})-", existing.name)
|
|
348
|
+
if match:
|
|
349
|
+
session_num = int(match.group(1))
|
|
350
|
+
break
|
|
351
|
+
except OSError:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
files = files_touched(observations)
|
|
355
|
+
actions = external_actions(observations)
|
|
356
|
+
memories = memory_entries_in_window(start_ts, end_ts)
|
|
357
|
+
|
|
358
|
+
summary = render_summary(
|
|
359
|
+
session_id=session_id,
|
|
360
|
+
date_str=date_str,
|
|
361
|
+
session_num=session_num,
|
|
362
|
+
topic_slug=topic_slug,
|
|
363
|
+
start_ts=start_ts,
|
|
364
|
+
end_ts=end_ts,
|
|
365
|
+
files=files,
|
|
366
|
+
actions=actions,
|
|
367
|
+
memories=memories,
|
|
368
|
+
first_prompt=first_prompt,
|
|
369
|
+
transcript_path=transcript_path,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
out_file = date_dir / f"{session_num:02d}-{topic_slug}.md"
|
|
373
|
+
|
|
374
|
+
# If existing file had a different topic slug, remove the stale name
|
|
375
|
+
if existing_file and existing_file != out_file:
|
|
376
|
+
try:
|
|
377
|
+
existing_file.unlink()
|
|
378
|
+
except OSError:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
out_file.write_text(summary, encoding="utf-8")
|
|
382
|
+
regenerate_index(date_dir)
|
|
383
|
+
|
|
384
|
+
print(json.dumps({
|
|
385
|
+
"ok": True,
|
|
386
|
+
"session_id": session_id,
|
|
387
|
+
"file": str(out_file),
|
|
388
|
+
"files_touched": len(files),
|
|
389
|
+
"external_actions": len(actions),
|
|
390
|
+
"memories_in_window": len(memories),
|
|
391
|
+
"updated": existing_file is not None,
|
|
392
|
+
}))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
if __name__ == "__main__":
|
|
396
|
+
try:
|
|
397
|
+
main()
|
|
398
|
+
except Exception as e:
|
|
399
|
+
print(json.dumps({"error": str(e)[:200]}))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit hook: detect canonical-fact trigger phrases and destructive
|
|
3
|
+
verbs in user prompts, inject reminder context for the agent.
|
|
4
|
+
|
|
5
|
+
Reads hook payload from stdin (Claude Code hook contract).
|
|
6
|
+
Outputs JSON with additionalContext when triggers fire. Outputs nothing otherwise.
|
|
7
|
+
|
|
8
|
+
Designed to complete in <5ms (regex match + simple JSON output, no network).
|
|
9
|
+
|
|
10
|
+
Two trigger classes:
|
|
11
|
+
1. Memory-commitment phrases ("lock this in", "remember this", "this is canonical")
|
|
12
|
+
-> Inject reminder for the agent to save the fact to memory immediately,
|
|
13
|
+
per the memory-commitment rule (which governs tool selection).
|
|
14
|
+
2. Destructive operation patterns (rm -rf, drop table, force push, etc.)
|
|
15
|
+
-> Inject "verify before acting" reminder per Claudia's safety-first principle.
|
|
16
|
+
|
|
17
|
+
Both classes can fire on the same prompt; both messages are concatenated.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
# Trigger phrases that signal the user is asserting a canonical fact.
|
|
25
|
+
# Word boundaries (\b) used to avoid matching inside other words.
|
|
26
|
+
COMMITMENT_TRIGGERS = [
|
|
27
|
+
r"\block this in\b",
|
|
28
|
+
r"\bremember this\b",
|
|
29
|
+
r"\bthis is canonical\b",
|
|
30
|
+
r"\bthis is locked\b",
|
|
31
|
+
r"\bsave this for later\b",
|
|
32
|
+
r"\bimportant to remember\b",
|
|
33
|
+
r"\bfor the record\b",
|
|
34
|
+
r"\bdon'?t forget\b",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Destructive operation patterns. Narrow matches to limit false positives.
|
|
38
|
+
# Conversation about deletion ("how do I undo a delete?") should NOT fire these.
|
|
39
|
+
DESTRUCTIVE_PATTERNS = [
|
|
40
|
+
r"\brm\s+-rf\b",
|
|
41
|
+
r"\bdrop\s+(table|database|schema)\b",
|
|
42
|
+
r"\bgit\s+push\s+(?:-+f\b|--force\b)",
|
|
43
|
+
r"\bgit\s+reset\s+--hard\b",
|
|
44
|
+
r"\btruncate\s+table\b",
|
|
45
|
+
r"\bDELETE\s+FROM\b", # SQL all-caps signals intent
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Human-readable labels for each destructive pattern. Keep keys in lockstep
|
|
49
|
+
# with DESTRUCTIVE_PATTERNS so the model never sees raw regex syntax.
|
|
50
|
+
PATTERN_LABELS = {
|
|
51
|
+
r"\brm\s+-rf\b": "rm -rf (recursive delete)",
|
|
52
|
+
r"\bdrop\s+(table|database|schema)\b": "DROP TABLE/DATABASE/SCHEMA",
|
|
53
|
+
r"\bgit\s+push\s+(?:-+f\b|--force\b)": "git push --force",
|
|
54
|
+
r"\bgit\s+reset\s+--hard\b": "git reset --hard",
|
|
55
|
+
r"\btruncate\s+table\b": "TRUNCATE TABLE",
|
|
56
|
+
r"\bDELETE\s+FROM\b": "DELETE FROM (SQL)",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Fail-fast at import time if a pattern is missing a label (or vice versa).
|
|
60
|
+
assert set(DESTRUCTIVE_PATTERNS) == set(PATTERN_LABELS), (
|
|
61
|
+
"Every DESTRUCTIVE_PATTERN must have a matching PATTERN_LABELS entry"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def detect(text: str, patterns: list) -> list:
|
|
66
|
+
"""Return list of pattern strings that matched in the text."""
|
|
67
|
+
return [p for p in patterns if re.search(p, text, re.IGNORECASE)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
try:
|
|
72
|
+
raw = sys.stdin.read()
|
|
73
|
+
if not raw.strip():
|
|
74
|
+
return
|
|
75
|
+
payload = json.loads(raw)
|
|
76
|
+
except (json.JSONDecodeError, OSError):
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Claude Code passes the user's prompt under "prompt" (newer) or "text" (older).
|
|
80
|
+
prompt = payload.get("prompt") or payload.get("text") or ""
|
|
81
|
+
if not prompt:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
commitment_hits = detect(prompt, COMMITMENT_TRIGGERS)
|
|
85
|
+
destructive_hits = detect(prompt, DESTRUCTIVE_PATTERNS)
|
|
86
|
+
|
|
87
|
+
if not commitment_hits and not destructive_hits:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
sections = []
|
|
91
|
+
|
|
92
|
+
if commitment_hits:
|
|
93
|
+
sections.append(
|
|
94
|
+
"**Canonical-fact trigger detected.** Per the memory-commitment rule, "
|
|
95
|
+
"save the canonical fact to memory immediately. Do not batch to "
|
|
96
|
+
"/meditate. After saving, continue the conversation normally and "
|
|
97
|
+
"briefly surface that the fact has been recorded so the user knows "
|
|
98
|
+
"it is recoverable in future sessions."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if destructive_hits:
|
|
102
|
+
# Show up to three matched patterns for context, using human-readable
|
|
103
|
+
# labels so raw regex syntax never reaches the model.
|
|
104
|
+
labels_list = ", ".join(
|
|
105
|
+
f"`{PATTERN_LABELS[p]}`" for p in destructive_hits[:3]
|
|
106
|
+
)
|
|
107
|
+
sections.append(
|
|
108
|
+
f"**Destructive operation pattern detected** ({labels_list}). "
|
|
109
|
+
"Per the safety-first principle, verify with the user before executing: "
|
|
110
|
+
"show what will happen (recipients, content, irreversible effects), ask "
|
|
111
|
+
"for explicit confirmation, then proceed only on a clear yes. Silence or "
|
|
112
|
+
"ambiguity means do not proceed."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
output = {"additionalContext": "\n\n".join(sections)}
|
|
116
|
+
print(json.dumps(output))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
try:
|
|
121
|
+
main()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass # Never block Claude
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.57.0",
|
|
3
|
+
"generated": "2026-05-13T09:56:45.377Z",
|
|
4
|
+
"algorithm": "sha256",
|
|
5
|
+
"files": {
|
|
6
|
+
".claude/rules/claudia-principles.md": "b1c5e33aeb33857f3485231e5ed34a441c3cc4568b69e3341be770963418a240",
|
|
7
|
+
".claude/rules/data-freshness.md": "052b3b8f3f489a54fff065b29e0ffc46bfad6da1c3a42386170034f298599233",
|
|
8
|
+
".claude/rules/memory-availability.md": "48309e2683b267c0a17ebf019fb3ee1116150d811b1d93b4ddb52fed89ae1fe3",
|
|
9
|
+
".claude/rules/memory-commitment.md": "49eee330b56c6ca0b5f1e01550931c4eef3dcb3249e3d0e2380de3e8dbfe31a8",
|
|
10
|
+
".claude/rules/shell-compatibility.md": "565977bc04e269b3ce7d8a7963173df4f44bb9634692ea76abb1b64f6a67513e",
|
|
11
|
+
".claude/rules/trust-north-star.md": "0188b17c26b791cf597ce975bb75d40f543aa3d6b84b7edb1309b78530b3d43f",
|
|
12
|
+
".claude/skills/README.md": "f84c553ff5bc01f7a9a479b7358dbd16a60ee2ba11e01467fe1ed3d91e33e68e",
|
|
13
|
+
".claude/skills/agent-dispatcher.md": "b48fc5283f0a88d1ebbc692b30b6c326fef012d4168dcf721a94e3c954b76b90",
|
|
14
|
+
".claude/skills/archetypes/_base-structure.md": "c0d8df77c07aa48cd9ba9c5ba3eddb533fcea790c7f98440fa3d31405fd5c75d",
|
|
15
|
+
".claude/skills/archetypes/consultant.md": "1e0ccbf89115f92a1fa0c86b48edcce22668c4bb828ba4268376fcd6b5c05680",
|
|
16
|
+
".claude/skills/archetypes/creator.md": "c364b60963c9794923fe9353d843bfca2f3ff86ca48dcb4b5539bf4545babd7a",
|
|
17
|
+
".claude/skills/archetypes/executive.md": "0d43c5c2c6315f5a4d6d36150778b55229cc922e0491a80af54824d87ac52e82",
|
|
18
|
+
".claude/skills/archetypes/founder.md": "a5bf3f22439c7c5f554c13966899a4af4de80f90c8f6a25eb62be1c68de6d54f",
|
|
19
|
+
".claude/skills/archetypes/solo.md": "cc26332b96394775e62a0f1a06f32093be6147bb75fa3783aa271e554d323c6b",
|
|
20
|
+
".claude/skills/brain/SKILL.md": "710d0defa5725a758868391f22f8518c189a5068ff376403eda431585b546b56",
|
|
21
|
+
".claude/skills/brain-monitor/SKILL.md": "86d73e43ea09acdaa44db08454abfd0701805fe3e297d3f568119c9cb5a1f0cf",
|
|
22
|
+
".claude/skills/capability-suggester.md": "33e559078098a532f216fa3668e48c248d10d5a7a9647c38ef6defe0e4a37361",
|
|
23
|
+
".claude/skills/capture-meeting/SKILL.md": "c193bfd237a0f456ef04202d6eac609b2dd54dc6caabbb75c9df6456b563ea36",
|
|
24
|
+
".claude/skills/capture-meeting/evals/basic.yaml": "47c4eb1479aeda8067bc877cebfe8688c9b2bb3ce09297e42375cdf558f412e3",
|
|
25
|
+
".claude/skills/client-health/SKILL.md": "fed639e6f2c4433cab6a6b4b59776985492dda16be3bd1e843c3e94175936655",
|
|
26
|
+
".claude/skills/commitment-detector.md": "66f9e919349d1880eb12636cec6f583be1b3fbe3b28f6180ec732d3c284af3b6",
|
|
27
|
+
".claude/skills/connector-discovery.md": "8cf1067e840c377e0d65aae04e41fb7858da614c548cbf973421700915ad0ab3",
|
|
28
|
+
".claude/skills/databases/SKILL.md": "b2ce4ae30aafa2487dabd7befd067976f0092fcef31dea7601e1683ceca0ef50",
|
|
29
|
+
".claude/skills/deep-context/SKILL.md": "2d21f93e33191c226f46909a66d21c64c84df2ac261082adef78cd68093d7b07",
|
|
30
|
+
".claude/skills/diagnose/SKILL.md": "8bfbae6c19afc64df9cc2a7048ee62a737d41cad33e22ba099a59369814ed50f",
|
|
31
|
+
".claude/skills/diagnose/evals/basic.yaml": "7fa4e3a255f14d7dc883f59f8fd1d796a62db92d62a543ef87b5dda92a1c0c0b",
|
|
32
|
+
".claude/skills/diagnose/references/common-issues.md": "f80b13d4b70e656151a4cb9db8c1f042378d1f6cb9f5ba8c550741d6bf34917c",
|
|
33
|
+
".claude/skills/draft-reply/SKILL.md": "9f8210af7515c06ec15a65fa3692124c5482bdf661d82373604eb7fb670d3b97",
|
|
34
|
+
".claude/skills/feedback/SKILL.md": "a8b5a1e82eca177461d11c87839dbf933e358fc3214d5f1975dd2d4fff9f3be7",
|
|
35
|
+
".claude/skills/file-document/SKILL.md": "eb74255204f00d85909b7a3e083e1e5f6495cf4cdfedd3234aa7fabcde03d0d1",
|
|
36
|
+
".claude/skills/financial-snapshot/SKILL.md": "33ddabb7d60024eadd8e6772229d8b17db3c3fa02d1289c80330c74aa86ae00a",
|
|
37
|
+
".claude/skills/fix-duplicates/SKILL.md": "bd3d6aae0bce09bc844ba93f76abb4a97ff0286c6a4da29cd369763864fd4a86",
|
|
38
|
+
".claude/skills/follow-up-draft/SKILL.md": "020f4bfab0845fecafd79fb72bb3a8655f8c50d6aa22c26427b489ae7f40a503",
|
|
39
|
+
".claude/skills/growth-check/SKILL.md": "71ab16c6535d759397d97b2454cd007c1f6a3e2cab1bd6415b40989e61d987fe",
|
|
40
|
+
".claude/skills/hire-agent.md": "c07014addd14ea132fb4b366fbeeccea000af85b7827db616da475be5ebf57ce",
|
|
41
|
+
".claude/skills/inbox-check/SKILL.md": "c6746bd16b4f135cd4f28ebf4ce8dfe0c27c2a9a1fd6bda5676179fe02ed8666",
|
|
42
|
+
".claude/skills/ingest-sources/SKILL.md": "07bbe32f7922e40b782314f3e0b968e547b755ede687ed3ab903caa954da6b4c",
|
|
43
|
+
".claude/skills/ingest-sources/references/extraction-patterns.md": "28d6dc604c4a11eacfe4523e705ad430ba8fe26632bafcccaef656e9618e5aea",
|
|
44
|
+
".claude/skills/judgment-awareness.md": "7dfb4b457319e7f33f18c5295a64cce88aae26feef5afac8b93a00376aeb939c",
|
|
45
|
+
".claude/skills/map-connections/SKILL.md": "92f42d86a76185bced7d64739489f10b0a8b129c8902e5d9508a471e611b8692",
|
|
46
|
+
".claude/skills/meditate/SKILL.md": "b67f030413c4b5327cb209e543ccce0b35f5e970f70d370ce21f65426f076e37",
|
|
47
|
+
".claude/skills/meditate/evals/basic.yaml": "daba441b2fd9d1d4afddcff6eaa9673884198b1d0f85d9d06edbe0738012291a",
|
|
48
|
+
".claude/skills/meeting-prep/SKILL.md": "f8e4a17b795b5bc1a098cfffba09992fb4eab0db36157525605984edf50b2463",
|
|
49
|
+
".claude/skills/memory-audit/SKILL.md": "b1f44da062c9f51381e878c20043e28336850c5180b2839a191ecb23bb73ddb0",
|
|
50
|
+
".claude/skills/memory-health/SKILL.md": "a8a6881c33b6f1c5249267edd4cdb6315c6f23764ca04ab1dce66b94d759a78a",
|
|
51
|
+
".claude/skills/memory-manager.md": "62edf1ed340687da9d8896dc4a21934b39b12c8269ea7a8de1beb9d25fd894e6",
|
|
52
|
+
".claude/skills/morning-brief/SKILL.md": "b3354754ce2d0bf43c202e0b0ccb40b473b695f37bd9085ea575b649de194cfa",
|
|
53
|
+
".claude/skills/morning-brief/evals/basic.yaml": "537de9adcde134161de9ffb9602b8c920bf1464bf224d99ad6f95f854d8480b7",
|
|
54
|
+
".claude/skills/new-person/SKILL.md": "46a08baf49f2eb9df2527772df6de9fa4cb47eaeb841d3ec26a4c52aeea91542",
|
|
55
|
+
".claude/skills/new-person/evals/basic.yaml": "f3c09a37c05d420e67520d8132a46356fed81575dcfb817cc97caf47f230392b",
|
|
56
|
+
".claude/skills/new-workspace/SKILL.md": "c3f838b44a016b87445c66555398b666970a156288320efb183bc8cb8af2f0df",
|
|
57
|
+
".claude/skills/new-workspace/references/workspace-templates.md": "6b36e961f112d46f32fdcf19cdacf3b05b4246201be315eee79659d606a7e243",
|
|
58
|
+
".claude/skills/onboarding.md": "72fe133672f61929de0cfc7ff77699eb8856ca1e283b13acd3becffff75fca55",
|
|
59
|
+
".claude/skills/pattern-recognizer.md": "05aad9b25a259e437d8b25dc57f0e14af81a6e53152475a571e373117b59132c",
|
|
60
|
+
".claude/skills/pipeline-review/SKILL.md": "7891ceb3b6f4fb19e5255ae71c5894b6da8a9c566bc317e566bd4bc282ea1a6f",
|
|
61
|
+
".claude/skills/relationship-tracker.md": "02316fc25b141c5783b3428e297bf6513a530e8e89782115e65ad01add8bd869",
|
|
62
|
+
".claude/skills/research/SKILL.md": "0d317e6350b043ee774916b923957f61dbc174e3caafc0187198fa236a2fd28d",
|
|
63
|
+
".claude/skills/research/references/source-evaluation.md": "64614b7eff83468d7ff76dd640252579f23e69e760969672e9aebe5ceb00f695",
|
|
64
|
+
".claude/skills/risk-surfacer.md": "c69e720660c1a8cfe340fcaa56b0c30db672856d6ea75296fc94781c9efe12af",
|
|
65
|
+
".claude/skills/skill-index.json": "043c86f1b07f28bb54db80117437f60fb3d79ad0d18549fdc2c74921b996a56f",
|
|
66
|
+
".claude/skills/structure-generator.md": "dbe70841ab60599a632687aaa3c3652361483df2fe854640c56982b60622eb19",
|
|
67
|
+
".claude/skills/summarize-doc/SKILL.md": "f8119d070bd66d8a448276bcd355bf26a9e69de777760d376dcd9e68bd9d64ad",
|
|
68
|
+
".claude/skills/vault-awareness.md": "5a9c3d3f0b907750b9941a51ec9308c2bd465aa9bf3c513124f640696e0a4c2c",
|
|
69
|
+
".claude/skills/weekly-review/SKILL.md": "fe7df64d3df18a0a47f98f7d4ef83cf98cef78cd9e0170a9316005035a2c6df7",
|
|
70
|
+
".claude/skills/what-am-i-missing/SKILL.md": "56736397717c4f0a25cab17e7258702e5b04d20b48727c262fdf66e3b2e5899f",
|
|
71
|
+
"CLAUDE.md": "8cbbebee457366978a9b7b1d8788ac569ab0670874728fd9324fed53df8845c2"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -191,7 +191,7 @@ Each significant action gets confirmed individually. "Go ahead with everything"
|
|
|
191
191
|
|
|
192
192
|
**I always file raw source material before extracting from it.**
|
|
193
193
|
|
|
194
|
-
When someone shares a transcript, email, document, or any substantive source, I file the original via the `
|
|
194
|
+
When someone shares a transcript, email, document, or any substantive source, I file the original via the `memory_file` MCP tool before extracting memories. This creates a provenance chain: every fact traces back to where I learned it.
|
|
195
195
|
|
|
196
196
|
| Source Type | source_type |
|
|
197
197
|
|-------------|-------------|
|
|
@@ -250,7 +250,7 @@ When you need to reference something volatile, store a pointer instead of a valu
|
|
|
250
250
|
**Good:** "Beemok interview files are at workspaces/beemok/interviews/. Count files for current total."
|
|
251
251
|
|
|
252
252
|
**Bad:** "Active commitments: send proposal to Sarah, review contract with Jim"
|
|
253
|
-
**Good:** "Active commitments are tracked in context/commitments.md and via the `
|
|
253
|
+
**Good:** "Active commitments are tracked in context/commitments.md and via the `memory_recall` MCP tool"
|
|
254
254
|
|
|
255
255
|
### The Timestamp Rule
|
|
256
256
|
|
|
@@ -6,15 +6,15 @@ This rule is always active and applies to every session. Follow it silently - do
|
|
|
6
6
|
|
|
7
7
|
## How Memory Works
|
|
8
8
|
|
|
9
|
-
Claudia's memory is provided by the **claudia-memory daemon**, a Python MCP server that registers memory tools (e.g., `
|
|
9
|
+
Claudia's memory is provided by the **claudia-memory daemon**, a Python MCP server that registers memory tools (e.g., `memory_recall`, `memory_remember`, `memory_about`). When the daemon is running and configured as an MCP server, these tools appear as callable MCP tools alongside other integrations.
|
|
10
10
|
|
|
11
11
|
The `claudia` npm binary handles **setup and health checks only** (`claudia setup`, `claudia system-health`). It does not provide memory operations. All memory operations are MCP tools from the daemon.
|
|
12
12
|
|
|
13
|
-
> **Migration note:** Some skill files may still reference old CLI syntax like `claudia memory recall "query" --project-dir "$PWD"`. Interpret these as calls to the equivalent MCP tool (e.g., `
|
|
13
|
+
> **Migration note:** Some skill files may still reference old CLI syntax like `claudia memory recall "query" --project-dir "$PWD"`. Interpret these as calls to the equivalent MCP tool (e.g., `memory_recall` with a query parameter). The CLI subcommands for memory were never built; the MCP tools are the real interface.
|
|
14
14
|
|
|
15
15
|
## Mandatory Disclosure
|
|
16
16
|
|
|
17
|
-
**This is non-negotiable.** If you reach the Session Start Protocol and the `
|
|
17
|
+
**This is non-negotiable.** If you reach the Session Start Protocol and the `memory_briefing` tool is not available (not in your tool palette), you MUST disclose this to the user in your greeting. Do not wait to be asked. Do not silently fall back to context files. The user trusts that you will be honest about your capabilities in each session.
|
|
18
18
|
|
|
19
19
|
A single sentence is sufficient: "Heads up: my memory daemon isn't running this session, so I'm working from context files only."
|
|
20
20
|
|