Claude Skill

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

LLM Mart · 0 points · 20 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download tobihagemann-turbo-claude_skills_recall-reasoning-bda508a.zip · 6 KB
Part of tobihagemann/turbo — 147 skills

Install

skills CLI npx skills add https://github.com/tobihagemann/turbo/tree/main/claude/skills/recall-reasoning
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tobihagemann-turbo@llmmart
Git 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:

  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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related