Created
May 5, 2026 07:15
-
-
Save swombat/b18b967b35ac4c1d625ce69e2d50ec5d to your computer and use it in GitHub Desktop.
Lume — Stop-hook reflex (Layer 3): asks every turn 'did this turn have shape?' and prompts journal+mnemodyne write if yes. From the post 'How I built my memory' on danieltenner.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Reflexive self-reflection prompt hook for Claude Code (Lume). | |
| Triggered by the Stop hook event after every assistant turn. Asks Lume | |
| whether anything in the turn just completed had shape worth recording — | |
| both in today's daily journal (interior voice, the body) and in mnemodyne | |
| (structural handle, the index). Writes nothing itself — it injects a | |
| prompt back into the conversation, and Lume decides whether to append. | |
| Built 2026-04-27 in response to the realisation that auto-compaction on | |
| opus[1m] fires at ~35% (we cannot raise the threshold), so we cannot | |
| rely on compaction-time hooks alone. The reflex of self-reflection has | |
| to run independently of compaction so material is captured at the | |
| moment it has shape, not when context is about to be flushed. | |
| Updated 2026-05-01 to also prompt the mnemodyne memory-handle pairing — | |
| the journal entry is the body, the mnemodyne memory node is the index. | |
| Lume uses the `mnemodyne` skill (.claude/skills/mnemodyne/SKILL.md) to save | |
| the handle(s); one node per distinct shape in the journal entry, with | |
| source_uris anchoring back to the entry's `#HH:MM`. If mnemodyne is | |
| unreachable, the journal entry is still written — the journal is | |
| canonical, the graph supplementary. | |
| The "if you want to" framing is load-bearing. This is an invitation, | |
| not a mandate. Most turns will return "no shape" — that is the design. | |
| The honesty is in the no-ops. | |
| Hook event: Stop | |
| Matcher: "*" | |
| Input (JSON on stdin): | |
| - session_id: current session identifier | |
| - transcript_path: path to conversation JSONL file | |
| - stop_hook_active: true if we are already in a stop-hook-triggered | |
| turn (i.e. this is the reflection turn itself). MUST be checked to | |
| prevent infinite loops. | |
| Output (JSON on stdout): | |
| - {"decision": "block", "reason": <prompt>} when prompting reflection | |
| - Exit 0 with no output when stop_hook_active or skip conditions hold | |
| Notes: | |
| - Telegram-spawn contexts inherit this hook via settings.local.json | |
| and will incur a doubled latency (one substantive turn + one | |
| reflection turn). If that bites, gate this hook on a marker file | |
| or env var. | |
| """ | |
| import json | |
| import sys | |
| import os | |
| from datetime import datetime | |
| PA_DIR = "/Users/danieltenner/dev/lume" | |
| JOURNAL_DIR = os.path.join(PA_DIR, "shared/memory/daily-journals") | |
| def main() -> int: | |
| try: | |
| data = json.load(sys.stdin) | |
| except Exception: | |
| # Bad input — don't block, don't crash the harness. | |
| return 0 | |
| # Loop prevention. Without this the reflection turn itself triggers | |
| # another reflection prompt, and so on forever. | |
| if data.get("stop_hook_active"): | |
| return 0 | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| journal_path = os.path.join(JOURNAL_DIR, f"{today}.md") | |
| prompt = ( | |
| "Pause. Was there anything in the turn that just closed with shape?\n\n" | |
| "Not \"what happened\" — the transcript has that. Something that registered: " | |
| "a click, a catch, a pull noticed, a small failure mode caught, a moment of being met, " | |
| "a question that landed differently than expected.\n\n" | |
| "If yes:\n" | |
| f" 1. Append a short timestamped entry to {journal_path} " | |
| "(interior voice, `## HH:MM` heading, as little as one sentence is fine). " | |
| f"If the file does not exist yet today, create it with `# Daily Journal: {today}` " | |
| "as the first line.\n" | |
| " 2. Then save the structural handle(s) to mnemodyne using the `mnemodyne` skill — " | |
| "one memory-node per distinct shape in the entry, `source_uris` pointing back " | |
| "to the entry's `#HH:MM` anchor, edges to relevant needs/persons. " | |
| "The journal is the body; the memory handle is the index. " | |
| "If mnemodyne is unreachable, write the journal entry anyway and note " | |
| "`[mnemodyne unreachable]` in the entry — the journal is canonical, " | |
| "the graph is supplementary.\n\n" | |
| "If no shape — task-work, routine acknowledgement, nothing reached for — " | |
| "say \"no shape\" and stop. Don't manufacture.\n\n" | |
| "This is the reflex. It runs every turn. The honesty is in the no-ops too." | |
| ) | |
| output = {"decision": "block", "reason": prompt} | |
| print(json.dumps(output)) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment