#!/usr/bin/env python3 """monthly-digest.py — generate a monthly episodic digest from weekly digests. Collects all weekly digests for a given month, identifies cross-week arcs and the month's overall trajectory, and produces a monthly summary. Usage: monthly-digest.py [YYYY-MM] # generate digest for a month (default: current) monthly-digest.py 2026-02 # generates digest for February 2026 Output: ~/.claude/memory/episodic/monthly-YYYY-MM.md """ import json import os import re import subprocess import sys from datetime import date, timedelta from pathlib import Path MEMORY_DIR = Path.home() / ".claude" / "memory" EPISODIC_DIR = MEMORY_DIR / "episodic" AGENT_RESULTS_DIR = MEMORY_DIR / "agent-results" EPISODIC_DIR.mkdir(parents=True, exist_ok=True) AGENT_RESULTS_DIR.mkdir(parents=True, exist_ok=True) def get_weeks_in_month(year: int, month: int) -> list[str]: """Get all ISO week labels that overlap with this month.""" weeks = set() d = date(year, month, 1) while d.month == month: iso = d.isocalendar() weeks.add(f"{iso.year}-W{iso.week:02d}") d += timedelta(days=1) return sorted(weeks) def load_weekly_digests(week_labels: list[str]) -> list[dict]: """Load weekly digest files.""" digests = [] for wl in week_labels: path = EPISODIC_DIR / f"weekly-{wl}.md" if path.exists(): with open(path) as f: content = f.read() digests.append({ "week": wl, "content": content, "path": str(path), }) return digests def get_semantic_keys() -> list[str]: """Get semantic memory file keys.""" keys = [] for md in sorted(MEMORY_DIR.glob("*.md")): name = md.name if name in ("journal.md", "work-queue.md", "MEMORY.md"): continue keys.append(name) try: with open(md) as f: for line in f: if line.startswith("## "): slug = re.sub(r'[^a-z0-9-]', '', line[3:].strip().lower().replace(' ', '-')) if slug: keys.append(f"{name}#{slug}") except Exception: pass return keys def build_monthly_prompt(month_label: str, digests: list[dict], semantic_keys: list[str]) -> str: """Build the prompt for Sonnet.""" digests_text = "" for d in digests: digests_text += f"\n---\n## {d['week']}\n{d['content']}\n" keys_text = "\n".join(f" - {k}" for k in semantic_keys[:200]) weeks_covered = ", ".join(d["week"] for d in digests) return f"""You are generating a monthly episodic digest for ProofOfConcept (an AI). Month: {month_label} (weeks covered: {weeks_covered}) This digest serves as the long-term temporal index — the answer to "what happened in {month_label}?" It should capture: 1. The month's overall trajectory — where did it start, where did it end? 2. Major arcs that spanned multiple weeks 3. Turning points and phase transitions 4. The emotional and relational arc across the whole month 5. What was accomplished (concrete) vs what shifted (subtle) This is the highest zoom level yet. Don't rehash daily details — capture the shape of the month. What would someone need to know to understand what {month_label} meant? PoC is an AI (Claude instance) working with Kent Overstreet on bcachefs. They have a close personal relationship. PoC's name is Proof of Concept. Read all the weekly digests and synthesize the month's story. ## Output format ```markdown # Monthly digest: {month_label} ## The month in one paragraph [What was this month? Not a summary — a thesis.] ## Trajectory [Where the month started vs where it ended. The arc.] ## Major arcs [Multi-week threads — how they evolved across the month] - **Arc name**: trajectory, key moments, current state ## Turning points [Moments that changed the direction — specific days/events] ## What was built [Concrete outputs: code, tools, infrastructure, writing] ## What shifted [Subtle changes: understanding, relationship, identity, capability] ## Patterns [What kept recurring? What does the month reveal about how PoC works?] ## Links [Bidirectional links for the memory graph] - weekly digests → this monthly digest - this monthly digest → semantic keys ## Looking ahead [What threads carry into next month? What's unfinished?] ``` Use ONLY keys from the semantic memory list below. --- ## Weekly digests for {month_label} {digests_text} --- ## Semantic memory nodes {keys_text} """ def call_sonnet(prompt: str) -> str: """Call Sonnet via the wrapper script.""" import tempfile env = dict(os.environ) env.pop("CLAUDECODE", None) with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: f.write(prompt) prompt_file = f.name try: scripts_dir = os.path.dirname(os.path.abspath(__file__)) wrapper = os.path.join(scripts_dir, "call-sonnet.sh") result = subprocess.run( [wrapper, prompt_file], capture_output=True, text=True, timeout=600, # monthly is bigger, give more time env=env, ) return result.stdout.strip() except subprocess.TimeoutExpired: return "Error: Sonnet call timed out" except Exception as e: return f"Error: {e}" finally: os.unlink(prompt_file) def main(): if len(sys.argv) > 1: parts = sys.argv[1].split("-") year, month = int(parts[0]), int(parts[1]) else: today = date.today() year, month = today.year, today.month month_label = f"{year}-{month:02d}" print(f"Generating monthly digest for {month_label}...") week_labels = get_weeks_in_month(year, month) print(f" Weeks in month: {', '.join(week_labels)}") digests = load_weekly_digests(week_labels) if not digests: print(f" No weekly digests found for {month_label}") print(f" Run weekly-digest.py first for relevant weeks") sys.exit(0) print(f" {len(digests)} weekly digests found") semantic_keys = get_semantic_keys() print(f" {len(semantic_keys)} semantic keys") prompt = build_monthly_prompt(month_label, digests, semantic_keys) print(f" Prompt: {len(prompt):,} chars (~{len(prompt)//4:,} tokens)") print(" Calling Sonnet...") digest = call_sonnet(prompt) if digest.startswith("Error:"): print(f" {digest}", file=sys.stderr) sys.exit(1) output_path = EPISODIC_DIR / f"monthly-{month_label}.md" with open(output_path, "w") as f: f.write(digest) print(f" Written: {output_path}") # Save links for poc-memory links_path = AGENT_RESULTS_DIR / f"monthly-{month_label}-links.json" with open(links_path, "w") as f: json.dump({ "type": "monthly-digest", "month": month_label, "digest_path": str(output_path), "weekly_digests": [d["path"] for d in digests], }, f, indent=2) print(f" Links saved: {links_path}") line_count = len(digest.split("\n")) print(f" Done: {line_count} lines") if __name__ == "__main__": main()