商业产业 Hacker News AI 2026-08-22T02:02 正文 ok 2766 words

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 等工具中获得可验证、可自愈且支持跨项目复用的代码知识检索能力。
打开原文相关 6 · 机会 1
media60.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 和代码自身注释中推断):

  1. v0.1.0 —— 打包的 CLI + 五个 harness adapter + 内容感知的可信度结论。Hook 从工具调用中推断图变更:通过正则解析 bash 命令,判断它是移动、删除还是修改操作,然后由每个 hook 进程直接删除并重新插入 graft 节点。
  2. 该设计崩溃了 —— 该方法只能看到它能识别的写入操作(git checkout、make、IDE 保存、第二个 Agent 在架构上均不可见);每个 hook 进程都会并发写入图(删除+插入产生竞态);一次误解析写入了错误数据,因为命令文本被当作变更内容的描述。结果是图逐渐偏离磁盘状态,却没有任何机制发现这一点。
  3. 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 unverified
  • REBUILT — file moved; Heimdall found it and re-anchored automatically
  • STALE / 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):

  1. 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.
  2. 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.
  3. 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 — an O_EXCL create 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-shot heimdall reconcile takes 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 generation counter is the ABA guard: a commit whose generation moved since the reconcile started is rejected (action stale) 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 verify compares the journal against the filesystem (stat-only: size/mtime, plus a depth-upgrade check) and exits 1 on drift; --deep re-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 via selftest: node ids (no graft daemon needed): content mismatch downgrades STRONG, content match upgrades to STRONG, binary files degrade gracefully, extract_paths home-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_PYTHON env, or ~/.heimdall/venv/bin/python3, or python3 in PATH)
  • Runtime npm deps: zero — typebox/typescript/@types/node are dev-only
  • Graft backend for search/doctor (built from vendor/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 by bin/seed-graft.sh), telemetry.tsv (written by bin/telemetry.sh), stale-removals.log/stale-rehomes.log (written by bin/kb-stale-scan.py/kb-rehome.sh), legacy copies of kb-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 of extensions/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-email etc. — unrelated repos; Heimdall indexes them (that's the point), it does not depend on them.

Development notes / gotchas

  • npm test glob bug (see Testing) — always pass explicit test paths.
  • Skip rules in two files : skipPath() in bin/lib/reconcile.mjs and the mirror in extensions/kb-autosync.ts (plus the exclusions inside kb-stale-scan.py's find). Change all three or the paths disagree.
  • Supply-chain guard : graft is resolved by absolute path (GRAFT env / ~/.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 usegraphify.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 : graftd is launchd-managed (KeepAlive); do not nohup graftd manually — two daemons fight over the socket. kb-health.sh reconciles to exactly one.