This file provides guidance to AI coding agents working with code in this repository.
Tempyr is a file-based knowledge graph system for AI-assisted product and technical design. It sits between a PRD helper, a project management system, and an AI-centric task system. The primary interaction model is an AI-assisted interview flow that decomposes brain dumps into typed graph nodes and edges.
The full specification lives in docs/graphspec.md. The journal subsystem (append-only agent reasoning log) has its own spec at docs/journal-spec.md. Read the relevant sections before implementing any component.
Core principles:
- Files (markdown + YAML frontmatter) are the source of truth; git provides versioning
- Documents (PRDs, TDDs) are rendered views (queries) over the graph, not stored artifacts
- Hybrid retrieval: structural traversal + BM25 full-text + vector similarity, all from a derived SQLite index
- The interview is the product: AI interviews the user, proposes nodes/edges, commits on approval
- Zero infrastructure: single binary, no servers,
git clone+tempyr index rebuild
cargo build # build
cargo test # run all tests
cargo test <test_name> # run a single test
cargo test --lib # lib tests only (no integration tests)
cargo clippy # lint
cargo fmt --check # check formatting
cargo run -- <subcommand> # run the CLIRust edition is 2024. Target toolchain is stable.
The project is a Rust workspace with eight crates:
| Crate | Purpose |
|---|---|
tempyr-core |
Graph data model: node/edge parsing, schema validation, in-memory graph, traversal, temporal filtering |
tempyr-index |
SQLite indexing: FTS5 full-text, sqlite-vec embeddings, hybrid retrieval pipeline, incremental updates |
tempyr-interview |
Interview state machine: session management, phase transitions, gap detection, LLM-based extraction |
tempyr-render |
Document rendering: TOML template parsing, graph collection, markdown output |
tempyr-linear |
Linear integration: push/pull sync, status mapping, context generation |
tempyr-journal |
Session journal: append-only JSONL of agent reasoning (decisions, dead ends, etc.) under <git-common-dir>/tempyr/journals/ with cross-platform locking and secret redaction |
tempyr-cli |
CLI binary (tempyr): clap-based, all user-facing commands |
tempyr-mcp |
MCP server library used by tempyr --mcp: exposes graph operations as tools for Claude Code |
Crate dependency order: core and journal are leaves. index/interview/render/linear depend on core. cli/mcp depend on the layer above plus journal.
- Nodes are
.mdfiles with YAML frontmatter ingraph/<type>/directories (e.g.,graph/features/feat-session-replay.md) - Edges are stored bidirectionally in YAML frontmatter — both source and target files contain the edge. Edge lists are sorted alphabetically by target.
- Schema (
schema.toml) defines node types, required fields, allowed statuses, and valid edge types with reverse mappings - Index (
.tempyr/index.db) is a derived SQLite database (gitignored, rebuildable) containing structural data, FTS5, and vector embeddings
- LLM is for extraction only. Gap detection, phase transitions, duplicate checking, and graph operations are deterministic Rust code. The LLM extracts structured data from natural language — it doesn't make control-flow decisions.
- Bidirectional edge sync. Every
add-edgewrites both files atomically.validatecatches drift. Edge type pairs are defined inschema.toml(e.g.,child_of<->parent_of). - Temporal edges. Edges have optional
valid_from/valid_untilfor point-in-time rendering. Nodes have astatuslifecycle (e.g.,superseded). Decisions get superseded, not deleted. - Content-hash embedding cache. Embeddings are keyed by blake3 hash of the markdown body (not frontmatter). Re-embed only when body changes.
- Token budget enforcement. Hybrid retrieval greedily fills context by combined score until the token budget is exhausted.
The interview is a 5-phase state machine: Discovery -> Product -> Technical -> Decomposition -> Review. Each phase has typed gaps (e.g., MissingPersona, NoTechnicalDecision) that drive contextual questions. Phase transitions happen when required gaps in the current phase are filled. All proposals are tentative until the user commits.
rusqlite(bundled) +sqlite-vec— index storageserde+serde_yaml+serde_json+toml— serializationtokio— async runtime (MCP server, API calls)reqwest— LLM/embedding API clientclap— CLIblake3— content hashingwalkdir— filesystem traversalchrono+uuid— timestamps and session IDs
When your work corresponds to a Tempyr task node (type task in graph/tasks/), keep it updated as you go using graph_update_node. This applies whether you were explicitly given a task ID, or you can identify the matching task via graph_search or graph_list.
| When | Set status to |
|---|---|
| Starting work on the task | in_progress |
| Blocked by something outside your control | blocked |
| Work is complete (code written, tests pass) | done |
Update status as soon as the transition happens — not batched at the end.
Append implementation context to the task body at natural milestones:
- Approach chosen: if you made a non-obvious design decision, note it briefly
- Key files touched: list the primary files so reviewers know where to look
- Blockers or surprises: anything that deviated from the original plan
Use graph_update_node with the body field. Read the existing body first (via graph_get_node) and append — don't overwrite.
If you're given a task description but not an ID:
graph_searchwith keywords from the task descriptiongraph_listfiltered to typetaskwith statusbacklogorin_progress- If no matching task exists, proceed without tracking — don't create task nodes ad hoc
The journal is an append-only log of agent reasoning -- decisions, dead ends, findings, plans, risks -- stored as JSONL under <git-common-dir>/tempyr/journals/ and published as Git refs (refs/tempyr/journals/archive/<YYYY>/<MM>/<DD>/<id>). It runs alongside the graph: the graph is curated knowledge that outlives this session, the journal is how that knowledge was reached (and how it changes when you have to throw it out). Spec: docs/journal-spec.md.
Use journal_log (MCP) or tempyr journal log (CLI) freely as you work. The eight kinds:
| Kind | When |
|---|---|
plan |
What you're about to attempt and why |
finding |
Something you learned by reading code or running a tool |
assumption |
Something you're acting on without verifying (polarity required) |
question |
Something you don't know yet -- to ask or look up |
decision |
A choice with reasoning (chosen, rationale, reversible required; detail >= 50 chars) |
dead_end |
An approach that didn't work (approach, failure_mode required; detail >= 50 chars). Highest signal -- future agents read these to avoid repeating you. |
risk |
A potential problem identified but not yet hit (severity recommended) |
outcome |
The result of a plan (passed, optional commit_sha); set final = true to close the session and trigger publish |
Log freely on dead ends and decisions -- the journal is empty if you don't, and a missing entry teaches no one. Successes alone are low-signal here.
These transitions emit a journal entry automatically -- don't also call journal_log for them:
| Trigger | Emits |
|---|---|
tempyr status <task> in_progress (or MCP graph_update_node from backlog) |
plan (provisional) |
Task in_progress -> done |
outcome with passed = true, final = true |
Task in_progress -> blocked |
risk with severity = blocker |
interview start |
plan (provisional) |
interview answer |
finding (provisional); plus finding for any phase advance |
interview adjust/add_node/add_edge |
finding (provisional) |
interview commit |
outcome with final = true |
A failed auto-emit is downgraded to a warning, never aborts the underlying mutation.
Before re-deriving something, search the journal:
journal_search "<query>"-- hybrid retrieval (BM25 + vec0 RRF + recency + kind boost). Pass--rerankto run a BGE cross-encoder over the top 50 RRF candidates and re-sort by relevance -- better on close calls (e.g. lexically distant but semantically on-topic). Filter by--kind dead_endto surface "approaches that didn't work."journal_get <id>-- fetch one entry by id, transparently refreshes the index on a cache miss.journal_range "<A..B>"-- list entries written while one of the in-range commits was checked out. Pairs withgit log A..Bfor "what reasoning happened during this span of work?" Accepts any range expressiongit rev-listunderstands (A..B,HEAD~10..HEAD,feature..main).journal_blame <file>-- every entry whosefilesfield referenced this path. The why complement ofgit blame's who/when: surfaces decisions, dead-ends, and findings tied to a specific file. Highest signal when the file accumulated several dead-ends before its current shape.
Sessions are per (worktree, agent) pair. The first journal_log opens one; subsequent calls reuse it. Closing the session means writing a .ready marker, which the publisher (tempyr journal flush) archives as a Git ref and pushes to the remote.
Three ways a session ends:
- An entry with
final = true(your ownoutcome, or an auto-emitted task-done / interview-commit) -> marker written immediately. - The
SessionEndClaude Code hook -> runstempyr journal finalize, idempotent. - Manual:
tempyr journal finalizefrom the shell.
The SessionStart hook runs tempyr journal bootstrap to ensure the layout exists. Both hooks are no-ops outside a git repo, so opening Claude Code in a non-tempyr directory is safe.
tempyr doctor shows a Journal section: open / ready session counts, publisher lock state, stamped PID. Use it when sessions seem to be queuing up locally -- usually means the publisher hasn't run.
tempyr journal lint flags task nodes with status = in_progress that have no journal entries referencing them. Catches the "edited the frontmatter directly without going through tempyr status" failure mode. The managed pre-commit git hook runs this in warn-only mode (never blocks a commit); pass --strict for CI to exit non-zero on warnings.
tempyr journal stats prints aggregate signal: kind distribution, dead-end ratio, sessions per agent, top tags / files, and a per-day activity histogram. Useful for spotting usage anomalies -- a low dead-end rate or a flat activity histogram during active work usually means agents aren't reaching the journal. The journal_stats MCP tool returns the same data as JSON.
- Node IDs are human-readable kebab-case slugs (e.g.,
feat-session-replay,decision-storage-backend) - Never manually rename node IDs — use
tempyr renamewhich updates all references atomically - Node granularity rule: one decision, one fact, or one concept per node. If it can't be independently linked, it's a paragraph, not a node.
- Interview extraction prompts use temperature 0.1 for structured JSON output
- The binary name is
tempyr