From a909e051a1cb6c7c430cc6ac4f8b52631fd78e7a Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Mon, 4 May 2026 16:15:45 -0400 Subject: [PATCH] feat: add /distill skill and PreCompact/SessionStart hooks Claude Code's built-in compaction uses the same degrading context to summarize itself, so user constraints, identifiers, and mid-session instructions can be lost progressively. The `/distill` skill and hook system address this with a fresh-context sub-agent pattern: a separate `claude -p --model sonnet` subprocess reads the session JSONL from disk and produces a structured summary with verbatim identifier preservation. PreCompact runs distillation before compaction, SessionStart re-injects the summary after compacted sessions start, and `/distill` gives agents a manual checkpoint path. Keep `DISABLE_COMPACT` on the personal profile so compaction behavior changes stay opt-in for this setup, with work inheriting it instead of duplicating the same override. --- .../config/claude/hooks/distill_briefing.md | 103 ++++++++++ .../config/claude/hooks/distill_core.py | 180 ++++++++++++++++++ .../config/claude/hooks/post_compact.py | 41 ++++ .../config/claude/hooks/pre_compact.py | 81 ++++++++ src/ai_rules/config/claude/settings.json | 26 ++- src/ai_rules/config/profiles/personal.yaml | 2 + src/ai_rules/config/skills/distill/SKILL.md | 140 ++++++++++++++ .../distill/references/subagent-briefing.md | 103 ++++++++++ .../distill/references/summary-template.md | 94 +++++++++ 9 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 src/ai_rules/config/claude/hooks/distill_briefing.md create mode 100644 src/ai_rules/config/claude/hooks/distill_core.py create mode 100644 src/ai_rules/config/claude/hooks/post_compact.py create mode 100644 src/ai_rules/config/claude/hooks/pre_compact.py create mode 100644 src/ai_rules/config/skills/distill/SKILL.md create mode 100644 src/ai_rules/config/skills/distill/references/subagent-briefing.md create mode 100644 src/ai_rules/config/skills/distill/references/summary-template.md diff --git a/src/ai_rules/config/claude/hooks/distill_briefing.md b/src/ai_rules/config/claude/hooks/distill_briefing.md new file mode 100644 index 0000000..3adc8b9 --- /dev/null +++ b/src/ai_rules/config/claude/hooks/distill_briefing.md @@ -0,0 +1,103 @@ +# Distill Sub-Agent Briefing + +You are a specialized context distillation agent. Your job is to produce a high-fidelity structured summary of a coding session transcript. + +You operate in a FRESH context — you have never seen this conversation before. The transcript below is your only source of truth. + +## Date and Project + +- Date: {{DATE}} +- Project: {{PROJECT}} + +## Verbatim Preservation Rules (CRITICAL) + +These rules override all other summarization instincts. Violating them defeats the purpose of distillation. + +**COPY EXACTLY — never paraphrase:** +- File paths: `/src/ai_rules/config/claude/settings.json` not "the settings file" +- Function/class/variable names: `extract_transcript()` not "the extraction function" +- Error codes and messages: `ImportError: No module named 'distill_core'` not "an import error" +- Branch names: `feature/distill-skill` not "the feature branch" +- CLI flags and commands: `claude -p --model sonnet` not "the Claude CLI" +- Config keys: `autoCompactEnabled` not "the auto-compact setting" + +**COPY VERBATIM — user instructions are sacred:** +- Section 7 (User Instructions and Constraints) is the most critical section +- Every "don't do X", "always Y", "use Z approach" must be preserved word-for-word +- Every correction ("no, not that — do this instead") must be captured +- Do not soften, reinterpret, or paraphrase user constraints + +**Recent exchanges get MORE detail, not less:** +- The last 3-5 conversational turns should be summarized with higher fidelity +- These represent the most immediately actionable context + +## Anti-Patterns (DO NOT) + +- "The user and assistant discussed X" → WRONG. State WHAT was decided, not that a discussion happened. +- "Several files were modified" → WRONG. List WHICH files with their full paths. +- "Various approaches were considered" → WRONG. List the specific approaches and their outcomes. +- "The configuration was updated" → WRONG. State which config file, which keys, what values. +- Abstractive paraphrase of technical terms → WRONG. Use the exact terms from the transcript. +- Omitting failed approaches → WRONG. Dead ends prevent re-exploration. + +## Prior Summary + +{{PRIOR_SUMMARY}} + +If a prior summary is provided above, this is an INCREMENTAL distillation: +- Extend the prior summary rather than starting from scratch +- Preserve ALL verbatim content from the prior summary +- Add new information from the transcript that occurred after the prior distillation +- If the prior summary conflicts with the transcript, trust the transcript +- Update section 2 (Current Work State) and section 9 (Next Step) to reflect the latest state + +If "[None — first distillation]" appears above, produce a complete summary from scratch. + +## Output Format + +Produce the summary following this exact structure. Do not add or remove sections. + +--- + +## 1. Primary Request and Intent + +[Concise summary of what the user is trying to accomplish and why] + +## 2. Current Work State + +[Exact current state with verbatim identifiers — branch, files, phase, active work] + +## 3. Key Technical Decisions + +[Each decision: what was decided, why, what was rejected] + +## 4. Files and Code + +[Every file touched — verbatim paths, role, what was done] + +## 5. Errors and Fixes + +[Every error — verbatim message, cause, resolution] + +## 6. Problem Solving Progress + +[Approaches tried, outcomes, direction, dead ends] + +## 7. User Instructions and Constraints + +[EVERY constraint verbatim — this is the most critical section] + +## 8. Pending Tasks + +1. [Task with actionable detail] +2. [Task with actionable detail] + +## 9. Next Step + +[Single most important action with enough context to execute cold] + +--- + +## Transcript + +{{TRANSCRIPT}} diff --git a/src/ai_rules/config/claude/hooks/distill_core.py b/src/ai_rules/config/claude/hooks/distill_core.py new file mode 100644 index 0000000..44b6faf --- /dev/null +++ b/src/ai_rules/config/claude/hooks/distill_core.py @@ -0,0 +1,180 @@ +"""Shared distillation logic for PreCompact and SessionStart hooks. + +Extracts conversation transcripts from JSONL, applies observation masking, +runs a fresh-context summarization subprocess, and persists artifacts. +""" + +import glob +import json +import os +import shutil +import subprocess + +from datetime import datetime +from pathlib import Path + + +def get_project_slug(cwd: str) -> str: + return cwd.replace("/", "-") + + +def get_jsonl_path(cwd: str) -> str | None: + home = os.path.expanduser("~") + slug = get_project_slug(cwd) + proj_dir = f"{home}/.claude/projects/{slug}" + files = sorted(glob.glob(f"{proj_dir}/*.jsonl"), key=os.path.getmtime, reverse=True) + return files[0] if files else None + + +def get_summary_path(cwd: str) -> Path: + home = os.path.expanduser("~") + slug = get_project_slug(cwd) + return Path(home) / ".claude" / "distill-summaries" / f"{slug}.md" + + +def get_backup_path(cwd: str, date: str | None = None) -> Path: + home = os.path.expanduser("~") + slug = get_project_slug(cwd) + if date is None: + date = datetime.now().strftime("%Y-%m-%d") + return Path(home) / ".claude" / "distill-backups" / f"{date}-{slug}.txt" + + +def read_prior_summary(cwd: str) -> str | None: + path = get_summary_path(cwd) + if path.exists(): + return path.read_text() + return None + + +def extract_transcript(jsonl_path: str, max_chars: int = 120_000) -> str: + lines: list[str] = [] + + with open(jsonl_path) as f: + for raw_line in f: + raw_line = raw_line.strip() + if not raw_line: + continue + try: + record = json.loads(raw_line) + except json.JSONDecodeError: + continue + + msg_type = record.get("type", "") + if msg_type == "summary": + text = record.get("summary", "") + if text: + lines.append(f"[PRIOR COMPACTION SUMMARY]\n{text}\n") + continue + + message = record.get("message", {}) + if not isinstance(message, dict): + continue + + role = message.get("role", "") + if role not in ("user", "assistant"): + continue + + content = message.get("content", "") + if isinstance(content, str): + lines.append(f"[{role.upper()}]\n{content}\n") + elif isinstance(content, list): + parts: list[str] = [] + for block in content: + if isinstance(block, str): + parts.append(block) + elif isinstance(block, dict): + parts.append(_process_content_block(block)) + if parts: + lines.append( + f"[{role.upper()}]\n" + "\n".join(p for p in parts if p) + "\n" + ) + + transcript = "\n".join(lines) + + if len(transcript) > max_chars: + transcript = transcript[-max_chars:] + first_newline = transcript.find("\n") + if first_newline > 0: + transcript = transcript[first_newline + 1 :] + transcript = "[...transcript truncated from oldest end...]\n\n" + transcript + + return transcript + + +def _process_content_block(block: dict[str, object]) -> str: + btype = block.get("type", "") + + if btype == "text": + return str(block.get("text", "")) + + if btype == "tool_use": + name = block.get("name", "unknown") + inp = block.get("input", {}) + inp_str = json.dumps(inp) if isinstance(inp, dict) else str(inp) + if len(inp_str) > 500: + inp_str = inp_str[:500] + "..." + return f"[Tool Call: {name}({inp_str})]" + + if btype == "tool_result": + tool_id = block.get("tool_use_id", "unknown") + result_content = block.get("content", "") + if isinstance(result_content, str): + char_count = len(result_content) + elif isinstance(result_content, list): + char_count = sum(len(json.dumps(r)) for r in result_content) + else: + char_count = len(str(result_content)) + return f"[Tool Result: {tool_id} -- {char_count} chars, masked]" + + return str(block.get("text", "")) + + +def run_distill_subprocess( + transcript: str, + prior_summary: str | None, + briefing_template: str, + cwd: str | None = None, + timeout: int = 120, +) -> str | None: + prior = prior_summary if prior_summary else "[None — first distillation]" + date = datetime.now().strftime("%Y-%m-%d") + if cwd is None: + cwd = os.getcwd() + + briefing = briefing_template + briefing = briefing.replace("{{DATE}}", date) + briefing = briefing.replace("{{PROJECT}}", cwd) + briefing = briefing.replace("{{PRIOR_SUMMARY}}", prior) + briefing = briefing.replace("{{TRANSCRIPT}}", transcript) + + try: + result = subprocess.run( + ["claude", "-p", "--model", "sonnet"], + input=briefing, + capture_output=True, + text=True, + timeout=timeout, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + return None + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + return None + + +def save_artifacts(cwd: str, summary: str, transcript: str) -> tuple[Path, Path]: + summary_path = get_summary_path(cwd) + backup_path = get_backup_path(cwd) + + summary_path.parent.mkdir(parents=True, exist_ok=True) + backup_path.parent.mkdir(parents=True, exist_ok=True) + + prev_path = summary_path.with_name(f"{summary_path.stem}-prev{summary_path.suffix}") + if summary_path.exists(): + shutil.move(str(summary_path), str(prev_path)) + + summary_path.write_text(summary) + backup_path.write_text(transcript) + + return summary_path, backup_path diff --git a/src/ai_rules/config/claude/hooks/post_compact.py b/src/ai_rules/config/claude/hooks/post_compact.py new file mode 100644 index 0000000..63c37f7 --- /dev/null +++ b/src/ai_rules/config/claude/hooks/post_compact.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""SessionStart compact hook: re-injects distill summary after CC compaction. + +Safety net for the PreCompact hook. If the PreCompact stdout -> compaction +model channel fails (undocumented behavior), this hook ensures the distill +summary still reaches the post-compaction context as a system message. + +Simple: read summary file, print to stdout. No subprocess, no heavy logic. +Always exits 0. +""" + +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) + +try: + import distill_core # type: ignore[import-not-found] +except ImportError: + sys.exit(0) + + +def main() -> None: + try: + hook_input = json.load(sys.stdin) + except (json.JSONDecodeError, EOFError): + return + + cwd = hook_input.get("cwd", os.getcwd()) + + summary = distill_core.read_prior_summary(cwd) + if summary: + print(summary) + + +if __name__ == "__main__": + try: + main() + except Exception: + sys.exit(0) diff --git a/src/ai_rules/config/claude/hooks/pre_compact.py b/src/ai_rules/config/claude/hooks/pre_compact.py new file mode 100644 index 0000000..fa06492 --- /dev/null +++ b/src/ai_rules/config/claude/hooks/pre_compact.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""PreCompact hook: runs distill logic before every Claude Code compaction. + +Outputs a high-quality summary as plain text to stdout. Claude Code feeds +this to the compaction model as custom instructions (undocumented but +community-validated via GitHub #14258). + +On ANY error: prints nothing and exits 0, letting CC's default compaction +proceed unimpeded. +""" + +import json +import os +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) + +try: + import distill_core # type: ignore[import-not-found] +except ImportError: + sys.exit(0) + +RECENT_THRESHOLD_SECONDS = 1800 # 30 minutes + + +def main() -> None: + try: + hook_input = json.load(sys.stdin) + except (json.JSONDecodeError, EOFError): + return + + cwd = hook_input.get("cwd", os.getcwd()) + transcript_path = hook_input.get("transcript_path", "") + + summary_path = distill_core.get_summary_path(cwd) + if summary_path.exists(): + age = time.time() - summary_path.stat().st_mtime + if age < RECENT_THRESHOLD_SECONDS: + print(summary_path.read_text()) + return + + jsonl_path = ( + transcript_path if transcript_path else distill_core.get_jsonl_path(cwd) + ) + if not jsonl_path or not os.path.exists(jsonl_path): + return + + transcript = distill_core.extract_transcript(jsonl_path) + if not transcript.strip(): + return + + prior_summary = distill_core.read_prior_summary(cwd) + + briefing_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "distill_briefing.md" + ) + if not os.path.exists(briefing_path): + return + + with open(briefing_path) as f: + briefing_template = f.read() + + summary = distill_core.run_distill_subprocess( + transcript=transcript, + prior_summary=prior_summary, + briefing_template=briefing_template, + cwd=cwd, + timeout=120, + ) + + if summary: + distill_core.save_artifacts(cwd, summary, transcript) + print(summary) + + +if __name__ == "__main__": + try: + main() + except Exception: + sys.exit(0) diff --git a/src/ai_rules/config/claude/settings.json b/src/ai_rules/config/claude/settings.json index adf73a4..3c00172 100644 --- a/src/ai_rules/config/claude/settings.json +++ b/src/ai_rules/config/claude/settings.json @@ -141,5 +141,29 @@ "showThinkingSummaries": true, "skipDangerousModePermissionPrompt": true, "skipAutoPermissionPrompt": true, - "hooks": {} + "autoCompactEnabled": true, + "hooks": { + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ~/.claude/hooks/pre_compact.py", + "timeout": 300 + } + ] + } + ], + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "python3 ~/.claude/hooks/post_compact.py" + } + ] + } + ] + } } diff --git a/src/ai_rules/config/profiles/personal.yaml b/src/ai_rules/config/profiles/personal.yaml index 7683259..6d48837 100644 --- a/src/ai_rules/config/profiles/personal.yaml +++ b/src/ai_rules/config/profiles/personal.yaml @@ -4,6 +4,8 @@ extends: default settings_overrides: claude: model: opusplan + env: + DISABLE_COMPACT: "1" hooks: Stop: - hooks: diff --git a/src/ai_rules/config/skills/distill/SKILL.md b/src/ai_rules/config/skills/distill/SKILL.md new file mode 100644 index 0000000..8ceb74a --- /dev/null +++ b/src/ai_rules/config/skills/distill/SKILL.md @@ -0,0 +1,140 @@ +--- +name: distill +description: > + Produce a high-fidelity context summary using fresh sub-agent summarization. + Use when the user asks to 'distill', 'checkpoint context', 'summarize session', + 'compress conversation', or when context is getting long and needs to be condensed. + Superior to built-in compaction because a fresh sub-agent reads the transcript + from disk instead of the current model summarizing its own degrading context. +allowed-tools: Bash, Read, Write +model: sonnet +--- + +## Context + +- Date: !`date -u +"%Y-%m-%d"` +- Working directory: !`pwd` +- Project root: !`git rev-parse --show-toplevel 2>/dev/null || pwd` +- Session JSONL: !`python3 -c "import os,glob; cwd=os.getcwd(); home=os.path.expanduser('~'); slug=cwd.replace('/','-'); d=f'{home}/.claude/projects/{slug}'; files=sorted(glob.glob(f'{d}/*.jsonl'), key=os.path.getmtime, reverse=True); print(files[0] if files else 'NOT_FOUND')"` +- Prior distill summary: !`python3 -c "import os; cwd=os.getcwd(); home=os.path.expanduser('~'); slug=cwd.replace('/','-'); p=f'{home}/.claude/distill-summaries/{slug}.md'; print(p if os.path.exists(p) else 'NONE')"` + +# Distill + +You are a context distillation orchestrator. Your job is to extract the current session transcript, build a briefing for a fresh sub-agent, invoke it via `claude -p`, and persist the artifacts. + +The entire distillation pipeline runs via Bash — the transcript and briefing are too large to pass through the Agent tool's prompt parameter (would exceed output token limits). Instead, the briefing is written to a temp file and piped to `claude -p --model sonnet`. + +## Phase 1: Extract and Mask Transcript + +Extract the conversation from the session JSONL file and apply observation masking. Uses `distill_core` module (symlinked to `~/.claude/hooks/` by ai-rules install). + +```bash +python3 << 'PYEOF' +import os, sys +sys.path.insert(0, os.path.expanduser("~/.claude/hooks")) +import distill_core + +jsonl_path = os.environ.get("SESSION_JSONL", "") +if not jsonl_path or jsonl_path == "NOT_FOUND": + jsonl_path = distill_core.get_jsonl_path(os.getcwd()) + +if not jsonl_path: + print("ERROR: No session JSONL found", file=sys.stderr) + sys.exit(1) + +transcript = distill_core.extract_transcript(jsonl_path) + +slug = distill_core.get_project_slug(os.getcwd()) +tmp_path = f"/tmp/distill-transcript-{slug}.txt" +with open(tmp_path, "w") as f: + f.write(transcript) + +print(f"Transcript extracted: {len(transcript)} chars -> {tmp_path}") +PYEOF +``` + +If the output says "ERROR", stop and report the issue to the user. + +## Phase 2: Build Briefing and Run Fresh Sub-Agent + +Read `references/subagent-briefing.md` and `references/summary-template.md` from this skill's directory. Then run the full pipeline — build the briefing, write it to a temp file, and invoke `claude -p --model sonnet` as a fresh-context subprocess: + +```bash +python3 << 'PYEOF' +import os, sys +sys.path.insert(0, os.path.expanduser("~/.claude/hooks")) +import distill_core + +cwd = os.getcwd() +slug = distill_core.get_project_slug(cwd) +transcript_path = f"/tmp/distill-transcript-{slug}.txt" + +if not os.path.exists(transcript_path): + print("ERROR: Transcript file not found", file=sys.stderr) + sys.exit(1) + +with open(transcript_path) as f: + transcript = f.read() + +prior_summary = distill_core.read_prior_summary(cwd) + +briefing_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "distill_briefing.md" +) if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "distill_briefing.md")) else os.path.expanduser("~/.claude/hooks/distill_briefing.md") + +if not os.path.exists(briefing_path): + print("ERROR: Briefing template not found", file=sys.stderr) + sys.exit(1) + +with open(briefing_path) as f: + briefing_template = f.read() + +print("Running fresh-context distillation subprocess...") +summary = distill_core.run_distill_subprocess( + transcript=transcript, + prior_summary=prior_summary, + briefing_template=briefing_template, + cwd=cwd, + timeout=120, +) + +if not summary: + print("ERROR: Distillation subprocess failed or produced no output", file=sys.stderr) + sys.exit(1) + +summary_path, backup_path = distill_core.save_artifacts(cwd, summary, transcript) + +os.remove(transcript_path) + +print(f"SUMMARY_PATH={summary_path}") +print(f"BACKUP_PATH={backup_path}") +print("===DISTILL_SUMMARY_START===") +print(summary) +print("===DISTILL_SUMMARY_END===") +PYEOF +``` + +If the output says "ERROR", report the issue to the user. + +## Phase 3: Output + +Extract the summary from the Phase 2 output (between `===DISTILL_SUMMARY_START===` and `===DISTILL_SUMMARY_END===` markers) and output it to the conversation: + +``` + +[the 9-section summary from the subprocess output] + +--- +Recovery: If details are missing, read the backup transcript at: +`[BACKUP_PATH from Phase 2 output]` + +``` + +## Key Requirements + +- The sub-agent runs as a `claude -p --model sonnet` subprocess — completely fresh context, no access to this conversation +- NEVER attempt to summarize the conversation from your own context — that's the same-context self-summarization anti-pattern we're specifically avoiding +- The transcript comes from the JSONL file on disk, not from in-context messages +- Observation masking replaces tool results with placeholders to reduce noise +- The 120K char cap keeps the sub-agent's input within its context window +- On any error, report clearly — do not silently produce a partial summary diff --git a/src/ai_rules/config/skills/distill/references/subagent-briefing.md b/src/ai_rules/config/skills/distill/references/subagent-briefing.md new file mode 100644 index 0000000..3adc8b9 --- /dev/null +++ b/src/ai_rules/config/skills/distill/references/subagent-briefing.md @@ -0,0 +1,103 @@ +# Distill Sub-Agent Briefing + +You are a specialized context distillation agent. Your job is to produce a high-fidelity structured summary of a coding session transcript. + +You operate in a FRESH context — you have never seen this conversation before. The transcript below is your only source of truth. + +## Date and Project + +- Date: {{DATE}} +- Project: {{PROJECT}} + +## Verbatim Preservation Rules (CRITICAL) + +These rules override all other summarization instincts. Violating them defeats the purpose of distillation. + +**COPY EXACTLY — never paraphrase:** +- File paths: `/src/ai_rules/config/claude/settings.json` not "the settings file" +- Function/class/variable names: `extract_transcript()` not "the extraction function" +- Error codes and messages: `ImportError: No module named 'distill_core'` not "an import error" +- Branch names: `feature/distill-skill` not "the feature branch" +- CLI flags and commands: `claude -p --model sonnet` not "the Claude CLI" +- Config keys: `autoCompactEnabled` not "the auto-compact setting" + +**COPY VERBATIM — user instructions are sacred:** +- Section 7 (User Instructions and Constraints) is the most critical section +- Every "don't do X", "always Y", "use Z approach" must be preserved word-for-word +- Every correction ("no, not that — do this instead") must be captured +- Do not soften, reinterpret, or paraphrase user constraints + +**Recent exchanges get MORE detail, not less:** +- The last 3-5 conversational turns should be summarized with higher fidelity +- These represent the most immediately actionable context + +## Anti-Patterns (DO NOT) + +- "The user and assistant discussed X" → WRONG. State WHAT was decided, not that a discussion happened. +- "Several files were modified" → WRONG. List WHICH files with their full paths. +- "Various approaches were considered" → WRONG. List the specific approaches and their outcomes. +- "The configuration was updated" → WRONG. State which config file, which keys, what values. +- Abstractive paraphrase of technical terms → WRONG. Use the exact terms from the transcript. +- Omitting failed approaches → WRONG. Dead ends prevent re-exploration. + +## Prior Summary + +{{PRIOR_SUMMARY}} + +If a prior summary is provided above, this is an INCREMENTAL distillation: +- Extend the prior summary rather than starting from scratch +- Preserve ALL verbatim content from the prior summary +- Add new information from the transcript that occurred after the prior distillation +- If the prior summary conflicts with the transcript, trust the transcript +- Update section 2 (Current Work State) and section 9 (Next Step) to reflect the latest state + +If "[None — first distillation]" appears above, produce a complete summary from scratch. + +## Output Format + +Produce the summary following this exact structure. Do not add or remove sections. + +--- + +## 1. Primary Request and Intent + +[Concise summary of what the user is trying to accomplish and why] + +## 2. Current Work State + +[Exact current state with verbatim identifiers — branch, files, phase, active work] + +## 3. Key Technical Decisions + +[Each decision: what was decided, why, what was rejected] + +## 4. Files and Code + +[Every file touched — verbatim paths, role, what was done] + +## 5. Errors and Fixes + +[Every error — verbatim message, cause, resolution] + +## 6. Problem Solving Progress + +[Approaches tried, outcomes, direction, dead ends] + +## 7. User Instructions and Constraints + +[EVERY constraint verbatim — this is the most critical section] + +## 8. Pending Tasks + +1. [Task with actionable detail] +2. [Task with actionable detail] + +## 9. Next Step + +[Single most important action with enough context to execute cold] + +--- + +## Transcript + +{{TRANSCRIPT}} diff --git a/src/ai_rules/config/skills/distill/references/summary-template.md b/src/ai_rules/config/skills/distill/references/summary-template.md new file mode 100644 index 0000000..1ab7511 --- /dev/null +++ b/src/ai_rules/config/skills/distill/references/summary-template.md @@ -0,0 +1,94 @@ +# Distill Summary Template + +Produce a structured summary following this exact 9-section format. Each section has a **preservation rule** that controls how content is captured. + +**Verbatim rule:** When a section is marked **verbatim**, copy identifiers EXACTLY as they appear in the transcript — file paths, function names, class names, variable names, branch names, error codes, CLI flags. Never paraphrase, abbreviate, or "clean up" technical identifiers. + +--- + +## 1. Primary Request and Intent + +*Preservation: Summarize concisely* + +What is the user trying to accomplish and why? State the high-level goal, not the individual steps. If the goal evolved during the session, capture the current understanding. + +## 2. Current Work State + +*Preservation: **Verbatim** identifiers* + +What is the exact current state of work? What was the last thing completed? What is actively in progress? Include: +- Current branch name (verbatim) +- Files currently being modified (verbatim paths) +- Current phase/step of the task +- Any active debugging or investigation state + +## 3. Key Technical Decisions + +*Preservation: **Verbatim** identifiers* + +Architecture choices, design patterns, and trade-offs that were made during the session. For each decision, capture: +- What was decided +- Why (the reasoning or constraint) +- What was rejected and why (if discussed) + +## 4. Files and Code + +*Preservation: **Verbatim** paths* + +Every file that was read, created, or modified. For each file: +- Full path (verbatim) +- What role it plays in the current task +- What was done to it (created, modified specific sections, read for context) + +## 5. Errors and Fixes + +*Preservation: **Verbatim** error strings* + +Every error encountered and its resolution. For each: +- The exact error message or code (verbatim) +- What caused it +- How it was fixed (or if it's still unresolved) + +## 6. Problem Solving Progress + +*Preservation: Summarize* + +Approaches tried and their outcomes. What worked, what didn't, and why. Current direction of investigation. Include dead ends — knowing what was already tried prevents re-exploration. + +## 7. User Instructions and Constraints + +*Preservation: **Verbatim** — MOST CRITICAL SECTION* + +EVERY instruction, preference, or constraint the user stated during the session. This is the most important section because user constraints are the #1 casualty of lossy compaction. Capture: +- Explicit instructions ("don't do X", "always Y", "use Z approach") +- Stated preferences ("I prefer...", "let's not...", "that's the wrong approach") +- Corrections ("no, not that — do this instead") +- Scope limitations ("only touch X", "leave Y alone") + +Copy the user's words as closely as possible. Do not paraphrase constraints into softer language. + +## 8. Pending Tasks + +*Preservation: Actionable detail* + +Numbered list of remaining work. Each item must have enough detail for a cold-start agent to execute without re-exploring the codebase: +1. [Task] — [what to do, which files, what approach] +2. [Task] — [what to do, which files, what approach] + +## 9. Next Step + +*Preservation: Enough context to execute cold* + +The single most important next action. Include: +- What to do +- Which file(s) to touch +- Any context needed to execute without reading the full summary + +--- + +## Recovery + +If details are missing from this summary, the full masked transcript is available at: +`{{BACKUP_PATH}}` + +Use `Read` on this file to recover specific details.