Show HN:Heimdall——面向 AI 编程 Agent 的可信验证知识层
英文标题:🔶 Show HN: Heimdall – Trust-verified knowledge layer for AI coding agents
Heimdall 是一个面向 AI 编程 Agent 的自愈式知识层,旨在避免 Agent 在不同项目中重复完成已经做过的工作。它通过语义记忆图跨项目索引代码与知识,并在每个搜索结果上标注 STRONG、WEAK、REBUILT、STALE 或 REMOVED 等可信度结论。其 v0.2.0 架构采用单写入者、水平触发的协调器,所有变更均从磁盘实际状态重新收敛,以避免并发写入、命令误解析和索引漂移。对开发者而言,这意味着可在 Claude Code、Codex、Cursor、Windsurf、Pi 等工具中获得可验证、可自愈且支持跨项目复用的代码知识检索能力。
60.1 KB
中文
你的 Agent 一直在重复构建你已经完成过的工作。Heimdall 让它停下来。
每次 AI 编程会话都是从零开始。跨仓库使用 Grep 无法回答 “我是否已经在另一个项目中解决过这个问题?”——答案存在于另一个目录中,以自然语言描述,藏在一个你从未打开过的路径下。结果就是:同一项工作被重复构建三次。
Heimdall 是面向 AI 编程 Agent 的自愈式、可信验证知识层。它观察 Agent 的操作,在你接触过的所有项目间持续更新语义记忆图;而且——这是其他人尚未构建的关键部分——它会为每个搜索命中标注可信度结论,让 Agent 永远不会基于失效路径或幻觉式匹配采取行动。
这是引擎仓库。运行时状态位于仓库之外:~/.heimdall/(journal、lock、hint queue、config)和 ~/.graft/(后端 daemon + sqlite DB),以及用于 inventory/telemetry/stale logs 的 ~/knowledge-base/。该仓库是位于 /Users/arihantdeva/Repos/heimdall 的 git worktree;整个系统通过 ~/.pi/agent/extensions/kb-*.ts(extensions/ 的副本)以及由 launchd 管理的 graft daemon 部署在这台机器上。
可信度结论(差异化能力)
每条搜索结果在 Agent 看到之前,都会根据真实状态进行验证:
STRONG—— 路径在磁盘上存在,词法覆盖度高,并且文件实际内容能够回答查询(内容感知评分)WEAK—— 仅为语义匹配;看起来合理,但尚未验证REBUILT—— 文件已移动;Heimdall 找到它并自动重新锚定STALE/REMOVED—— 失效路径,会被记录并清理,使其不再参与排名
Agent 基于失效路径行动,比没有答案更糟糕。可信度结论正是让这张图足够可靠、可以据此行动的关键。
历史分为三个阶段(可从 git log 和代码自身注释中推断):
- v0.1.0 —— 打包的 CLI + 五个 harness adapter + 内容感知的可信度结论。Hook 从工具调用中推断图变更:通过正则解析 bash 命令,判断它是移动、删除还是修改操作,然后由每个 hook 进程直接删除并重新插入 graft 节点。
- 该设计崩溃了 —— 该方法只能看到它能识别的写入操作(
git checkout、make、IDE 保存、第二个 Agent 在架构上均不可见);每个 hook 进程都会并发写入图(删除+插入产生竞态);一次误解析写入了错误数据,因为命令文本被当作变更内容的描述。结果是图逐渐偏离磁盘状态,却没有任何机制发现这一点。 - v0.2.0(当前版本) —— 以单写入者、水平触发式协调器取代了整个变更路径:没有任何组件再向图告知_发生了什么变化_,只会告知_某个路径可能发生了变化_。协调器从磁盘读取文件,并使图与之匹配。过去会写入图的 Hook 现在只追加 hint 行。
可信度结论层来自同一个更高层次的教训:即使图本身完美,如果其锚点腐化,它依然会说谎(Desktop 会被频繁重组)。因此,搜索结果会在查询时再次对照文件系统验证;对于已移动文件,系统会进行自愈式重新安置(rehome)。
架构 / 工作方式
┌─────────────────────────── harness integrations ───────────────────────────┐
│ Pi: extensions/kb-*.ts Claude Code: PostToolUse hook │
│ (kb-tools, kb-autosync, kb-orient, kb-search-guard) Codex/Cursor/Windsurf │
└───────────────┬──────────────────────────────┬─────────────────────────────┘
│ only ever appends hints │
▼ ▼
~/.heimdall/hints.jsonl heimdall CLI (bin/heimdall.js)
│ init / insert / hint / verify / depth
▼ │
┌───────────────────────── THE SINGLE WRITER (holds O_EXCL lock) ───┘
│ bin/heimdall-reconciler.mjs (daemon: fs watch + hint ingest + drain loop + audit timer)
│ │ drain() reads hint queue → reconcilePath() per path
│ ▼
│ bin/lib/reconcile.mjs — level-triggered convergence
│ reads file from disk → desiredState() → compares hash/depth
│ → sink projection (graft insert/delete) BEFORE journal commit
│ → journal.commit() in one transaction, generation-guarded (ABA)
│ ▼
│ bin/lib/journal.mjs — AUTHORITATIVE index (~/.heimdall/journal.db, node:sqlite, WAL)
│ paths (hash/size/mtime/depth/generation/state) · owned_nodes · owned_edges
│ pending_edges (order-independent cross-file edges) · queue (dedup)
│ ▼
└──────────► bin/lib/sink.mjs — projection target (pluggable backend)
GraftSink (graft CLI) | MemorySink (tests, --dry-run)
▼
~/.graft daemon (vendor/graft, built from source) — sqlite + bge-m3 embeddings + graph edges
SEARCH PATH (read side, no lock):
kb-search.sh → graft retrieve (hybrid ranked) → kb_search_verify.py (verdicts + stale heal)
→ graft explore (graph walk of related work) → printed, ranked, verified
核心不变量(每一项都有测试):
- 水平触发,而非事件驱动。 Hooks、watchers 和 scripts 只能说“查看这个路径”。系统绝不相信它们关于_发生了什么_的描述。遗漏、重复乃至完全错误的 hint,最多只会带来一次
stat操作——不会造成更多影响。这使得 40 个并发 Agent 编辑同一个文件也无害:它们会产生 N 次廉价追加,最终折叠为一条队列记录(主键去重)。 - 从架构上保证单写入者。 每一次图/journal 变更都要通过
Lock——即采用带有过期 PID 存活回收能力的O_EXCL创建锁(Node 没有可移植的 flock)。不可能存在两个写入者,因此旧有的“删除后再插入”竞态(文件会暂时从图中消失)无法表达。daemon 在其整个生命周期内持有锁;一次性的heimdall reconcile同样会获取该锁,并与 daemon 串行执行。 - 精确所有权。 每个节点和边都恰好属于一个路径。一次提交会在一个事务中删除并重新插入该路径的所有行——被删除的符号无法作为幽灵残留,撤销一个路径也永远不会影响另一路径的数据行。
- 内容哈希是唯一准则。 目标状态 =
(bytes, depth)的纯函数。协调两次与协调一次在字节层面完全一致。generation计数器是 ABA 防护:若某次协调启动后 generation 已发生变化,其提交会被拒绝(操作为stale),路径将重新进入队列,因此一次延迟的协调永远不能复活一个已被较新操作删除的节点。 - 顺序无关性。 指向尚未索引符号的跨文件边会作为 pending edge 暂存,由源文件拥有;目标文件完成协调后,它会从两个方向解析。最终图结构不依赖于文件的协调先后顺序。
- 审计作为兜底。
heimdall verify将 journal 与文件系统进行比较(仅stat:size/mtime,外加深度升级检查),发现漂移时以退出码 1 结束;--deep会重新计算所有内容哈希,即使发生了“相同 size/相同 mtime 的重写”也能检测出来。这是可在 CI 中执行的准确性声明。daemon 也会按定时器执行相同审计(默认每 15 分钟),并将发生漂移的路径重新入队。 - Journal 在 sink 之前,sink 在提交之前。 Sink 写入发生在 journal 提交之前:若中间发生崩溃,路径仍会保持 dirty 状态,因此会被重做——由于协调是幂等的,这是安全的。反过来的顺序可能将一个从未投影的路径标记为 clean,而这是审计之后也无法检测到的唯一故障。
- 诚实的保证是有界陈旧性的收敛,而不是即时正确性:编辑发生到下一次协调执行之间,图会落后于实际状态。但它绝不会以一种能在一次协调后仍然存续的方式保持_错误_。
节点以不同深度建立索引。默认值和推荐值是maximum——图不仅知道文件存在,还知道其中有哪些函数和类、它们位于哪些行,以及哪些内容调用了哪些内容。
| 深度 | 节点所知信息 |
|---|---|
path(L0) |
文件存在 |
file(L1) |
+ 语言、大小、描述 |
symbol(L2) |
+ 每个顶层定义对应一个节点,包含 file:line 和签名 |
graph(L3) |
+ import / call / inherit / use 边,涵盖文件内和跨文件关系 |
max 会解析为机器实际能够支持的最高级别:L2/L3 需要 tree-sitter,因此没有该组件的机器会降级到 L1,而非执行失败;tree-sitter 出现后,下一次审计会自动以更深层级重新建立索引(深度升级会被识别为漂移)。请求超过机器能力的深度会在能力范围内尽可能执行,并报告为 clamped:
heimdall depth src/server.ts # requested vs effective depth for one path
提取使用通过 Python bridge 实现的 tree-sitter AST 解析,而不是 LLM 调用:深度消耗的是 CPU,而不是 token。该 bridge(bin/lib/heimdall_extract.py)刻意直接调用 graphify 的逐语言 extractor,而不调用 graphify.extract(),因为后者会在其读取文件的旁边写入 graphify-out/cache/ 目录(Heimdall 会索引整个 home tree),并添加一个带有自身失效规则、可能与真实状态不一致的第二缓存。
| 组件 | 文件 | 职责 |
|---|---|---|
| CLI | bin/heimdall.js + bin/lib/cli-main.mjs |
init / search / insert / doctor / daemon / reconcile / verify / depth / hint——对 bin/ scripts 的轻量封装,绝不重写 |
| Reconciler daemon | bin/heimdall-reconciler.mjs |
单写入者:递归 fs watch + hint 摄取 + drain loop + audit timer,在整个生命周期内持有锁 |
| Reconcile | bin/lib/reconcile.mjs |
水平触发式收敛:读取磁盘,使图匹配;audit() = 漂移检测器 |
| Journal | bin/lib/journal.mjs |
权威索引:所有权、哈希、generation、去重队列、pending edges |
| Extraction | bin/lib/extract.mjs + bin/lib/heimdall_extract.py |
每个文件的目标状态:hash + L0-L3 nodes/edges、以路径命名空间隔离的 node ids |
| Depth ladder | bin/lib/depth.mjs |
层级、能力探测、每 root 覆盖配置、config |
| Single-writer lock | bin/lib/lock.mjs |
每次图变更均须通过的 O_EXCL 锁 |
| Hints | bin/lib/hints.mjs |
非写入者唯一可使用的通道(仅追加、原子、可容忍不完整行) |
| Sink | bin/lib/sink.mjs |
投影目标:GraftSink(CLI)和 MemorySink(tests/dry-run) |
| Ranked search | bin/kb-search.sh |
top-k 混合排序 + 图遍历,--scope 过滤器 |
| Trust verification | bin/kb_search_verify.py |
内容感知的 STRONG/WEAK/STALE/REBUILT/REMOVED 结论;陈旧数据自愈 |
| Stale pruning | bin/kb-stale-scan.py, bin/kb-rehome.sh |
全图扫描:确定性 rehome,或记录后删除 |
| Health & telemetry | bin/kb-health.sh, bin/telemetry.sh |
daemon 健康状态、索引新鲜度、使用统计(24 小时内 kb_* 调用次数、命中率、预计节省时间) |
| Bootstrap | bin/sync-edits.sh, bin/seed-graft.sh |
重放会话编辑日志 → hints;将 inventory TSV 注入 Graft |
| Full rebuild | bin/kb-rebuild.sh |
backup → wipe → parallel restore → re-seed → prune → verify |
| Pi extension: tools | extensions/kb-tools.ts |
将 kb_search / kb_insert / kb_sync 暴露为 Agent 工具 |
| Pi extension: autosync | extensions/kb-autosync.ts |
追加路径 hint 的 hook——绝不写入图 |
| Pi extension: orient | extensions/kb-orient.ts |
在会话的首个用户提示中注入既有工作命中(2.5 秒上限,静默降级) |
| Pi extension: guard | extensions/kb-search-guard.ts + extensions/lib/kb-guard-core.mjs |
连续 3 次 grep 风格操作却未使用 kb_search 后发出警告 |
| Backends | vendor/graft/(Apache 2.0)、vendor/graphify/(MIT) |
Graft = 语义记忆 daemon;graphify = 代码图提取器 |
后端可插拔。 任何遵守 graft CLI 协议的存储都可使用;Graft 是内置的参考实现。请参阅 docs/heimdall_compare.dot/png,了解 graphify、Graft 与 Heimdall 的定位对比(graphify 回答的是_代码库_问题,Graft 跨项目持久化_笔记/事实_,Heimdall 则通过可信度结论和自愈能力将二者连接起来)。
| Harness | 命令 | 集成方式 |
|---|---|---|
| Pi | heimdall init --harness pi |
原生 extensions:kb_search/kb_insert/kb_sync 工具、编辑自动同步、会话定向 |
| Claude Code | heimdall init --harness claude-code |
PostToolUse hook 发出路径 hints + memory snippet |
| Codex CLI | heimdall init --harness codex |
AGENTS.md 搜索/插入说明 |
| Cursor | heimdall init --harness cursor |
含 search-first 工作流的 rules file |
| Windsurf | heimdall init --harness windsurf |
含 search-first 工作流的 rules file |
在这台机器上,Pi extensions 已作为 ~/.pi/agent/extensions/kb-*.ts 启用(harness 层面“实现前必须执行 kb_search”的硬性门槛,就是本项目的 kb-search-guard 行为)。Claude Code hook 会在初始化时解析一次 CLI(npm root -g),并回退到 PATH shim;它必须保持快速,且绝不能写入图。
npm i -g heimdall
# one-time: install the graft backend (local semantic-memory daemon)
# see https://github.com/tinygrad/graft — build vendor/graft, put the `graft`
# binary on PATH (e.g. ~/.local/bin/graft) and create ~/.graft/config.yaml:
# cp "$(npm root -g)/heimdall/config/heimdall.yaml.example" ~/.graft/config.yaml
# wire it into your harness (pi | claude-code | codex | cursor | windsurf | all)
heimdall init --harness claude-code
# verify the backend is healthy
heimdall doctor
# search across every project you've worked in
heimdall search "excel tracker portfolio optimization"
# record reusable work when your agent finishes something
heimdall insert --title "poker jam_opt optimizer"
English / 原文
Your agent keeps rebuilding work you already did. Heimdall makes it stop.
Every AI coding session starts cold. Grep across your repos can't answer "did I already solve this in another project?" — the answer lives in a different directory, described in prose, under a path you've never opened. The result: the same work rebuilt three times.
Heimdall is a self-healing, trust-verified knowledge layer for AI coding agents. It watches what your agent does, keeps a semantic memory graph fresh across every project you touch, and — the part nobody else builds — labels every search hit with a trust verdict so your agent never acts on a dead path or a hallucinated match.
This is the engine repo. Runtime state lives outside it: ~/.heimdall/ (journal, lock, hint queue, config) and ~/.graft/ (backend daemon + sqlite DB), plus ~/knowledge-base/ for inventory/telemetry/stale logs. The repo is a git worktree at /Users/arihantdeva/Repos/heimdall; the whole system is deployed on this machine via ~/.pi/agent/extensions/kb-*.ts (copies of extensions/) and the launchd-managed graft daemon.
The trust verdict (the differentiator)
Every search result is verified against reality before your agent sees it:
STRONG— path exists on disk, strong lexical coverage, and the file's actual content answers the query (content-aware scoring)WEAK— semantic match only; plausible but unverifiedREBUILT— file moved; Heimdall found it and re-anchored automaticallySTALE/REMOVED— dead path, logged and pruned so it stops ranking
An agent acting on a dead path is worse than no answer. Verdicts are what make the graph trustworthy enough to act on.
History in three stages (inferable from the git log and the code's own comments):
- v0.1.0 — packaged CLI + five harness adapters + content-aware trust verdicts. Hooks inferred graph mutations from tool calls: regex-parse the bash command, decide it was a move/remove/modify, then delete+reinsert graft nodes directly from each hook process.
- The design collapsed — that approach could only see writes it recognized (
git checkout,make, IDE saves, second agents were structurally invisible); every hook process wrote the graph concurrently (delete+insert raced); a misparse wrote wrong data because the command text was treated as a description of what changed. The result: the graph drifted from disk and nothing detected it. - v0.2.0 (current) — replaced the whole mutation path with a single-writer, level-triggered reconciler : nothing ever tells the graph what changed, only that a path might have. The reconciler reads the file from disk and makes the graph match. Hooks that used to write the graph now just append hint lines.
The trust verdict layer came from the same lesson one level up: even a perfect graph lies if its anchors rot (Desktop gets reorganized aggressively). So search results are re-verified against the filesystem at query time, with self-healing (rehome) for moved files.
Architecture / how it works
┌─────────────────────────── harness integrations ───────────────────────────┐
│ Pi: extensions/kb-*.ts Claude Code: PostToolUse hook │
│ (kb-tools, kb-autosync, kb-orient, kb-search-guard) Codex/Cursor/Windsurf │
└───────────────┬──────────────────────────────┬─────────────────────────────┘
│ only ever appends hints │
▼ ▼
~/.heimdall/hints.jsonl heimdall CLI (bin/heimdall.js)
│ init / insert / hint / verify / depth
▼ │
┌───────────────────────── THE SINGLE WRITER (holds O_EXCL lock) ───┘
│ bin/heimdall-reconciler.mjs (daemon: fs watch + hint ingest + drain loop + audit timer)
│ │ drain() reads hint queue → reconcilePath() per path
│ ▼
│ bin/lib/reconcile.mjs — level-triggered convergence
│ reads file from disk → desiredState() → compares hash/depth
│ → sink projection (graft insert/delete) BEFORE journal commit
│ → journal.commit() in one transaction, generation-guarded (ABA)
│ ▼
│ bin/lib/journal.mjs — AUTHORITATIVE index (~/.heimdall/journal.db, node:sqlite, WAL)
│ paths (hash/size/mtime/depth/generation/state) · owned_nodes · owned_edges
│ pending_edges (order-independent cross-file edges) · queue (dedup)
│ ▼
└──────────► bin/lib/sink.mjs — projection target (pluggable backend)
GraftSink (graft CLI) | MemorySink (tests, --dry-run)
▼
~/.graft daemon (vendor/graft, built from source) — sqlite + bge-m3 embeddings + graph edges
SEARCH PATH (read side, no lock):
kb-search.sh → graft retrieve (hybrid ranked) → kb_search_verify.py (verdicts + stale heal)
→ graft explore (graph walk of related work) → printed, ranked, verified
Core invariants (each has a test):
- Level-triggered, not event-driven. Hooks/watchers/scripts can only say "look at this path". They are never believed about what happened. A missed, duplicated, or flatly wrong hint costs one
stat— nothing more. This is what makes 40 concurrent agents editing one file harmless: they produce N cheap appends that collapse to one queue row (primary-key dedup). - Single writer by construction. Every graph/journal mutation passes through
Lock— anO_EXCLcreate with stale-PID liveness reclamation (Node has no portable flock). Two writers cannot exist, so the old delete-then-insert race (file briefly vanished from the graph) is unrepresentable. The daemon holds the lock for its lifetime; one-shotheimdall reconciletakes it too and serializes against the daemon. - Exact ownership. Every node and edge belongs to exactly one path. A commit deletes and re-inserts all of that path's rows in one transaction — a deleted symbol cannot survive as a ghost, and one path's retraction never touches another's rows.
- Content hash is the oracle. Desired state = pure function of (bytes, depth). Reconciling twice is byte-identical to reconciling once. The
generationcounter is the ABA guard: a commit whose generation moved since the reconcile started is rejected (actionstale) and the path re-queued, so a late reconcile can never resurrect a node a newer one deleted. - Order independence. A cross-file edge to a not-yet-indexed symbol parks as a pending edge, owned by the source file, and resolves in both directions once the target file reconciles. The final graph does not depend on which file was reconciled first.
- Audit as backstop.
heimdall verifycompares the journal against the filesystem (stat-only: size/mtime, plus a depth-upgrade check) and exits 1 on drift;--deepre-hashes everything and catches even a same-size/same-mtime rewrite. This is the accuracy claim as a CI-able command. The daemon runs the same audit on a timer (default 15 min) and re-queues drift. - Journal before sink, sink before commit. Sink writes happen BEFORE the journal commit: a crash in between leaves the path dirty, so it is redone — safe because reconcile is idempotent. The reverse order could mark a path clean that was never projected, which is the one failure the audit cannot detect later.
- The honest guarantee is bounded-staleness convergence , not instantaneous correctness: between an edit and the next reconcile pass the graph is behind. It is never wrong in a way that survives a pass.
Nodes are indexed at a depth. The default and the recommendation is maximum — the graph knows not just that a file exists but which functions and classes live in it, at which lines, and what calls what.
| Depth | Node knows |
|---|---|
path (L0) |
the file exists |
file (L1) |
+ language, size, description |
symbol (L2) |
+ one node per top-level definition, with file:line and signature |
graph (L3) |
+ imports / calls / inherits / uses edges, intra- and cross-file |
max resolves to whatever the machine can actually do: L2/L3 need tree-sitter, so a box without it degrades to L1 rather than failing — and the next audit automatically re-indexes at the deeper level once tree-sitter appears (a depth upgrade shows up as drift). Asking for a depth above the machine's capability is honored as far as possible and reported as clamped:
heimdall depth src/server.ts # requested vs effective depth for one path
Extraction is tree-sitter AST parsing via a Python bridge, not an LLM call : depth costs CPU, never tokens. The bridge (bin/lib/heimdall_extract.py) deliberately calls graphify's per-language extractors directly rather than graphify.extract(), because the latter writes a graphify-out/cache/ directory next to the files it reads (Heimdall indexes the whole home tree) and adds a second cache with its own invalidation rules that could disagree with reality.
| Component | File | Job |
|---|---|---|
| CLI | bin/heimdall.js + bin/lib/cli-main.mjs |
init / search / insert / doctor / daemon / reconcile / verify / depth / hint — thin wrappers over the bin/ scripts, never a rewrite |
| Reconciler daemon | bin/heimdall-reconciler.mjs |
the single writer: recursive fs watch + hint ingest + drain loop + audit timer, holds the lock for its lifetime |
| Reconcile | bin/lib/reconcile.mjs |
level-triggered convergence: read disk, make graph match; audit() = drift detector |
| Journal | bin/lib/journal.mjs |
authoritative index: ownership, hashes, generations, dedup queue, pending edges |
| Extraction | bin/lib/extract.mjs + bin/lib/heimdall_extract.py |
desired state per file: hash + L0-L3 nodes/edges, path-namespaced node ids |
| Depth ladder | bin/lib/depth.mjs |
levels, capability probe, per-root overrides, config |
| Single-writer lock | bin/lib/lock.mjs |
O_EXCL lock every graph mutation passes through |
| Hints | bin/lib/hints.mjs |
the one channel a non-writer may use (append-only, atomic, torn-line tolerant) |
| Sink | bin/lib/sink.mjs |
projection targets: GraftSink (CLI) and MemorySink (tests/dry-run) |
| Ranked search | bin/kb-search.sh |
top-k hybrid ranked + graph walk, --scope filter |
| Trust verification | bin/kb_search_verify.py |
content-aware STRONG/WEAK/STALE/REBUILT/REMOVED verdicts; stale self-heal |
| Stale pruning | bin/kb-stale-scan.py, bin/kb-rehome.sh |
full-graph sweep: deterministic rehome or log+delete |
| Health & telemetry | bin/kb-health.sh, bin/telemetry.sh |
daemon health, index freshness, usage stats (kb_* calls/24h, hit rate, est. time saved) |
| Bootstrap | bin/sync-edits.sh, bin/seed-graft.sh |
replay session edit logs → hints; seed inventory TSV into Graft |
| Full rebuild | bin/kb-rebuild.sh |
backup → wipe → parallel restore → re-seed → prune → verify |
| Pi extension: tools | extensions/kb-tools.ts |
exposes kb_search / kb_insert / kb_sync as agent tools |
| Pi extension: autosync | extensions/kb-autosync.ts |
hook that appends path hints — never writes the graph |
| Pi extension: orient | extensions/kb-orient.ts |
injects prior-work hits into the first user prompt of a session (2.5s cap, silent degrade) |
| Pi extension: guard | extensions/kb-search-guard.ts + extensions/lib/kb-guard-core.mjs |
warns after 3 consecutive grep-style actions without kb_search |
| Backends | vendor/graft/ (Apache 2.0), vendor/graphify/ (MIT) |
Graft = semantic-memory daemon; graphify = code-graph extractors |
Backends are pluggable. Any store speaking the graft CLI contract works; Graft is the vendored reference. See docs/heimdall_compare.dot/png for the graphify vs Graft vs Heimdall positioning (graphify answers codebase questions, Graft persists notes/facts across projects, Heimdall ties them together with trust verdicts and self-healing).
| Harness | Command | Integration |
|---|---|---|
| Pi | heimdall init --harness pi |
native extensions: kb_search/kb_insert/kb_sync tools, edit autosync, session orientation |
| Claude Code | heimdall init --harness claude-code |
PostToolUse hook emits path hints + memory snippet |
| Codex CLI | heimdall init --harness codex |
AGENTS.md search/insert instructions |
| Cursor | heimdall init --harness cursor |
rules file with search-first workflow |
| Windsurf | heimdall init --harness windsurf |
rules file with search-first workflow |
On this machine the Pi extensions are live as ~/.pi/agent/extensions/kb-*.ts (the harness-level "kb_search before implementing" hard gate is this project's kb-search-guard behavior). The Claude Code hook resolves the CLI once at init (npm root -g) and falls back to a PATH shim; it must stay fast and never write the graph.
npm i -g heimdall
# one-time: install the graft backend (local semantic-memory daemon)
# see https://github.com/tinygrad/graft — build vendor/graft, put the `graft`
# binary on PATH (e.g. ~/.local/bin/graft) and create ~/.graft/config.yaml:
# cp "$(npm root -g)/heimdall/config/heimdall.yaml.example" ~/.graft/config.yaml
# wire it into your harness (pi | claude-code | codex | cursor | windsurf | all)
heimdall init --harness claude-code
# verify the backend is healthy
heimdall doctor
# search across every project you've worked in
heimdall search "excel tracker portfolio optimization"
# record reusable work when your agent finishes something
heimdall insert --title "poker jam_opt optimizer" \
--body "~/Repos/poker-bot/tools — heads-up jam/fold EV optimizer" \
--keywords poker,optimize
init, insert and the harness wiring work immediately; search and doctor need the graft backend installed separately (build from source — embedding model download included, so not instant). The vendored vendor/graft/ is source + attribution, not a prebuilt binary. On macOS the backend runs as the launchd job com.graft.daemon (template: launchd/com.heimdall.backend.plist.example).
Self-healing surface (daemon-era commands)
heimdall daemon # the single writer: watch, reconcile, audit on a timer
heimdall reconcile # converge now (one-shot; takes the same lock)
heimdall reconcile --all # deep audit + repair everything
heimdall verify --deep # read-only drift report; exit 1 if any. CI-safe
heimdall hint PATH ... # mark paths dirty — no lock needed, any process
heimdall hint --stdin # harness hooks hand tool-call JSON on stdin
node --test /Users/arihantdeva/Repos/heimdall/tests/*.test.mjs # 44 pass
# NOTE: `npm test` runs `node --test "tests/**/*.test.mjs"` which matches
# ZERO files on shells that pass the glob literally. Use explicit paths.
/Users/arihantdeva/Repos/heimdall/node_modules/.bin/tsc --noEmit -p tsconfig.json # typecheck (extensions only)
Suites:
tests/reconcile.test.mjs— the point. Invariant tests: 40 racing writers converge to one node set; separate OS processes hinting one file collapse to one queue row; reconciling twice is byte-identical; ABA generation guard rejects stale commits; deletion retracts exactly its own nodes; cross-file edges converge regardless of reconcile order; depth clamping; parse-failure degrades to L1; audit catches behind-our-back edits incl. same-size-same-mtime rewrites (--deep); git checkout picked up (the old command-regex path could not see it); lock admits exactly one writer + stale-PID reclamation; node-id path namespacing; garbage hints dropped. The L3 end-to-end test self-skips without tree-sitter.tests/guard.test.mjs— kb-search-guard contract (warn on 3rd consecutive grep action, reset on kb_search/kb_sync/graft, interleaved reads do NOT reset).tests/init.test.mjs+tests/adapters.test.mjs— CLI contract and per-harness config-writer smoke tests against temp HOMEs.tests/kb-verify.test.mjs— content-aware verdict contract viaselftest:node ids (no graft daemon needed): content mismatch downgrades STRONG, content match upgrades to STRONG, binary files degrade gracefully,extract_pathshome-anchor regression (the tilde-form bug).
The concurrency tests are the point: if the single-writer or idempotency properties ever break, those are the tests that go red.
- Node ≥ 22.5 (the journal uses the built-in
node:sqlite),bash,python3 - macOS today (launchd daemon management); Linux works with a manual daemon
- tree-sitter-capable python for L2/L3 (
HEIMDALL_PYTHONenv, or~/.heimdall/venv/bin/python3, orpython3in PATH) - Runtime npm deps: zero —
typebox/typescript/@types/nodeare dev-only - Graft backend for
search/doctor(built fromvendor/graft/)
Operations (this machine)
bash bin/kb-health.sh # heimdall doctor — daemon up, index fresh
bash bin/sync-edits.sh # one-shot: replay session edit logs → hints → reconcile
bash bin/telemetry.sh view # last snapshots: nodes/day, sync age, kb_* calls, hit rate
python3 bin/kb-stale-scan.py # full-graph stale sweep (rehome or log+delete)
bash bin/kb-rebuild.sh # last-resort full rebuild (backup → wipe → restore)
Live paths to be careful with: ~/.heimdall/journal.db, ~/.heimdall/reconciler.lock, ~/.heimdall/hints.jsonl, ~/.graft/config.yaml, ~/.graft/profiles/default/graft.db, ~/knowledge-base/.inventory.tsv, ~/knowledge-base/telemetry.tsv, ~/knowledge-base/stale-removals.log. These are data, not build artifacts — never rm/mv over them blindly (the 2026-08-20 matches.sqlite3 loss is the cautionary tale).
v0.2.0 — adds the reconciler: a single-writer, level-triggered convergence loop with a content-hash oracle, exact per-path ownership, and a depth ladder that indexes symbols and call edges by line. Replaces the previous design, where hooks inferred changes from commands and wrote the graph directly from several processes at once.
v0.1.0 shipped the packaged CLI, five harness adapters, content-aware trust verdicts, and a fresh-install-verified quickstart. Backend (Graft) is a separate install — see Quickstart.
MIT. Independent project — not affiliated with Graft or its authors.
-
Graft (Apache 2.0) — vendored as the default semantic-memory backend (
vendor/graft/, source + build instructions, not prebuilt). -
graphify v0.3.17 (MIT, © Safi Shamsi) — vendored per-repo code-graph extraction (
vendor/graphify/), the tree-sitter bridge's extraction engine. -
~/knowledge-base/— the operational data home on this machine:.inventory.tsv(seeded bybin/seed-graft.sh),telemetry.tsv(written bybin/telemetry.sh),stale-removals.log/stale-rehomes.log(written bybin/kb-stale-scan.py/kb-rehome.sh), legacy copies ofkb-search.sh/kb_search_verify.py(the scripts prefer the package copy and fall back to~/knowledge-base/). -
~/.graft/— backend config (config.yaml), sqlite profile DB,graftd.log,.last-sync(sync-edits watermark). -
~/.pi/agent/extensions/— live copies ofextensions/kb-*.ts; the harness's AGENTS.md references the kb_search/kb_insert/kb_sync workflow this repo implements. -
~/.heimdall/— journal, lock, hint queue,config.json(harness selection), adapter install records. -
~/Repos/cli-emailetc. — unrelated repos; Heimdall indexes them (that's the point), it does not depend on them.
Development notes / gotchas
npm testglob bug (see Testing) — always pass explicit test paths.- Skip rules in two files :
skipPath()inbin/lib/reconcile.mjsand the mirror inextensions/kb-autosync.ts(plus the exclusions insidekb-stale-scan.py's find). Change all three or the paths disagree. - Supply-chain guard : graft is resolved by absolute path (
GRAFTenv /~/.local/bin/graft), never PATH-resolved, in every script — a shadowed binary could delete memory nodes from inside a search. - Do not hand-roll SQL string building in the journal — bound parameters only; the old LIKE-escaping was a recurring correctness bug in a project whose whole claim is accuracy.
- The bridge must not use
graphify.extract()— it writes a cache dir next to indexed files and adds a second invalidation source. - The L3 test self-skips without tree-sitter — a green suite can silently mean "L1-only machine"; check
heimdall depth <file>output for the effective level. - Daemon discipline :
graftdis launchd-managed (KeepAlive); do notnohup graftdmanually — two daemons fight over the socket.kb-health.shreconciles to exactly one.