recall-reasoning
Recall the reasoning behind a past change by locating the Claude Code transcript that produced it. Use when the user asks to "recall reasoning", "find reasoning", "look up reasoning", "recall implementation reasoning", "find the rationale", "why did I do X", "recall from transcri
Install
npx skills add https://github.com/tobihagemann/turbo/tree/main/claude/skills/recall-reasoning
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tobihagemann-turbo@llmmart
git clone https://github.com/tobihagemann/turbo.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tobihagemann/turbo collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Recall Reasoning
Locate the Claude Code transcript that produced a given change and extract the implementer's reasoning. Useful for answering reviewer questions, writing post-hoc explanations, or recovering forgotten context.
Inputs
Accept any of:
- A commit SHA
- A file path, optionally with a line number (
<path>:<line>) - A reviewer question plus surrounding context (file and line)
If only a file is given, git blame resolves the commit that last touched the line.
Step 1: Resolve the Commit and Run the Script
Call scripts/find_transcript.py with either --commit <sha> or --file <path>[:<line>]. Pass --cwd /path/to/repo when searching a different repo than the current working directory.
python3 <skill-dir>/scripts/find_transcript.py --file <path>:<line>
python3 <skill-dir>/scripts/find_transcript.py --commit <sha>
The script:
- Resolves the commit via
git rev-parseorgit blame - Enumerates directories under the effective Claude configuration home's
projects/tree and filters their transcripts by authoritativecwdrecords, covering path encoding, truncation, collisions, and subdirectory launches - Ranks candidate transcripts whose mtime is within
--window-daysof the commit (default 14) - Scores candidates by mentions of touched files and tool-use edits on them
- Extracts cleaned user prompts and substantive assistant text from the top candidates
The JSON output has status, commit, project_dir (the top candidate's directory when matched), project_dirs (directories with matching cwd records in the time window), and candidates with session_id, score, match_reasons, and excerpts.
Status values:
ok— candidates returnedno-commit— couldn't resolve a commitno-transcripts— the effective Claude configuration home has no project transcript directoriesno-match— transcripts exist but none match the touched files in the window
Step 2: Read and Synthesize
If status is ok:
- Start with the top candidate (highest score). Read its excerpts first.
- If the excerpts already explain the change, stop. If they are thin or ambiguous, read the full transcript at
jsonl_pathdirectly for more context. - Ignore candidates with low scores or scores far below the top — they are false positives.
- When the reasoning spans multiple excerpts, quote the most specific one.
Synthesize a concise summary tied to the question being answered:
- Lead with the why. The diff already shows the what.
- Quote the implementer's own words when they already say it well.
- Keep it to one or two paragraphs. Don't narrate the whole session.
If status is anything other than ok, report that no reasoning was found and fall back to reading the commit diff and surrounding code. Say so explicitly so it's clear whether the answer still holds up.
Step 3: Output
Return the reasoning in this shape:
**Commit:** <short-sha> — <subject>
**Transcript:** <session-id> (score <N>)
<one or two paragraphs of reasoning, quoting the implementer where useful>
If no transcript was found:
**Commit:** <short-sha> — <subject>
**Transcript:** none found (<status>)
<fallback explanation derived from reading the commit and current code>
Then use the TaskList tool and proceed to any remaining task.
Rules
- Treat excerpts as evidence, not ground truth. The implementer's intent at the time may have changed. If the current code contradicts an excerpt, note the discrepancy.
- Only read the full
.jsonlif the excerpts are insufficient. These files are large. - Never quote noise prefixes like
<command-message>or skill-loading stubs. The script filters these out, but stay alert if reading a transcript directly. - If multiple candidates have similar high scores, name both and prefer the one whose time window contains the commit.
Files (turbo)
-
scripts
-
find_transcript.py 14.8 KB
#!/usr/bin/env python3 """Find the Claude Code transcript that produced a given commit and extract reasoning excerpts. Usage: python3 scripts/find_transcript.py --commit <sha> python3 scripts/find_transcript.py --file <path>[:<line>] python3 scripts/find_transcript.py --file <path>:<line> --window-days 14 Resolves a commit SHA (directly or via `git blame`), locates candidate Claude Code project transcript directories under the effective configuration home (`CLAUDE_CONFIG_DIR` or `~/.claude`), ranks candidate transcripts by time overlap and file/tool-use references, and extracts relevant user prompts and assistant text from the top candidate. Output: JSON on stdout with commit metadata, candidate list, and excerpts. Exit codes: 0 on success (even if no transcripts found), 1 on usage errors, 2 on git failures. """ import argparse import json import os import subprocess import sys from datetime import datetime, timedelta, timezone from pathlib import Path def run_git(args, cwd=None): result = subprocess.run( ['git', *args], cwd=cwd, capture_output=True, text=True, ) if result.returncode != 0: return None return result.stdout def resolve_commit(commit, file_ref, repo_root): """Return commit SHA, resolving via git blame if needed.""" if commit: sha = run_git(['rev-parse', commit], cwd=repo_root) if sha is None: return None return sha.strip() if file_ref: path, _, line = file_ref.partition(':') line = line or '1' blame = run_git(['blame', '-L', f'{line},{line}', '--porcelain', path], cwd=repo_root) if blame is None: return None blame_lines = blame.splitlines() first_line = blame_lines[0] if blame_lines else '' sha = first_line.split(' ', 1)[0] if first_line else '' if not sha or sha == '0000000000000000000000000000000000000000': return None return sha return None def get_commit_meta(sha, repo_root): """Return dict with sha, timestamp (ISO), message, files touched.""" info = run_git( ['show', '-s', '--format=%H%n%cI%n%s%n%b', sha], cwd=repo_root, ) if info is None: return None lines = info.splitlines() if len(lines) < 3: return None files = run_git(['show', '--name-only', '--format=', sha], cwd=repo_root) or '' return { 'sha': lines[0], 'timestamp': lines[1], 'subject': lines[2], 'body': '\n'.join(lines[3:]).strip(), 'files': [f for f in files.splitlines() if f], } def claude_projects_root(): """Return the effective Claude Code transcript-storage root.""" config_home = os.environ.get('CLAUDE_CONFIG_DIR') if config_home: return Path(config_home).expanduser() / 'projects' return Path.home() / '.claude' / 'projects' def project_transcript_dirs(): """Return Claude Code project directories for authoritative cwd filtering.""" projects_root = claude_projects_root() if not projects_root.is_dir(): return [] return sorted( (path for path in projects_root.iterdir() if path.is_dir()), key=lambda p: p.stat().st_mtime, reverse=True, ) def iter_transcripts(project_dirs): transcripts = [ transcript for project_dir in project_dirs for transcript in project_dir.glob('*.jsonl') ] return sorted(transcripts, key=lambda p: p.stat().st_mtime, reverse=True) def parse_timestamp(value): if not value: return None try: return datetime.fromisoformat(value.replace('Z', '+00:00')) except ValueError: return None def load_transcript_records(path, max_records=20000): """Stream-load a transcript JSONL file, capping records to avoid runaway memory.""" records = [] try: with path.open('r', encoding='utf-8', errors='replace') as f: for i, line in enumerate(f): if i >= max_records: break line = line.strip() if not line: continue try: records.append(json.loads(line)) except json.JSONDecodeError: continue except OSError: return [] return records def transcript_belongs_to_project(records, repo_root): """Return whether a transcript was launched in the repo or a subdirectory.""" root = str(repo_root) seen = set() for record in records: cwd = record.get('cwd') if not cwd or cwd in seen: continue seen.add(cwd) resolved = str(Path(cwd).expanduser().resolve()) if resolved == root or resolved.startswith(root + os.sep): return True return False def transcript_time_range(records): timestamps = [] for r in records: ts = parse_timestamp(r.get('timestamp')) if ts is not None: timestamps.append(ts) if not timestamps: return (None, None) return (min(timestamps), max(timestamps)) def extract_text(message_content): """Pull plain text out of assistant or user message content (string or list of parts).""" if message_content is None: return '' if isinstance(message_content, str): return message_content if isinstance(message_content, list): parts = [] for p in message_content: if not isinstance(p, dict): continue t = p.get('type') if t == 'text' and p.get('text'): parts.append(p['text']) elif t == 'tool_use': name = p.get('name', '') inp = p.get('input') or {} parts.append(f'[tool_use:{name} {json.dumps(inp)[:500]}]') return '\n'.join(parts) return '' def score_transcript(records, commit_ts, touched_files): """Return (score, reasons, relevant_record_indices) for how well this transcript matches the commit.""" score = 0 reasons = [] relevant_indices = [] # Time overlap start, end = transcript_time_range(records) if start is not None and end is not None and commit_ts is not None: # Session must have had activity before the commit if start <= commit_ts: score += 2 reasons.append('session active before commit') if start <= commit_ts <= end + timedelta(hours=2): score += 3 reasons.append('commit within session window') file_hits = {} tool_hits = {} for idx, r in enumerate(records): is_relevant = False text_blob = '' msg = r.get('message') or {} content = msg.get('content') if content is not None: text_blob = extract_text(content) # Full-path text mentions only. Basename-only matching would false-positive on common filenames # (e.g., index.ts, main.py) appearing across unrelated transcripts in the time window. for full_path in touched_files: if full_path and full_path in text_blob: file_hits[full_path] = file_hits.get(full_path, 0) + 1 is_relevant = True # Tool uses referencing touched files if isinstance(content, list): for part in content: if not isinstance(part, dict) or part.get('type') != 'tool_use': continue name = part.get('name', '') if name not in ('Edit', 'Write', 'MultiEdit', 'NotebookEdit'): continue inp = part.get('input') or {} fp = inp.get('file_path', '') for f in touched_files: # Match full equality or a proper path suffix; plain endswith would # false-match (e.g., '/src/foobar.py'.endswith('bar.py') is True). if f and (fp == f or fp.endswith('/' + f)): tool_hits[f] = tool_hits.get(f, 0) + 1 is_relevant = True break if is_relevant: relevant_indices.append(idx) score += sum(file_hits.values()) score += 3 * sum(tool_hits.values()) if file_hits: reasons.append(f'mentions {len(file_hits)} touched file(s): {", ".join(sorted(file_hits))[:200]}') if tool_hits: reasons.append(f'edits {len(tool_hits)} touched file(s)') return score, reasons, relevant_indices NOISE_PREFIXES = ( '<command-message>', '<command-name>', '<command-args>', '<local-command-stdout>', '<local-command-caveat>', '<bash-input>', '<bash-stdout>', '<bash-stderr>', '<system-reminder>', '<task-notification>', 'Base directory for this skill:', 'Caveat: The messages below were generated', '(Re-invocation of', ) # Read like ordinary prose, so only harness-injected user records are filtered. USER_NOISE_PREFIXES = ( 'Called the ', 'Skill /', 'Another Claude session sent', ) def clean_text(content, rtype): """Extract and clean message text, returning None for non-reasoning content.""" text = extract_text(content).strip() if not text: return None if rtype == 'user' and isinstance(content, list): if all(isinstance(p, dict) and p.get('type') == 'tool_result' for p in content): return None if any(text.startswith(p) for p in NOISE_PREFIXES): return None if rtype == 'user' and text.startswith(USER_NOISE_PREFIXES): return None if rtype == 'assistant' and '[tool_use:' in text: lines = [l for l in text.split('\n') if not l.startswith('[tool_use:')] text = '\n'.join(lines).strip() return text or None def extract_excerpts(records, relevant_indices, commit_ts, max_excerpts=40): """Return reasoning-relevant excerpts. Always captures every non-sidechain user prompt in the session (the intent is gold), plus substantive assistant text near relevant tool calls (filters out short transitions). """ excerpts = [] seen = set() interesting = set() for idx in relevant_indices: for j in range(max(0, idx - 2), min(len(records), idx + 3)): interesting.add(j) for idx, r in enumerate(records): if r.get('type') == 'user' and not r.get('isSidechain'): interesting.add(idx) for idx in sorted(interesting): if idx in seen: continue seen.add(idx) r = records[idx] rtype = r.get('type') if rtype not in ('user', 'assistant'): continue if r.get('isSidechain'): continue msg = r.get('message') or {} text = clean_text(msg.get('content'), rtype) if text is None: continue # Assistant: skip short transitional messages unless they sit directly at a relevant tool call if rtype == 'assistant' and len(text) < 80 and idx not in relevant_indices: continue excerpts.append({ 'index': idx, 'role': rtype, 'timestamp': r.get('timestamp'), 'text': text[:2000], }) if len(excerpts) >= max_excerpts: break return excerpts def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument('--commit', help='commit SHA to look up (overrides --file)') parser.add_argument('--file', help='file path, optionally FILE:LINE to use git blame') parser.add_argument('--window-days', type=int, default=14, help='max days between commit and transcript mtime (default 14)') parser.add_argument('--limit', type=int, default=3, help='max candidate transcripts to report (default 3)') parser.add_argument('--cwd', default=os.getcwd(), help='project directory (default: current working directory)') args = parser.parse_args() if not args.commit and not args.file: print('error: --commit or --file is required', file=sys.stderr) return 1 repo_root_raw = run_git(['rev-parse', '--show-toplevel'], cwd=args.cwd) if repo_root_raw is None: print('error: not a git repository', file=sys.stderr) return 2 repo_root = Path(repo_root_raw.strip()).resolve() sha = resolve_commit(args.commit, args.file, repo_root) if sha is None: print(json.dumps({ 'status': 'no-commit', 'error': 'could not resolve a commit (blame returned no author, or commit not found)', })) return 0 meta = get_commit_meta(sha, repo_root) if meta is None: print(json.dumps({'status': 'no-commit', 'error': f'commit {sha} not found'})) return 0 commit_ts = parse_timestamp(meta['timestamp']) storage_dirs = project_transcript_dirs() result = { 'status': 'ok', 'commit': meta, 'project_dir': None, 'project_dirs': [], 'candidates': [], } if not storage_dirs: result['status'] = 'no-transcripts' result['error'] = 'no Claude Code transcript directories under the effective config home' print(json.dumps(result, indent=2)) return 0 touched = meta['files'] window = timedelta(days=args.window_days) scored = [] matching_dirs = set() for path in iter_transcripts(storage_dirs): # Coarse pre-filter: skip files whose mtime is far from the commit to avoid # loading and parsing every historical transcript. score_transcript() uses # the precise in-file timestamps for the final time-overlap check. mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc) if commit_ts is not None and abs(mtime - commit_ts) > window: continue records = load_transcript_records(path) if not records or not transcript_belongs_to_project(records, repo_root): continue matching_dirs.add(path.parent) score, reasons, rel_idx = score_transcript(records, commit_ts, touched) if score <= 0: continue excerpts = extract_excerpts(records, rel_idx, commit_ts) scored.append({ 'session_id': path.stem, 'jsonl_path': str(path), 'mtime': mtime.isoformat(), 'score': score, 'match_reasons': reasons, 'excerpt_count': len(excerpts), 'excerpts': excerpts, }) scored.sort(key=lambda c: c['score'], reverse=True) result['candidates'] = scored[: args.limit] result['project_dirs'] = [str(path) for path in sorted(matching_dirs)] if not result['candidates']: result['status'] = 'no-match' result['error'] = 'no transcript in the window matched the touched files' else: result['project_dir'] = str(Path(result['candidates'][0]['jsonl_path']).parent) print(json.dumps(result, indent=2)) return 0 if __name__ == '__main__': sys.exit(main())
-
-
SKILL.md 4.2 KB
--- name: recall-reasoning description: "Recall the reasoning behind a past change by locating the Claude Code transcript that produced it. Use when the user asks to \"recall reasoning\", \"find reasoning\", \"look up reasoning\", \"recall implementation reasoning\", \"find the rationale\", \"why did I do X\", \"recall from transcripts\", or \"find the transcript for this commit\"." --- # Recall Reasoning Locate the Claude Code transcript that produced a given change and extract the implementer's reasoning. Useful for answering reviewer questions, writing post-hoc explanations, or recovering forgotten context. ## Inputs Accept any of: - A commit SHA - A file path, optionally with a line number (`<path>:<line>`) - A reviewer question plus surrounding context (file and line) If only a file is given, `git blame` resolves the commit that last touched the line. ## Step 1: Resolve the Commit and Run the Script Call `scripts/find_transcript.py` with either `--commit <sha>` or `--file <path>[:<line>]`. Pass `--cwd /path/to/repo` when searching a different repo than the current working directory. ```bash python3 <skill-dir>/scripts/find_transcript.py --file <path>:<line> python3 <skill-dir>/scripts/find_transcript.py --commit <sha> ``` The script: 1. Resolves the commit via `git rev-parse` or `git blame` 2. Enumerates directories under the effective Claude configuration home's `projects/` tree and filters their transcripts by authoritative `cwd` records, covering path encoding, truncation, collisions, and subdirectory launches 3. Ranks candidate transcripts whose mtime is within `--window-days` of the commit (default 14) 4. Scores candidates by mentions of touched files and tool-use edits on them 5. Extracts cleaned user prompts and substantive assistant text from the top candidates The JSON output has `status`, `commit`, `project_dir` (the top candidate's directory when matched), `project_dirs` (directories with matching `cwd` records in the time window), and `candidates` with `session_id`, `score`, `match_reasons`, and `excerpts`. Status values: - `ok` — candidates returned - `no-commit` — couldn't resolve a commit - `no-transcripts` — the effective Claude configuration home has no project transcript directories - `no-match` — transcripts exist but none match the touched files in the window ## Step 2: Read and Synthesize If `status` is `ok`: 1. Start with the top candidate (highest score). Read its excerpts first. 2. If the excerpts already explain the change, stop. If they are thin or ambiguous, read the full transcript at `jsonl_path` directly for more context. 3. Ignore candidates with low scores or scores far below the top — they are false positives. 4. When the reasoning spans multiple excerpts, quote the most specific one. Synthesize a concise summary tied to the question being answered: - Lead with the **why**. The diff already shows the what. - Quote the implementer's own words when they already say it well. - Keep it to one or two paragraphs. Don't narrate the whole session. If `status` is anything other than `ok`, report that no reasoning was found and fall back to reading the commit diff and surrounding code. Say so explicitly so it's clear whether the answer still holds up. ## Step 3: Output Return the reasoning in this shape: ``` **Commit:** <short-sha> — <subject> **Transcript:** <session-id> (score <N>) <one or two paragraphs of reasoning, quoting the implementer where useful> ``` If no transcript was found: ``` **Commit:** <short-sha> — <subject> **Transcript:** none found (<status>) <fallback explanation derived from reading the commit and current code> ``` Then use the TaskList tool and proceed to any remaining task. ## Rules - Treat excerpts as evidence, not ground truth. The implementer's intent at the time may have changed. If the current code contradicts an excerpt, note the discrepancy. - Only read the full `.jsonl` if the excerpts are insufficient. These files are large. - Never quote noise prefixes like `<command-message>` or skill-loading stubs. The script filters these out, but stay alert if reading a transcript directly. - If multiple candidates have similar high scores, name both and prefer the one whose time window contains the commit.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.