Architecture
This document is for contributors and people who want to extend MissionCache. It describes how the pieces fit together, where state lives, and what invariants you need to respect if you want to add a new MCP tool, hook, or slash command without breaking anything downstream.
If you are just looking to use MissionCache, start with the README instead.
Mental model#
MissionCache is not one program. It is six small programs that agree on two files and one database:
- Two files:
~/.missioncache/<status>/<project>/<project>-tasks.mdand<project>-context.md. These are the human- and Claude-readable source of truth for what a project is doing. - One database:
~/.missioncache/tasks.db(SQLite). This is the source of truth for cross-project metadata: which projects exist, when they were last worked on, how much time was spent, and which repository they belong to.
Everything else is either a producer (hooks write heartbeats, MCP tools create files) or a consumer (the dashboard reads the DB, the statusline reads both DB and files, MissionCache Auto reads the tasks file and writes iteration logs). When you are trying to understand any component, ask: is it writing to the files, writing to the DB, or reading them?
Component map#
| Component | Directory | What it does | When it runs |
|---|---|---|---|
| MCP server | mcp-server/ |
Exposes ~30 tools to Claude Code over stdio for managing projects, files, time, plans, and iteration logs | One subprocess per Claude Code session, started on demand via uvx |
| missioncache-db | missioncache-db/ |
SQLite schema, data classes, and all direct DB operations. Every other component calls into this instead of opening the DB themselves | In-process library - embedded by MCP server, hooks, MissionCache Auto, and dashboard |
| Hooks | hooks/ |
Short-lived Python scripts Claude Code invokes on session lifecycle events (prompt submitted, session started, context compacting, session stopped) | One shot per event, all four hooks have a timeout budget between 5 and 30 seconds |
| Commands | commands/ |
Slash command markdown files that describe workflows to Claude. They do not run code - Claude reads them and decides which MCP tools to call | Parsed by Claude Code on plugin load, rendered when user types /missioncache:<name> |
| missioncache-auto | missioncache-auto/ |
Standalone CLI that runs Claude in a loop over a task file, in either sequential or parallel-with-DAG mode | A long-running user-invoked process, one execution per missioncache-auto <project> call |
| Dashboard | missioncache-dashboard/ |
FastAPI backend plus a single-file HTML frontend at http://localhost:8787 for visualizing time, tasks, sessions, and MissionCache Auto runs |
One persistent process, usually managed by launchd |
| Statusline | missioncache-dashboard/missioncache_dashboard/statusline.py |
~1,350-line Python script that produces a 6-7 line ANSI status block shown at the bottom of the Claude Code TUI. Shipped inside the missioncache-dashboard package and exposed via the missioncache-statusline console entry point |
Invoked by Claude Code after every message, gets JSON on stdin, must be fast (sub-200ms target) |
| Rules | rules/ |
Plain markdown files describing MissionCache conventions to Claude. Auto-installed into ~/.claude/rules/ by the SessionStart hook using a write-if-different copy. missioncache-install seeds the initial copies (symlinked in --local mode, copied with an ownership marker otherwise); the hook replaces stale symlinks on first run |
Refreshed on every SessionStart event |
Two files at .claude-plugin/ wire the plugin into Claude Code: plugin.json registers the MCP server and metadata, and marketplace.json catalogs MissionCache as an installable plugin so the repo itself doubles as a one-plugin marketplace. End users install the full experience via uvx missioncache-install, or just the plugin core via /plugin marketplace add missioncache/missioncache followed by /plugin install missioncache@missioncache. Maintainers run uvx missioncache-install --local from a clone, which creates a separate local marketplace at ~/.claude/plugins/local-marketplace/ and installs the plugin from there as missioncache@local for fast iteration without pushing to GitHub.
Talking to the database through missioncache-db#
missioncache-db is the only place in the codebase that writes raw SQL. Everything else goes through its Python API. This is intentional - the schema has grown over time, there are triggers, and there is an invariant about where claude_session_cache lives (covered below) that you will violate the first time you try to open the DB directly from somewhere else.
The package exposes a single TaskDB class that wraps a sqlite3.Connection with WAL mode enabled. You get it by calling TaskDB() (no arguments - it finds the DB at ~/.missioncache/tasks.db). Every other component constructs its own instance; there is no global or singleton, because the consumers (hooks, MCP subprocess, MissionCache Auto, dashboard) are all different processes.
The file is ~3,400 lines, but the mental model is small:
- A handful of schema tables (see Storage below).
- One method per use case (
find_task_for_cwd,record_heartbeat,process_heartbeats,get_task_time,create_task,complete_task,get_repo_breakdown,scan_repos, ...). - Helpers for path-to-repo resolution, tag extraction, and heartbeat-to-session aggregation.
If you find yourself wanting to add an SQL query anywhere else in the repo, add a method to TaskDB first and call it from your component. This keeps the schema refactorable.
How a Claude session flows through MissionCache#
This is the best way to build intuition for the component boundaries: follow one user prompt from the moment it is typed to the moment the dashboard reflects it.
1. Session start#
When Claude Code starts a new session in a repo, it fires the SessionStart hook. hooks/session_start.py does four things, in order:
- Resolves the session ID from
CLAUDE_SESSION_IDor stdin JSON. - Writes a
term-sessionmapping file so that the statusline can later map terminal IDs back to session IDs. - Calls
db.find_task_for_cwd(cwd, session_id)to see if the current directory belongs to a tracked MissionCache project. - If a task is found, writes
~/.claude/hooks/state/projects/<session-id>.json- the per-session project pointer used both by the statusline (to render the active project name) and byTaskDB.find_task_for_cwdon subsequent prompts (to resolve which task a heartbeat belongs to). (A legacypending-task.jsonfile used to be written here but was removed in mcp-orbit 0.2.13; see CHANGELOG.)
The hook also prints a short "Active Task Detected" block to stdout, which Claude Code injects into the conversation context. This is how Claude learns which project it is working on without the user having to say so.
2. User submits a prompt#
Every time the user hits enter, Claude Code fires UserPromptSubmit. MissionCache registers two independent scripts on this hook:
hooks/activity_tracker.pyspawns a short-lived subprocess (python -m missioncache_db heartbeat-auto) with a hard 2-second timeout andPYTHONPATHset to the plugin-bundledmissioncache-db. The subprocess callsTaskDB.record_heartbeat_auto(cwd, session_id), which delegates tofind_task_for_cwd- that checksprojects/<session-id>.jsonand then pattern-matchescwdagainst~/.missioncache/active/<task>/and repo-local legacydev/active/layouts. If a match is found, a row is inserted into theheartbeatstable with the current timestamp and session ID. The subprocess boundary is deliberate: SQLite lock contention on the task DB can otherwise stall the heartbeat call for up to its 5-secondbusy_timeout, which would eat the entireUserPromptSubmithook budget. The 2-second subprocess deadline bounds that worst case. Skip patterns filter out slash commands, shell commands, yes/no confirmations, and empty prompts so they do not inflate the count.hooks/task_tracker.pychecks for "divergence" between the tasks file and the context file (headings like### Task 3present in context while- [ ] 3. ...is still unchecked in tasks) and prints a reminder to stdout so Claude sees it. This is the guardrail that keeps the tasks file honest.
Both hooks have a 5-second budget. If no task matches the current directory or session, they both exit silently; there is no penalty for running MissionCache in a repo that has no projects.
3. Claude uses MCP tools#
When the user (or a slash command) asks Claude to do something that touches MissionCache state - "mark task 3 complete", "give me the current project status", "record an iteration" - Claude calls one of the mcp__plugin_missioncache_pm__* tools. Those tools live in the MCP server subprocess that was started by the plugin manifest.
The MCP server is stdio-based and very thin. mcp-server/src/mcp_missioncache/server.py is 30 lines: it imports five tool modules, each of which registers its tools against a shared FastMCP instance from app.py. Every tool:
- Opens a
TaskDBvia a lazyget_db()helper. - Calls a handful of
TaskDBmethods. - Returns a Pydantic-validated dict.
- Catches
MissionCacheErrorand returns it as a structured error dict.
No tool writes to the DB on its own. No tool opens a second database file. No tool spawns subprocesses. If a new tool needs to do something weird, the fix is usually to add a method to TaskDB or a helper to helpers.py, not to deviate from this pattern.
The MCP tool call is what turns "user intent expressed in English" into "row inserted in SQLite". This is the primary write path for projects, task updates, completion, and MissionCache file creation.
4. Context compaction#
Claude Code fires PreCompact when it is about to compress the conversation to fit in the context window. hooks/pre_compact.py has 30 seconds to do three things:
- Find the task for the current cwd.
- Open
<project>-context.md, update the**Last Updated:**line, and add a compaction note under## Recent Changes. - Call
db.process_heartbeats()to aggregate unprocessed heartbeats into thesessionstable.
This is the load-bearing piece of MissionCache's memory story. Without it, the context file would go stale between sessions and Claude would lose the thread on resume. The time budget is larger than the other hooks because heartbeat processing is not free on large DBs.
5. Session stop#
hooks/stop.py runs when the session ends. It reads the transcript file Claude Code points it at, checks whether any Write or Edit tool calls happened during the session, and if there is an active task with MissionCache files, prints a stderr reminder to run /missioncache:save. It does not touch the DB - its job is purely to nudge the user.
6. Dashboard reflection#
Separately from the per-session flow above, the dashboard is a persistent process reading from a DuckDB copy of the SQLite DB. At startup (or on explicit GET /api/sync) it copies tables from ~/.missioncache/tasks.db into ~/.missioncache/tasks.duckdb, which it then uses for fast aggregate queries. The dashboard also reads MissionCache files directly from disk to render task modals, and it reads ~/.claude/projects/<cwd>/*.jsonl files (Claude Code's own transcript store) to render untracked activity.
See The dual-database pattern for the details of why there are two DB files and what trade-offs it makes.
The fork path#
The six steps above are one linear trace of a prompt, and a fork is not a step in it. It is a second shape the project state can take, so it is worth naming here.
A project created with /missioncache:fork carries a **Fork of:** <parent> line in its context header. The child is a full project with its own plan, context, tasks, and clock. What it shares is the parent's context file, which becomes the knowledge layer every child and the parent's own sessions read. Tasks are never shared or copied.
The header is the durable link. tasks.parent_id is derived from it by the scan's reconcile pass, not authored directly. get_context_digest on a fork returns a parent_digest block so /missioncache:load can tell you when a sibling session changed the shared layer, using a per-session marker at ~/.claude/hooks/state/shared-seen/<session-id>.json that the statusline also reads. Full detail in forks.md.
Storage#
MissionCache's state lives in three places:
- Structured data in SQLite at
~/.missioncache/tasks.db, plus the DuckDB analytics mirror at~/.missioncache/tasks.duckdb. - Human-readable project state in markdown files under
~/.missioncache/{active,completed}/<project>/. - Ephemeral hook state in JSON files under
~/.claude/hooks/state/.
SQLite schema#
The canonical tables are all defined in missioncache-db/missioncache_db/__init__.py. There is one more table (claude_session_cache) that is created by the dashboard, which is why this table list has two sections.
Defined by missioncache-db:
| Table | Purpose | Key columns |
|---|---|---|
repositories |
Tracked git repos | id, path (UNIQUE), short_name, active, last_scanned_at |
tasks |
Projects (both coding and non-coding) | id, repo_id, name, full_path, parent_id, status, type, tags (JSON), category, jira_key, branch, pr_url, last_worked_on |
task_updates |
Append-only progress notes for non-coding projects | task_id, note, created_at |
heartbeats |
WakaTime-style activity pings, one per non-skipped user prompt | task_id, timestamp, session_id, processed |
sessions |
Aggregated work sessions, computed from heartbeats by process_heartbeats() |
task_id, session_id, start_time, end_time, duration_seconds, heartbeat_count |
config |
Key-value settings (idle timeout, assumed work seconds, prune threshold, tag keywords) | key, value |
auto_executions |
One row per missioncache-auto run |
task_id, started_at, completed_at, status, mode, worker_count, total_subtasks, completed_subtasks, failed_subtasks |
auto_execution_logs |
Streaming log lines from MissionCache Auto workers | execution_id, worker_id, subtask_id, level, message, timestamp |
Triggers keep updated_at fresh on every row update and set completed_at/archived_at when status transitions into the corresponding state. The tasks table has a UNIQUE(repo_id, full_path) constraint that matters during scan - rescanning the same repo should not produce duplicate rows.
tasks.parent_id is the fork linkage (see forks.md). You never author it directly. It is derived from the **Fork of:** <parent> line in the child's context header: the scan's reconcile pass sets it, re-heals it when the link was lost, and clears it when the header goes away. The header is the source of truth, the column is the index - idx_tasks_parent is what makes the children lookup fast. The FK is ON DELETE SET NULL, which is why delete_task refuses to delete a parent that still has children rather than silently unlinking them.
Created by the dashboard (also in ~/.missioncache/tasks.db):
| Table | Purpose | Key columns |
|---|---|---|
claude_session_cache |
Cache of Claude Code's JSONL session transcripts, populated by missioncache-dashboard/missioncache_dashboard/lib/analytics_db.py. Used to compute "untracked" sessions (Claude activity with no MissionCache project loaded) and to merge JSONL time with MissionCache heartbeat time |
session_id, file_path, date, hour, cwd, git_branch, project_path, message_count, tool_call_count, input_tokens, output_tokens, duration_seconds |
Invariant: claude_session_cache and sessions must be in the same SQLite file. The dashboard's "show me untracked Claude activity" query is a LEFT JOIN ... WHERE s.session_id IS NULL anti-join between the two tables. If you split them into separate databases, the anti-join silently returns empty and all untracked sessions disappear from the dashboard.
This is the thing that will bite you first if you refactor the storage layer. There is a note in the context file about it and it is worth respecting.
The dual-database pattern#
The dashboard reads from DuckDB (~/.missioncache/tasks.duckdb), not from SQLite directly. The trade-off is explicit:
- Writes go to SQLite. Heartbeats, task creation, completion, scanning, session aggregation - all of that is standard SQLite via
missioncache-db. SQLite handles concurrent writers across processes well enough for MissionCache's write volume. - Analytics reads go to DuckDB. The dashboard runs a lot of aggregate queries (day-of-week buckets, 30-day heatmaps, repo breakdowns, trend deltas), and DuckDB is 10-100x faster than SQLite for those.
- Sync is explicit. On dashboard startup and on
GET /api/sync, the dashboard copies tables from SQLite into DuckDB. There is no continuous streaming replication.migrate_to_duckdb.pyis the standalone script version of the same sync.
The practical consequence is that the dashboard is always slightly stale. A heartbeat recorded ten seconds ago will not show up until the next sync. In exchange, the "Activity History" screen renders 90 days of data in under a second.
If you are adding a new dashboard endpoint: do the read against DuckDB via analytics_db.py. If you are adding a new MCP tool: do the write against SQLite via missioncache-db. Cross the streams only if you have a very specific reason.
Files on disk#
Projects have their own directory on disk under ~/.missioncache/:
~/.missioncache/
├── active/
│ └── <project-name>/
│ ├── <project-name>-plan.md # Optional: implementation plan
│ ├── <project-name>-context.md # Key decisions, gotchas, waiting-on, next steps
│ ├── <project-name>-journal.md # Recent Changes overflow (oldest first), auto-managed by the 12-entry cap
│ ├── <project-name>-tasks.md # Checkboxes - source of truth for progress
│ ├── <project-name>-auto-log.md # Written by missioncache-auto during runs (optional)
│ └── prompts/ # Optional: optimized per-subtask prompts
│ ├── README.md
│ ├── task-01-prompt.md
│ └── ...
└── completed/
└── <project-name>/ # Same layout, moved on /missioncache:done
The full_path column in the tasks table stores "active/<name>" or "completed/<name>", which is how the DB stays in sync with the on-disk status. /missioncache:done moves the directory and updates the row in one step.
There is also a legacy layout under <repo>/dev/{active,completed}/ that older projects still use. Dashboard and hooks fall back to it if the centralized path does not exist. If you are touching path resolution, check both mcp-server/src/mcp_missioncache/helpers.py (for MCP writes) and parse_missioncache_progress() in missioncache-dashboard/missioncache_dashboard/server.py (for dashboard reads). They are independent implementations - keep them consistent.
Ephemeral hook state#
~/.claude/hooks/state/ holds small JSON files that coordinate between hooks and the statusline:
| File | Written by | Read by | Purpose |
|---|---|---|---|
projects/<session-id>.json |
session_start.py, /missioncache:load, /missioncache:new (via get_task / create_missioncache_files server-side binding), /missioncache:done (removes) |
statusline.py, TaskDB.find_task_for_cwd |
Which project is active for a given session. Read on both the statusline rendering path (to show the project name) and the heartbeat path (to attribute time to the right task when cwd does not match a known task directory) |
term-sessions/<term-id> |
session_start.py |
statusline.py |
Maps terminal-emulator session IDs back to Claude session IDs so mid-session lookups work from any tab |
pending-project.json |
(nothing) | TaskDB.find_task_for_cwd priority-1 branch |
Inverse legacy: read but never written. The priority-1 branch in find_task_for_cwd is effectively dead code - task resolution always falls through to the projects/<session-id>.json branch |
~~pending-task.json~~ |
(removed in mcp-orbit 0.2.13) | (never read) | Removed. Old files left on disk from pre-0.2.13 installs are harmless; the rename-sweep also stopped maintaining them. Safe to delete by hand |
These files are deliberately plain JSON and deliberately per-session. Early versions of MissionCache used a single shared project file that race-conditioned badly once multiple Claude sessions ran concurrently. If you add new state here, shard by session ID by default.
Process model#
It is easy to get confused about what is running where, because MissionCache has a mix of long-running processes, stdio subprocesses, per-event short-lived scripts, and an embedded library. Here is the full picture:
| Runs | Process type | Lifetime | Triggered by |
|---|---|---|---|
| Dashboard | Long-running (launchd-managed in practice) | Until stopped | Manual start or launchd |
| MCP server | Stdio subprocess | One per Claude Code session | Claude Code on plugin load |
Hooks (session_start, pre_compact, stop, activity_tracker, task_tracker) |
Short-lived one-shot | Single event | Claude Code hook events |
| missioncache-auto | User-invoked CLI | Until task complete or failed | Manual missioncache-auto <project> |
| missioncache-auto workers | Subprocesses of missioncache-auto | One per subtask in parallel mode | parallel.py |
| Statusline | Very short-lived | ~50-200ms per invocation | Claude Code after every message |
Rules and slash commands are not processes - they are files read by Claude Code.
Two implications worth keeping in mind:
- There is no MissionCache daemon. Everything except the dashboard runs on demand. If the dashboard is down, heartbeats still accumulate in SQLite; the dashboard will just look stale until it syncs.
- There is no IPC except the database. Hooks do not call the MCP server. The statusline does not call the dashboard (well, it does fetch usage data from a separate endpoint, but that is unrelated to MissionCache state). Dashboards and MCP tools do not coordinate. All of them read and write the same SQLite file (plus, optionally, the files on disk).
This shape was a deliberate choice. It makes the system easy to reason about and easy to run partially (you can skip the dashboard, skip the statusline, even skip the hooks and still have a working MissionCache), at the cost of losing some real-time-ness that a centralized daemon would give you for free.
Extension points#
Here is how to do the six most common things you might want to extend MissionCache to do.
1. Add a new MCP tool#
Pick the tool module that matches your use case:
- Task lifecycle (create, list, complete, reopen, update):
tools_tasks.py - MissionCache file operations (create files, update context, mark tasks complete in the markdown, get progress):
tools_docs.py - Time tracking (heartbeats, sessions, repos):
tools_tracking.py - MissionCache Auto iteration logging:
tools_iteration.py - Multi-agent planning:
tools_planning.py
Then follow the pattern every existing tool uses:
from typing import Annotated
from pydantic import Field
from .app import mcp
from .db import get_db
from .errors import MissionCacheError
@mcp.tool()
async def my_tool(
param: Annotated[str, Field(description="What this parameter is for")],
) -> dict:
"""One-line summary shown in Claude's tool picker. Keep it short."""
db = get_db()
try:
return {"success": True, ...}
except MissionCacheError as e:
return e.to_dict()
except Exception as e:
logger.exception("Error in my_tool")
return {"error": True, "message": str(e)}
The @mcp.tool() decorator registers your function against the shared mcp instance. As long as the containing module is imported by server.py, the tool will show up as mcp__plugin_missioncache_pm__my_tool in Claude Code. If you add a new module file, add the from . import tools_mything line to server.py too.
Important: MCP tool docstrings and parameter descriptions are the only thing Claude sees when picking tools. A vague docstring is a bug - the tool will never get called or will be called at the wrong time. Write the docstring like you are writing a commit title.
After adding the tool, reinstall the plugin. If you are hacking on MissionCache locally from a clone, the fast path is the local marketplace that uvx missioncache-install --local set up:
claude plugins install missioncache@local
If you are iterating against a marketplace-installed copy instead, push your changes to GitHub and run claude plugins update missioncache@missioncache. Either way, restart the Claude Code session afterwards - MCP tool registration is cached at plugin load time.
2. Add a new hook#
Decide which Claude Code hook event you want to react to. The supported events are documented in Claude Code's hook reference - MissionCache currently uses four: SessionStart, UserPromptSubmit, PreCompact, Stop.
Write a Python script in hooks/ that:
- Reads the hook JSON from stdin (for events that provide it).
- Does its work within the event's time budget (typically 5-10 seconds;
PreCompactgets 30). - Exits silently on any error. Hooks should never block Claude Code from running.
- Uses
from missioncache_db import TaskDBif it needs DB access - never opens SQLite directly.
Add it to hooks/hooks.json under the corresponding event:
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/my_hook.py", "timeout": 5 }
]
}
]
Multiple hooks on the same event run in the order they are listed. They share no state other than what they write to disk. If you need to coordinate with another hook, use a file under ~/.claude/hooks/state/.
Reinstall the plugin and restart Claude Code for the hook definition changes to take effect.
3. Add a new slash command#
Slash commands are markdown files in commands/. There is no code. Create commands/<name>.md:
---
description: "Short description shown in /help"
argument-hint: "[optional args]"
---
# What this command does
<instructions for Claude, written as prose>
The frontmatter is parsed by Claude Code for the help menu. The body is prose instructions that Claude follows when the user types /missioncache:<name>. You can reference MCP tools by their mcp__plugin_missioncache_pm__* names and Claude will call them in order.
If your command needs to be explicit about when to call which tool, write a numbered workflow with MCP tool names inline. See commands/load.md for a good example of a multi-step workflow with conditional branches.
Reinstall the plugin (or use /reload-plugins for command-only changes) to pick up new commands.
4. Add a new missioncache-auto mode or worker behavior#
missioncache-auto is structured around three files you will probably want to touch:
missioncache-auto/missioncache_auto/dag.py- builds the dependency graph from prompt frontmatter. Touch this if you are changing how subtasks discover their dependencies.missioncache-auto/missioncache_auto/sequential.pyorparallel.py- the two execution strategies. They shareworker.pyfor the actual Claude subprocess spawning.missioncache-auto/missioncache_auto/worker.py- what a single iteration does: load task file, spawn Claude, parse learning tags from the response, decide if the task is done, update state.
If you add a new mode (say, a "review mode" that spawns a code reviewer after every task), put the top-level orchestration in a new module parallel to sequential.py and parallel.py, and route to it from cli.py. Do not fork worker.py unless you have a very good reason - the completion-detection logic (<what_worked>, <promise>COMPLETE</promise>) is subtle and worth sharing.
db_logger.py is what writes to auto_executions and auto_execution_logs. If your new mode needs to be visible in the dashboard, log through it.
5. Customize the statusline#
missioncache-dashboard/missioncache_dashboard/statusline.py is a single ~1,350-line file because it is performance-sensitive - every import costs milliseconds at the bottom of every Claude message. It ships inside the missioncache-dashboard pip package and is exposed as the missioncache-statusline console entry point. The layout is:
- Constants and colors at the top.
- Data collection functions that read from the DB, the projects state dir, git, and the file system.
- Rendering functions that build each of the 6-7 lines.
main()at the bottom that stitches them together.
If you want to add a new line, add a rendering function and call it from main() before the final print. If you want to change what an existing line shows, find the matching rendering function by grep-ing for its label.
The statusline has its own environment-variable-based configuration (search for STATUSLINE_ in the file). Document any new variable at the top of the file next to the existing ones.
Do not add dependencies. The statusline uses stdlib only because installing packages into the wrong Python would silently break it for every user. If you absolutely need a third-party library, wire it up via missioncache-db or analytics_db.py instead and call that.
6. Run the dashboard somewhere non-default#
The dashboard reads from ~/.missioncache/tasks.db and ~/.missioncache/tasks.duckdb and listens on port 8787 by default. To change:
DUCKDB_PATHandSQLITE_PATHinmissioncache-dashboard/missioncache_dashboard/lib/analytics_db.pycontrol the read paths.- The port is the argument to
uvicorn.run(...)inmissioncache-dashboard/missioncache_dashboard/server.py. MISSIONCACHE_DASHBOARD_URLis the env var the statusline uses to build OSC 8 hyperlinks to the dashboard. If you change the dashboard's host or port, set this in your shell init.
launchd config lives at ~/Library/LaunchAgents/com.missioncache.dashboard.plist. It explicitly points at /opt/homebrew/bin/python3.11 because MissionCache's DuckDB install is Python-version-specific. Do not switch it to system Python or python3 without reinstalling DuckDB under that Python.
Invariants and gotchas#
These are the things that will trip you up if you are new to the codebase, collected from bugs that have actually shipped.
- Never open
~/.missioncache/tasks.dboutside ofmissioncache-dboranalytics_db. The schema has triggers, the WAL settings matter, andclaude_session_cacheis implicitly required to live in that same file. Opening it from a new script almost always breaks something. claude_session_cachemust stay intasks.db, nottasks.duckdb. The untracked-session anti-join is aLEFT JOINagainst thesessionstable. Separate databases means the join silently returns empty.full_pathon thetasksrow must match the actual directory location under~/.missioncache/./missioncache:donemoves the directory and updatesfull_pathand setsstatus=completed. If you write code that does one and not the others, you get orphan tasks that the dashboard renders in the wrong list.- Hook failures must be silent. Any exception in any hook will be swallowed by a top-level
try/exceptand printed to stderr. This is intentional - a broken hook should never block Claude Code from working. If your new hook throws uncaught exceptions, Claude Code will still keep running, but you will never know the hook is broken. - MCP tool docstrings are the tool description. They are not optional. Claude reads them to decide when to call the tool.
projects/<session-id>.jsonis the only path that routes automatic heartbeats to a task whencwdis outside~/.missioncache/active/<task>/.session_start.pyis its sole writer, and it only writes it when it resolves a task atSessionStart. If a user loads a project mid-session via/missioncache:loadin a session that did not resolve at start, subsequentUserPromptSubmitheartbeats will not attribute to the new task (cwd pattern matching will not help, and this file will not exist)./missioncache:loadmasks this by recording an explicit initial heartbeat, but that covers only the one call. If you add a new "load project mid-session" command, write this file yourself or accept the tracking gap.process_heartbeats()drains unprocessed heartbeats into aggregated sessions in a single pass. Each row is aggregated exactly once and then markedprocessed=1, so calling it twice is safe (the second call is a no-op). Calling it zero times means newly accumulated time will not appear in the dashboard untilPreCompactfires. missioncache-auto and some MCP tools invoke it manually to keep the dashboard fresh.- Repo ID resolution is order-dependent in
scan_repos(). If you callcreate_missioncache_fileswith a brand-new repo path, the same call will register the repo and may assign the task'srepo_idto the wrong row if there are unresolved ambiguities. This has been fixed multiple times; if you are touching that path, re-test the "brand new repo with existing tasks" case manually. - The dashboard's
_get_jsonl_task_times()joins bycwd = repo.path. Multiple tasks sharing a repo all draw from the same JSONL pool, and the only disambiguator isc.date >= DATE(t.created_at). Overlapping tasks on the same repo can therefore double-count - known limitation. _effective_time(task_id, heartbeat, jsonl) = max(heartbeat, jsonl)is applied in/api/tasks/active,/api/tasks/completed, and/api/task/<id>. Dashboard-reported times are merged MissionCache+Claude-JSONL, not pure heartbeats. If you are debugging a time discrepancy between missioncache-db and the dashboard, this is almost always why.UserPromptSubmitcan receivepromptas a list of content blocks (when the user attaches an image). Any hook or tool that treatspromptas a string without flattening the list will crash.activity_tracker.pyandtask_tracker.pyboth handle this - copy the pattern.- The
**Fork of:**header regex is hand-mirrored in two places and must stay byte-identical._FORK_NAME_REatmissioncache-db/missioncache_db/context_health.py:494and_FORK_HEADER_REatmissioncache-dashboard/missioncache_dashboard/statusline.py:682are the same pattern, written out twice. The statusline is a stdlib-only standalone script and cannot importmissioncache_db, so it cannot share the constant. The pattern's shape - no slashes, must lead with an alphanumeric - is a load-bearing security control: it blocks path traversal through a hand-edited context header, and that header is a plain text file any user can write.missioncache-dashboard/tests/test_statusline_fork.py:219asserts the two patterns are equal. If you change one, change the other in the same commit, or that test fails, which is the point.
Where to go from here#
This doc is the foundation; the other component docs assume you have read it.
dashboard.md- dashboard screens, API endpoints, sync, customization.forks.md- when to fork, the shared context layer, theFork of:header, parallel-session freshness.missioncache-auto.md- sequential vs parallel, DAG resolution, learning tags, worker model.mcp-tools.md- full MCP tool reference with parameters and return shapes.statusline.md- statusline layout, configuration, icons.hooks.md- hook event reference, state files, adding new hooks.