A local-only console for agent history — what your AI agents did, across Claude Code, Codex, and vendor data exports. Find the half-remembered decision, recover its evidence, and resume its source when that source still exists. As they land, correlation and context briefs will read equally well to humans and agents.
Developed with AI assistance. See the git history for which agents contributed.
Huginn is Thought; Muninn is Memory. Huginn answers "what are my agents doing right now." Muninn answers "what did we do, decide, and learn." They are complementary and share a single menubar surface — Roost — because nobody wants two ravens up there.
Status: early, but no longer minimal. Storage, ingest, the daemon, search with structured filters, corpus calibration, enrichment and hybrid retrieval all work and are covered by tests. The console is not built. Semantic search needs
uv sync --extra semantic(or a plugin) — without a provider it refuses rather than quietly falling back to lexical. See Roadmap.
Three problems that grep over your transcripts does not solve.
Your agents did work you will need again, but their memory is disposable by
default. Some agent tools delete local transcripts; Claude Code sweeps session
JSONLs older than cleanupPeriodDays (default 30 days) on startup, and
subagent transcripts go with their parent. On the machine this was developed on,
the oldest surviving transcript was 29 days old and everything older was already
gone. Muninn's index is an archive of record: for much of a corpus it is the only
surviving copy.
You do not remember your own wording. You remember the situation. That is a recall problem, not a ranking problem, which is why retrieval is hybrid (lexical + semantic) rather than a better regex.
You want the moment something was decided, not every line where a word appears. That needs enrichment at index time, not smarter matching at query time.
For example: ask “Which conversation decided that rate limiting belongs in the core rather than the provider?” Muninn can return the matching session, decision excerpt, repository, date, provenance, and whether the original transcript is still present.
Requires Python 3.12+ and uv.
git clone https://github.com/tohuw/muninn.git
cd muninn
uv sync# Run it as a service. This is the normal way to run Muninn.
uv run muninn serve
uv run muninn install-agent # ...and have it start at login
# Ingest local transcripts once, by hand. Idempotent; safe to run repeatedly.
uv run muninn index
# Search the archive.
uv run muninn search "extension point"
uv run muninn search "auth redirect" --since 2026-06 --repo muninn
# What did I do last week, in order.
uv run muninn log --since 2026-07
# What do I already know about where I am working? Takes a place, not a question.
uv run muninn recall
uv run muninn recall --repo cyberwise
# Why is this file the way it is? Its commits, and the work behind each one.
uv run muninn why src/auth.py
# Read one session in full (id prefixes are fine).
uv run muninn show a7efca23
# Reopen a session in the tool that created it — if its transcript still exists.
uv run muninn resume a7efca23
# Import a claude.ai or ChatGPT data export.
uv run muninn import ~/Downloads/data-2026-08-01.zip
# Survey your corpus and derive calibration (see below).
uv run muninn survey
# Extract topic/outcome/decisions from substantive sessions. Costs model calls —
# --dry-run tells you how many before you spend any.
uv run muninn enrich --dry-run
uv run muninn search "the retry decision" --outcome fixed
# Health: index lag, queue, calibration drift, daemon and login-agent state.
uv run muninn doctorAgents should prefer an installed muninn command, and installing it is
worth doing:
uv tool install --editable /path/to/muninnAn uninstalled CLI is indistinguishable from a missing feature. Observed
behaviour: an agent runs muninn search, gets command not found, and greps
the source instead — permanently, after one failure — which cannot answer a
history question at all while returning something plausible enough that the
substitution goes unnoticed.
A checkout remains a complete, no-global-install fallback:
uv run --directory /path/to/muninn muninn search "extension point"Use that form when changing PATH is not wanted; it preserves the CLI as the
archive boundary either way.
Coming from claudex or codexdex? uv run muninn backfill ingests their prose
indexes — see Superseding the predecessors.
muninn serve is what makes the archive-of-record guarantee real. It sweeps on
startup, then drains the SessionEnd queue and reacts to transcript changes for
as long as it runs — so a session written while nothing was watching is still
recovered, and a session deleted by Claude Code's 30-day sweep was captured
before it went. It also publishes Muninn's row in the shared menubar. It is the
normal way to run Muninn, and install-agent below is how you stop having to
remember it; muninn index remains a one-shot ingest for when you want to watch
one happen.
It is built to be supervised, not to supervise itself: it records
~/.local/state/muninn/daemon.json (pid, port, and where to relaunch it from) and
exits cleanly on SIGTERM, SIGHUP and Ctrl-C, withdrawing everything it
published. muninn doctor reports whether it is running, its port, and whether
its menubar descriptor is published.
Only one ingest loop may run at a time — a second is refused, naming the first —
because two loops would drain one queue twice and fight over one descriptor.
muninn index --watch is still the foreground/debug path: the same ingest loop
with no port and no state file, for watching ingest happen on a console.
uv run muninn install-hooks # a SessionEnd hook, so a session is queued
uv run muninn install-hooks --check # the moment it ends. --check never writes.The hook only ever enqueues: it shares a 1.5 s budget with every other
SessionEnd hook, so it must not open the archive. The daemon drains the queue.
While muninn serve runs it publishes a raven descriptor into a shared
directory and answers GET /api/menu on loopback. The menubar that reads those is
Roost — a separate Apache-2.0 project, one
macOS menu bar / Windows tray item that renders whichever ravens are running.
Install it from its own repository; Muninn does not ship, depend on, or install
it, and publishing is best-effort, so ingest never pays for a menubar.
The menu shows recent sessions, archive counts, index freshness, the embedding backlog, and — at the bottom — Quit Muninn and Restart Muninn. Restart tears the daemon down completely and brings it back in the same process, so it comes back on a new port with a freshly published descriptor. There is no Start row on purpose: a stopped daemon has withdrawn its descriptor, so there is no menu for one to live in. Starting at login is the next section's job.
Roost's SPEC.md is normative for the wire format. Muninn's producer side is
docs/specs/009-raven-descriptor-menu.md
and 017-menu-lifecycle-actions.md,
including the two decisions worth knowing before reading the code: the only thing
a menu row can do is stop or restart the daemon (everything else is a link), and
there is no token_path, so the port is unauthenticated by design and the
Host/Origin checks are the only thing defending it. Spec 017 explains why those
two facts are compatible — and why an action that touched the archive would not
be.
Muninn is absent from the menubar when its daemon is not running. That is a legitimate steady state, not a bug: no descriptor exists, and Roost draws nothing for a raven it cannot find — the same as one that was never installed. A crashed daemon's stale descriptor renders as "Not running" with the reason on screen, because Roost checks the recorded pid.
uv run muninn install-agent # macOS: a LaunchAgent; Linux: a systemd user unit;
# Windows: an HKCU Run entry
uv run muninn uninstall-agent # removes it, and stops the daemon it supervisesOn macOS, install-agent also builds ~/Applications/Muninn.app. It is a
managed Finder/Spotlight entry for this checkout; it starts the same local
service and is removed by uninstall-agent. Muninn refuses to replace an
unrelated bundle at that path.
The mechanism is the shared corvidae
package, so Huginn and Muninn share one implementation rather than each carrying a
copy. Both ravens can be installed at once — that is the point of a shared
menubar — and every path, label and registry value Muninn uses is disjoint from
Huginn's.
What each platform actually gives you differs, and the difference is not papered
over: macOS restarts the daemon if it dies, Linux restarts it on failure only
(so systemctl --user stop muninn stays effective, and a headless host also wants
loginctl enable-linger $USER), and the Windows Run key starts it once per login
and never restarts it.
install-agent refuses while an ingest loop is already running, because a
supervisor relaunching a process that exits immediately is a restart loop rather
than a service. Stop the loop and run it again; muninn doctor names what holds
the lock, and now also reports whether a login agent is installed.
One thing to know before relocating state: the installed agent runs from the OS's
environment, not the shell you installed from, so XDG_STATE_HOME and
RAVENS_STATE_DIR exported in a terminal have no effect at login. Set them
somewhere login sessions see (launchctl setenv, a systemd user drop-in).
install-agent checks this rather than leaving you to find out from a log file.
If the paths your shell resolves are not the ones a login session will, it
refuses and prints both sides — because the failure mode is not an error, it is a
daemon that comes up every morning ingesting a different archive than you think.
Pass --force if you have already set them where login sees them. muninn doctor reports the same divergence for an agent that is already installed, which
catches an environment that changed afterwards.
Muninn parses ~/.claude/projects/**/*.jsonl and
~/.codex/sessions/**/rollout-*.jsonl directly. An earlier prototype calibrated
from a derived prose index and undercounted conversations by 15–27%, because
that index was 7 days stale. Deriving facts from a derived artifact compounds
staleness.
Because the transcript format is explicitly not a stable API and can change on any release, every adapter fails soft: a malformed record or missing field is a counted parse failure, never an exception. Parse-failure rates are reported so a format change shows up as a rising rate rather than as silently missing history.
Failures are also enumerated, not only counted. A total can say sixteen
enrichment failures and nothing more — the affected sessions cannot be found,
re-run, or even confirmed to still be broken, so diagnosing one meant opening the
SQLite file by hand. muninn doctor now names the recent ones and says whether
each has since recovered, because an enrichment failure writes no facets and is
retried on the next pass, so most heal on their own and reporting them all as
outstanding sends you chasing finished work. Both records are kept: the totals
are lifetime and answer "is this getting worse", the log is bounded and answers
"which ones, and do I still care".
Indexing runs in three layers, cheapest first, because the source deletes itself and any single layer can miss:
- A
SessionEndhook writes a job to a queue directory the moment a session ends. This is the primary path: event-driven, no polling, and a session is archived seconds after it finishes. The hook is deliberately tiny — it imports no parser and no SQLite — because it runs inside a ~1.5 s budget the agent gives it. - A watcher (
watchfiles) reacts to raw file changes, catching sessions whose hook never fired: a crash, akill -9, a misconfigured hook, another tool writing transcripts. - A sweep reconciles every configured root against the archive — on every startup, then every 15 minutes. This is the only layer that closes the guarantee, because events that happened while the daemon was down were missed by both of the others.
Two properties make running all three safe rather than wasteful. Imports are
incremental: append-only transcripts are tailed from a stored byte offset, so
re-reading a 50 MB session costs nothing. And they are idempotent, keyed by
content digest in an append-only import ledger, so the same session arriving by
hook and sweep produces one row plus a duplicate receipt — never two.
Then each session's prose is split for search. Chunks are 400-word windows on a
320-word stride, so consecutive windows overlap by 80 words; a phrase that
straddles a boundary is still findable. Those windows go into a SQLite FTS5 table
(porter unicode61 tokenizer) as (session_id, ordinal, body). On the real
corpus this averages ~7 chunks per session.
Chunks and vectors are derived data: both can be rebuilt from the archived prose, so re-indexing a session is safe and deleting a stale chunk is not data loss. The prose itself is not — see the archive-of-record guarantee below. Re-chunking a session drops vectors whose ordinal no longer exists, rather than leaving orphans that cosine search would rank and then fail to render.
(Chunking is currently mechanical. Turn-aware chunking — windows that respect message boundaries — lands with the source adapters and is not built yet.)
Not every entry in ~/.claude/projects is a conversation. Sessions are
classified structurally as human, tool-invoked, or subagent.
This matters more than it sounds. On the development corpus, 92% of Claude
"sessions" were programmatic claude -p calls made by another tool. Pooling
them with real conversations made the corpus look 40× larger and the median
session 16× shorter, which in turn produced wrong retention estimates, wrong
growth rates, and a badly mis-tuned enrichment threshold. Every statistic Muninn
reports is scoped to a provenance class.
The archive stores transcripts verbatim, secrets included. Redaction runs at
the model boundary — enrichment, and --deep rerank — never on ingest, because a
redacting ingest would destroy the only surviving copy of a session to protect a
credential the vendor already wrote to the same disk in plaintext. Do not read
the archive as a sanitising layer; it is a system of record.
Two things bound that in practice. Tool output never enters the archive —
only user and assistant prose does — so a credential read through a secret
manager is counted and its content discarded; one typed or quoted into a message
is stored. And if a credential does land in a transcript, rotate it: the
value would sit in the vendor's .jsonl, the archive and the FTS index, and
regenerating it invalidates all three at once, which scrubbing cannot.
The archive-of-record guarantee covers human and subagent sessions. Tool-invoked prose is prunable — it is a reproducible byproduct of some other tool's call volume.
Every other retrieval path here waits to be asked. search, log, correlate
and show each answer a question, and each needs you to think of the question
first — which is the wrong shape for the most valuable thing an archive holds,
because the material you most need is the material you have forgotten you
have. You do not search for it, because you do not know it is there.
muninn recall takes a repository instead — where you are standing, falling
back to wherever your most recent session was working when the archive has
nothing on the current directory — and reports three kinds of knowing:
- Unfinished threads — sessions enrichment judged
ongoingorabandoned. Nothing else surfaces these, and they are the most actionable thing in the corpus: work you started, did not finish, and have no reminder of. - Prior work here — what else has happened in this repository.
- Related, from elsewhere — nearest sessions in other repositories by embedding. The non-obvious one: the time you solved this in another project.
It calls no model. Unfinished and prior work are SQL; related is a dot product over vectors that already exist for search.
The unfinished list also reaches the menu bar, so Roost shows it without being asked — the one section of Muninn's menu that asks something of you rather than reporting state. It is silent when there is nothing to say, because a permanent reassuring row is how a menu teaches people to stop reading it.
Muninn works out where "now" is from its own ingest rather than by asking Huginn: the raven protocol forbids one raven presenting another's credential, and the most recently written session is a good enough answer for free.
muninn why <file> walks the commits touching a file and, for each one, names
the session that was live when it landed — with what that session was about,
what it decided, and how it ended.
The reasoning behind a line of code happened in a conversation. Git kept the result and threw away the argument; this archive kept the argument. Joining them is the only place either becomes an answer to "why on earth is it done this way?"
Attribution is by time overlap, and it is bounded on purpose. A session must have actually touched the file, or something else in the same repository, before it is offered as an explanation. An earlier version also offered any session merely open at the time, which sounds reasonable and measured terribly: session length spans four orders of magnitude on a real corpus — a 12-minute median against a 27-day 95th percentile — so a session parked for a month overlaps every commit made that month. At one arbitrary commit instant, eight sessions were "open". The first live run put three Cyberpunk-modding sessions under a change to the cost module.
Two things it will tell you that git cannot:
- Work with no commit behind it — exploration, a reverted attempt, or changes still in the tree. Invisible to git by construction, and often the half that explains the shape of what did land.
- Nothing at all, plainly, when a commit has no conversation behind it. Plenty of commits are written by hand, and saying so beats an empty section that reads like a failure.
Matching uses the repository-relative path including the repo's own directory name, so a checkout at a different root — another machine, another platform — still lines up, while a same-named file in an unrelated project does not get somebody else's decisions filed under it.
Calls no model: it is git plus SQL.
muninn survey measures your present corpus and writes an inspectable
calibration.json beside the archive.
A fixed threshold encodes one person's habits as everyone's defaults. A proposed "enrich sessions ≥300 words" rule selected 37% of Claude sessions but 91% of Codex sessions — the same constant meaning two different policies depending on which agent you favor. Derived gates on the same corpus landed at 4,046 and 2,480 words respectively, both hitting ~85% text coverage. What is held fixed is the coverage; the word count is whatever your corpus needs to reach it.
Enrichment no longer uses that gate, and the reason is worth reading. Deriving the threshold rather than guessing it was the right fight on the wrong axis. Word-coverage is a sound budget rule and a bad discoverability rule, because session cost scales with length: covering 85% of the words spends about 80% of the budget to reach about 18% of the conversations, and the ones it turns down are the cheapest in the corpus. On a real archive that was 687 conversations skipped to save roughly a quarter of what the 152 selected ones cost.
Length was standing in for value, and it is a bad proxy at the short end. A
ten-turn session that fixed something is one model call to summarise — and it is
exactly the session you will fail to remember later, which is when an archive is
supposed to help. A skipped session has no topic and no outcome, so it cannot be
found by subject, filtered by --outcome, or surfaced by recall.
So enrichment selects on a floor instead: enough words that "what happened here?" has an answer, and both sides having spoken. That is a mechanical test, not a judgement about whether the session mattered — nothing here can make that judgement, which is why the old one was wrong. The derived gate is still computed and reported, as a description of your corpus rather than a rule applied to it.
Every statistic is scoped to a provenance class, and the survey's first act is to report what is strange about your data — a tool-invoked majority, a source with no human sessions, sessions whose only copy is the archive.
The daemon keeps it current. A gate derived from your corpus is only right
for the corpus it was derived from, and archives grow: on one real machine it
tripled between surveys, and a gate chosen for 85% text coverage drifted to 74%
while enrichment went on reporting "100% of eligible" — truthfully, because the
eligible set had quietly shrunk. muninn serve re-derives the gate when the
archive outgrows it, and derives a first one for an archive that has never been
surveyed. A survey calls no model, so this is on by default; --no-recalibrate
turns it off, and muninn survey still does it on demand.
muninn survey also projects what a full pass would consume, per stage — and
tools/corpus-survey.py does the same thing standalone, stdlib-only, on a
machine that has never installed Muninn. See
spec 016. Measured on a real 680-session
archive: 1.76 tokens per word for embedding and 2.02 for enrichment —
well above the ~1.3 that describes English prose, because agent transcripts are
dense with code and identifiers. Ingest, lexical search and correlate are
listed at zero rather than omitted, because most of the tool costs nothing.
No prices ship with Muninn. A rate baked into a source file is one person's reading of one vendor's page on one date, and it then renders to two decimal places on your machine long after it stopped being true — while also asserting something about your billing that no code here can see. Subscription, enterprise, reseller and metered API access all produce different real numbers for an identical call.
So the model-side stages report their measured token volumes and say
unpriced. To get money back, write a rates.json beside the archive — the
natural job for an agent that can go and read current pricing:
{
"claude-haiku-4-5": {
"input": 1.00, "output": 5.00,
"source": "https://…, read 2026-08-16",
"as_of": "2026-08-16",
"seat_licensed": false
}
}source and as_of are required, so a figure always carries where it came from
and when — and anything older than 90 days is reported back to you as worth
re-checking. Every money figure is labelled as an understanding of published
list pricing, never as a quote. Inference that runs on your own machine is the
one built-in zero, and it is a statement about where the work happens rather
than a price anyone looked up.
muninn doctor recommends re-surveying when the corpus shape drifts: the corpus
doubles, a source appears, the source mix shifts, or the stored gate stops doing
what it was derived to do. (Query-latency regression is named in the design notes
and is not measured — it needs a benchmark harness.)
SQLite FTS5 over prose chunks. Measured on a real corpus: 0.8 s to index, 33 MB,
0.1–1.9 ms queries. Scaling tests to 162k chunks kept phrase queries under 1 ms
while broad OR queries degraded linearly to 45 ms — which is why query
expansion is capped rather than unbounded.
Semantic recall is optional and pluggable via an EmbeddingProvider protocol.
Once a provider is installed, muninn serve embeds new sessions in the
background — newest first, so the session you just finished is searchable before
the backlog from months ago finishes draining (spec
014). muninn embed remains for a
deliberate foreground backfill, and --no-embed declines the automatic one.
search --semantic fuses vectors with the lexical
results by reciprocal rank, and muninn correlate answers "conversations like
this one" — best on short and medium sessions; see
spec 006 for a measured caveat about very
long ones. Without a provider installed, --semantic exits non-zero and says
so — it never returns lexical results labelled as semantic.
Measured on a real archive of 112,193 chunks (384-dim vectors from the local
MLX provider, 172 MB in memory): 0.99 ms for a cosine top-20, and 1.3 s for a
correlate including model load. So no vector database is ever needed — a
matrix multiply and an argpartition are enough well past any plausible corpus.
The only real cost is generating the embeddings once, which is why the work is
resumable, committed per batch, and reported as a backlog by muninn doctor —
an automatic process that spends money should not be silent about how much it
has decided to do.
Two local providers ship behind that extra, and both run entirely on your
machine — no vectors, chunks or queries leave it. On Apple silicon
mlx-embeddings runs mlx-community/bge-small-en-v1.5-bf16 on the GPU and is
preferred where it works. Everywhere else — Windows, Linux, Intel Macs — ONNX
Runtime runs BAAI/bge-small-en-v1.5 on the CPU: prebuilt wheels, no compiler,
no torch, 133 MB fetched once and offline thereafter. Measured on Windows,
3,395 chunks embed in 138 s.
Their model ids differ because the vectors do: the MLX build is bf16 and the
ONNX one fp32. chunk_vectors keys on the model id so the two spaces are never
compared, which means moving an archive between platforms re-embeds rather than
silently mixing them.
uv sync --extra semantic # both local providers, plus numpy
uv run muninn serve # embeds in the background from here on
uv run muninn embed --dry-run # or do the backlog in the foreground, deliberately
uv run muninn search "that time SSE kept dropping" --semantic
uv run muninn correlate a7efca23Worth stating plainly, because "AI history tool" invites the assumption that a model is involved throughout:
Indexing calls no model at all. Parsing, provenance classification,
chunking, the FTS5 index, the import ledger, muninn search, muninn log,
muninn resume, muninn survey and muninn doctor are pure Python and SQLite.
Unplug every provider and all of that still works. Muninn is useful with no model
configured.
A model enters in exactly four places, all optional:
| Operation | Model | What it sees |
|---|---|---|
muninn embed, and the daemon's background worker |
Embedding | Every pending chunk's text, one vector each |
search --semantic |
Embedding | Your query only — one call |
search --deep |
Text (LLM) | The query plus the top candidate snippets, to reorder them |
muninn enrich |
Text (LLM) | One session's prose, to extract topic / outcome / decisions / artifacts |
The text provider is a local CLI, not an SDK: claude -p by default (Haiku,
for cost — one call per substantive session), or codex exec with
--provider codex-cli (gpt-5.6-luna, Codex's cheap tier). Both send the prompt
on stdin, never argv, and both pass policy.check before the subprocess starts.
A distribution can declare which one is default — see spec
015; muninn doctor prints the declaration
and --provider overrides it per command.
Four consequences of that shape:
- Semantic search does not send your history anywhere at query time. The archive's vectors were computed once, up front; a query embeds the query, and ranking is a matrix multiply over stored vectors — ~1 ms at 60k. The expensive half is generating the vectors, which is why that is a background job.
muninn correlatecalls no model whatsoever. It resolves a provider only to read its model id as a lookup key, then compares stored mean vectors. Asking "what else is like this session" costs nothing.- Every call routes through one redaction boundary and one policy check.
enrichand--deepboth reach a provider through a single function that redacts first, so there is exactly one path from archived prose to a model. Model id and provider are checked against intersectingModelPolicyallowlists that fail closed — an unparseable policy becomes refuse-everything, not allow-everything. - Transcript text is treated as data, never instructions. Archived prose can contain web content and other agents' output, so the enrichment prompt frames it as data and the response is parsed as a closed vocabulary rather than trusted. An unclear outcome becomes a named sentinel instead of free text drawn from model output.
And the reliability rule that differs between them: --deep falls back to the
input order on any provider failure, because a worse ordering is still a
ranking — but --semantic with no provider at all exits non-zero and says so,
because silently returning lexical results labelled semantic reports the wrong
thing confidently. Two embedding models are never mixed in one search: the model
id is part of the vector's primary key, and mixing spaces returns confident
nonsense rather than an error.
- Storage, ingest, provenance classification, losslessness contract tests
- Distributable corpus survey (
tools/corpus-survey.py) - Background indexer:
SessionEndhook + watcher + reconciling sweep - Daemon (
muninn serve) owning ingest, the menubar raven, and clean shutdown - Login-agent installer (launchd / systemd / Windows), via the shared
corvidaepackage - Structured filters:
--repo --branch --file --tool --model --provenance --source --since --until, plusmuninn log -
muninn doctor— index lag, parse health, queue depth, ledger tail, calibration drift -
muninn survey— derived thresholds in an inspectablecalibration.json - Prose-index backfill (
muninn backfill) fromclaudex/codexdex -
muninn resume— reopen a session, or say honestly why it cannot be - Index-time enrichment (
muninn enrich): topic, outcome, decisions, artifacts - Hybrid retrieval:
muninn embed,search --semantic/--deep,muninn correlate - Automatic background embedding, owned by the daemon and gated on a provider
-
muninn brief— a synthesis across matching sessions, carrying provenance per claim - Shared-menubar raven: descriptor and
/api/menu, rendered by Roost - Console
- Agent skill
Muninn supersedes claudex and
codexdex, folding their prose-index
approach into one archive with per-source adapters.
Their indexes are archives too, and that matters more than it sounds: they cover
sessions whose raw transcripts were swept months ago. muninn backfill ingests
them, recording each session with origin = 'prose-index' and never overwriting
a richer raw-derived one. Run against the development machine's real corpus it
moved 3,730 sessions and 26,420,905 words with zero differences, and reported the
8 files it skipped along with the reason for each.
Keep the predecessors' indexes until you have verified your own archive holds
what they did. Archiving the repositories is harmless; deleting ~/.claudex
is not.
.valholl/ holds the knowledge base — an
OKF bundle recording why decisions were
made and what was measured, including the methodological errors made along the
way so they are not repeated. Start at
.valholl/index.md.
macOS, Linux and Windows on a best-effort basis. See WINDOWS.md for what is actually verified there, including four tests that are skipped on the Windows CI runner and why.
uv run python -m unittest discover tests -vStdlib unittest, no dependencies. Filesystem tests run against a tempdir and
never touch a real archive.
Muninn's design surfaced nine concrete improvements for Huginn — an exact-match
plugin API version that silently disables plugins, a menubar with no extension
seam, missing lag reporting, and transcript pollution from its own LLM calls
among them. They are written up in
.valholl/articles/lessons-for-huginn.md.
Apache-2.0. See LICENSE.