spelunkcloud

spelunk memory guide

How spelunk's project memory works — kinds, supersede chains, point-in-time queries, harvesting from git history, and git-notes storage.

spelunk memory is a per-project knowledge store. Use it to capture decisions, context, requirements, questions, and handoff notes that would otherwise live only in chat history or someone's head.

Code tells you what the system does. Memory tells you why it was built that way.


Storage

Memory entries are stored in a local SQLite database by default, and — with store_in_git_notes enabled (the default) — also written through to refs/notes/spelunk on HEAD, so they travel with the repository. No external database or server is required.

  • Use --backend git-notes to make git-notes the primary backend instead of SQLite.
  • Point server_url at a shared spelunk-server to sync memory across a team (see server setup).
  • Entries are searchable by full text at all times; semantic search (by meaning) is available when a server is running — the local one is autostarted on demand.

git-notes write-through: when store_in_git_notes is true (the default), spelunk memory add also appends the entry to refs/notes/spelunk on HEAD. Failure to write the note is non-fatal — a warning is logged and the primary SQLite write is unaffected. Outside a git repo this is a graceful no-op.


Memory kinds

KindUse for
decisionArchitecture or design choices with rationale
contextBackground information that helps understand the codebase
requirementProduct or technical requirements
noteGeneral observations (default)
questionOpen questions that need an answer
answerAnswers to previously stored questions
handoffState transfer between work sessions or agents
intentActive work signal; surfaced by spelunk check with file-overlap warnings
antipatternThings to avoid; list with spelunk memory failures

spelunk memory failures is a shortcut for spelunk memory list --kind antipattern.


Adding entries

# Quick note with body inline
spelunk memory add --title "Chunker uses 120-line sliding window as fallback" \
              --body "This applies to unsupported file types and binary-adjacent files." \
              --kind context \
              --tags chunker,indexer

# Open your $EDITOR for the body (omit --body)
spelunk memory add --title "Decision: use blake3 for file hashing" --kind decision

# Link to specific files
spelunk memory add --title "Auth middleware refactored" \
              --body "Moved session validation to src/auth/middleware.rs" \
              --files "src/auth/middleware.rs,src/auth/session.rs"

When --body is omitted, spelunk opens $VISUAL or $EDITOR (falling back to vi). Lines starting with # are stripped (comment convention).

From a URL

--from-url fetches content from a GitHub issue, Linear ticket, or any web page and stores it as a memory entry. The title is inferred from the page automatically.

# GitHub issue — uses `gh api` for clean structured content
spelunk memory add --from-url https://github.com/owner/repo/issues/42

# Override the inferred title
spelunk memory add --from-url https://github.com/owner/repo/issues/42 \
              --title "Auth: session token storage compliance issue" \
              --kind requirement

# Any URL — fetches page title and strips HTML
spelunk memory add --from-url https://linear.app/myteam/issue/ENG-1234/... \
              --kind context

For GitHub issues, spelunk calls gh api to get structured issue data (requires the GitHub CLI and gh auth login). For all other URLs it does an HTTP GET and extracts readable text.


Supersede chains and valid_at / invalid_at

Every memory entry has a valid_at timestamp (when it became true) and an optional invalid_at timestamp (when it stopped being true).

# Record when a decision became valid (ISO 8601)
spelunk memory add --title "Adopted monorepo layout" --kind decision \
              --valid-at 2026-01-15

# Supersede an old entry — archives it, sets its invalid_at, and records a
# supersedes edge to the new entry
spelunk memory add --title "New auth approach" --kind decision --body "..." \
              --supersedes <old-id>

# Same thing via the dedicated subcommand: archive old, add replacement
spelunk memory supersede <id> --title "..." --body "..."

# Mark two entries as related — creates a relates_to edge (no archiving)
spelunk memory add --title "Follow-up note" --kind note --body "..." \
              --relates-to <other-id>

Use --supersedes whenever an earlier decision changes — it keeps a traceable chain of reasoning. Use --relates-to for non-superseding connections (a follow-up note, a contradicting observation).

Point-in-time queries

valid_at / invalid_at make it possible to reconstruct what was known at any past date:

# Only entries that were valid at this date
spelunk memory list --as-of 2026-01-01
spelunk memory search "auth decisions" --as-of 2026-01-01

This is useful for post-mortems or understanding old decisions in the context they were made.


Searching, listing, and showing

# Semantic search — finds entries by meaning
spelunk memory search "why did we choose sqlite"
spelunk memory search "authentication decisions" --limit 5

# Also surface 1-hop relates_to neighbours of each result
spelunk memory search "authentication decisions" --expand-graph

# Search mode: hybrid (default), semantic, text
spelunk memory search "auth" --mode semantic
spelunk memory search "auth" --mode text

# List recent entries (newest first)
spelunk memory list
spelunk memory list --kind decision
spelunk memory list --limit 50

# Filter by commit SHA (exact or prefix)
spelunk memory list --source-ref abc1234

# Show a single entry — full body plus relationship edges
spelunk memory show 42
spelunk memory show 42 --format json

question and answer entries show titles only in list view to avoid context saturation. Use spelunk memory show <id> to read the full body.


Tracking topic evolution

spelunk memory timeline returns all entries related to a topic, sorted by the time they became valid — useful for understanding how a decision or understanding evolved.

spelunk memory timeline "authentication strategy"
spelunk memory timeline "database choice" --limit 30
spelunk memory timeline "auth" --format json

Relationship graph

# Show all edges for an entry (text)
spelunk memory graph 42

# Machine-readable
spelunk memory graph 42 --format json

memory graph and --expand-graph surface supersedes, relates_to, and contradicts edges with linked entry titles.


Harvesting from git history

spelunk memory harvest reads your git log, sends commit messages to the LLM, and automatically extracts significant entries. Requires llm_model in ~/.config/spelunk/config.toml (and a server with an LLM backend configured — the local autostarted server provides this once llm_model is set).

# Default: last 10 commits
spelunk memory harvest

# Custom range
spelunk memory harvest --git-range HEAD~30..HEAD
spelunk memory harvest --git-range v1.0..HEAD

# Source: git log (default), Claude Code session history, or antipatterns
spelunk memory harvest --source git
spelunk memory harvest --source claude-code
spelunk memory harvest --source failures

Already-harvested commits are skipped (tracked via a git:<sha> tag). Routine commits ("fix typo", "wip", etc.) are ignored by the LLM.

Automatic harvesting

Install the git hook and harvesting happens on every commit:

spelunk hooks install

This writes a post-commit hook that runs spelunk index and spelunk memory harvest after each commit (both --detach, so git is not blocked).


Syncing with a team server

# Push local entries to the configured server
spelunk memory push
spelunk sync                 # alias for `spelunk memory push`

# Pull entries created after a Unix timestamp
spelunk memory since <unix-ts>

# Stream new entries as they arrive (Server-Sent Events)
spelunk memory watch

spelunk memory push reads your local memory database and sends all active entries to the server. Archived entries are skipped by default; pass --include-archived to push them. See server setup for configuring server_url and server_key.


Machine-readable output

All memory commands support --format json, and setting AGENT=true forces JSON mode globally:

AGENT=true spelunk memory list --kind question
AGENT=true spelunk memory search "database decisions"

spelunk plumbing read-memory emits memory entries as JSONL for scripting.


Tips

  • Store the "why", not just the "what" — the code already captures what was built.
  • Use question kind actively — when you hit a decision point you're unsure about, store it. Come back with spelunk memory list --kind question at the start of the next session.
  • Use handoff kind at the end of a long session to summarise the current state for your next session (or for another agent).
  • Tag entries — tags like auth, database, performance make spelunk memory list more scannable and improve search relevance.
  • Use --supersedes when updating a decision — it archives the old entry, sets its invalidation time, and creates a traceable edge so you can always follow the chain of reasoning.
  • Use --relates-to for non-superseding connections.
  • Use --as-of for archaeology.

What's next

On this page