Bootstrap or update a Mnemex knowledge graph from an EXISTING code or documentation repository (a local folder or a git remote) — no live session required. Use this when the user wants to seed a graph from a repo, says "ingest this repo", "bootstrap the graph from <repo>", "import our docs/code into memory", "index this codebase into the graph", or wants to re-sync a previously ingested corpus. Walks the source, DISTILLS durable atoms (never transcribes), discovers a deduped entity catalog, w...
Scanned 9/6/2026
Install to Claude Code
npx -y skills add kritird/OpenMnemex --skill mnx-ingest --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mnx Ingest?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kritird-mnx-ingest)More formats (shields.io, HTML) on the badges page.
---
name: mnx-ingest
description: Bootstrap or update a Mnemex knowledge graph from an EXISTING code or documentation repository (a local folder or a git remote) — no live session required. Use this when the user wants to seed a graph from a repo, says "ingest this repo", "bootstrap the graph from <repo>", "import our docs/code into memory", "index this codebase into the graph", or wants to re-sync a previously ingested corpus. Walks the source, DISTILLS durable atoms (never transcribes), discovers a deduped entity catalog, wikifies links, and stages a labeled bulk batch — then /mnemex:mnx-promote --bulk merges it. Two gates only (scope up front, bulk summary at the end); no per-atom review. Idempotent on re-run (a deleted source file surfaces as an orphan candidate, never auto-death). Never writes the graph (staging only) and never mutates the source.
argument-hint: "<local-path|git-url> [--into <graph>] [--dry-run] [--resume <ingest-batch>]"
---
# mnx-ingest — bootstrap the graph from an existing repo (a *source adapter*, not a new subsystem)
> **Speak in the graph's labels.** Read/promote payloads carry a `naming` object
> (`{root_label, space_label}`) — the words THIS graph uses for its root and its spaces (e.g. `area`,
> `repo`, `client`). When the Gate #1 source-tree→cluster map or a routing echo refers to a space, say
> `naming.space_label` ("repo: checkout-svc"), never hard-code "team"/"org". No `naming` in the payload
> (older engine) → fall back to org/team. Folders stay `space-*` on disk — that's storage, not what you
> say.
A live session is one producer of staged atoms; **a corpus is a second.** Ingest walks a repo, *distills*
durable atoms, discovers the entity structure a lived transcript would have handed capture for free,
wikifies links, and stages a **labeled bulk batch** into the same staging tier. Everything downstream —
reconcile, MERGE/SUPERSEDE, contradiction HITL, the wiki mesh, consolidate, doctor, push — is the
**existing** `/mnemex:mnx-promote --bulk`, reused unchanged. **`mnx-promote` stays the only writer to the
graph; ingest only stages.**
The deterministic mechanics (walk · classify · chunk · hash · delta · ER blocking/clustering · bulk
staging) live in `mnx_*.py`; the **judgment** (is this a durable atom? which `[[link]]`? which merge?)
lives here in prose + sub-agents and cannot move into Python.
Background: `docs/corpus-ingestion.md` (the full model), `docs/staging-and-promotion.md` (the pipeline
this reuses), `docs/link-reconciliation.md` (the mesh), `docs/multi-graph-and-team-routing.md` (routing).
Helpers: `mnx_ingest` (walk/probe/delta/manifest), `mnx_glean` (the bounded recall loop), `mnx_er` (entity
resolution), `mnx_simindex` (the ER blocker), `mnx_stage` (bulk staging), `mnx_binding` (locate/clone),
`mnx_phonebook`/`mnx_mesh` (wikification catalog).
## The two invariants that shape everything
- **Distill, never transcribe (DP2).** No file body is copied wholesale into a node; **zero atoms from a
file is valid and common** (boilerplate, generated code, changelogs, lockfiles). The graph is distilled
durable memory, not a RAG index over the repo — but distilled is not thin: the body carries the
*reasoning* behind the fact, the summary is the index row.
- **Single writer + read-only source (DP1, DP3).** Ingest stages only; it never writes the graph and never
mutates the source (a remote is cloned to a read-only cache; a local path is read in place). Secrets are
never read.
---
## Preflight — resolve the target graph
Run `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/mnx_binding.py" status --session <sid>` (the session id
from session-start, if you have one — see mnx-init step 1; honors a mid-session graph switch), or
honor an explicit `--into <graph>`.
- If unresolved and no `--into` → **STOP**: *"No target graph. Pass `--into <graph>` or run `/mnemex:mnx-init`."*
- **Echo the resolved target graph** before anything stages, exactly like capture/promote — the user must
see where the import will land. If `default_fallback` is true, flag it prominently and confirm.
- If `clone_present` is false → `mnx_binding.py sync` once. Note `graph_root`.
## SOURCE — acquire the corpus (read-only)
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/mnx_ingest.py" acquire --source <path|url>`.
- A **local path** is used in place; a **remote URL / `*.git`** is shallow-cloned into a read-only cache.
- Note `root` (what you walk), `commit` (the exact ref distilled from), and the source slug (derive it with
`mnx_ingest.py source-slug --source <…>` — it keys the manifest). **Never** write under `root`.
## PROBE + DELTA — scope the run (feeds gate #1)
1. `mnx_ingest.py probe --root <root>` → `{units[], files{}, counts, est_atoms, bytes_total, skipped_secrets}`.
Keep `files` (path → whole-file hash) for the manifest at DRAIN — a unit's `unit_hash` is per-chunk
and must never go into one. Units are already classified (`doc | interface | code-doc | config | skip`) and chunked along structure
(docs by heading, code by **exported** symbol — private symbols are never emitted).
YAML/JSON are **shape-gated** (they are mostly generated data): an OpenAPI / JSON-Schema-shaped file
(`openapi:` / `swagger:` / `$schema`) → `interface`, an authored (commented) YAML config → `config`,
and everything else (data blobs, lockfiles, uncommented JSON) → `skip`. So a `.yaml` file showing up
under `skip` is expected, not a miss.
2. On a **re-ingest**, diff against the prior manifest:
`mnx_ingest.py delta --root <root> --manifest <graph_root>/.mnemex/ingest/<slug>.json --commit <sha>`
→ `{added[], changed[], unchanged, orphans[], warning?}`. **Extract only from `added` + `changed`** — unchanged
files are skipped before any sub-agent runs (the dominant re-run cost saver). Hold the `orphans` list
(deleted source files' node_ids) for the report — **never auto-tombstone** them (DP4). A `warning` means
the delta is implausible (a bad prior manifest); show it and stop rather than re-importing the corpus.
## GATE #1 — scope + source-tree → cluster map (STOP for the human)
Emit the scope preview and **wait**:
```
INGEST SCOPE source: github.com/acme/payments-service @ 9f3c1a → graph: payments-knowledge
files: doc 42 · interface 30 · code-doc 18 · config 6 · skip 210 · secrets-skipped 3
est. atoms ≈ 140 (cost ceiling ingest_max_atoms_per_run: 2000 — excess resumes next run)
re-ingest delta: 12 added · 4 changed · 900 unchanged (skipped) · 2 orphan candidates
SOURCE-TREE → CLUSTER MAP (proposed from paths; editable — the bulk analog of per-atom domain:)
settlement/** → space-payments/settlement
rails/** → space-payments/rails
docs/risk/** → space-payments/finality (ONE space per run — another space is its own promote)
proto/** → (classify per file — mixed)
code_extract: gated (public/documented/config-only; deep opt-in per subtree)
```
Every cluster this map creates gets a **one-line description** (proposed here, written into the
cluster's `index.md` header `> …` line after the bulk promote lands) — read routing decides on that
line and index regeneration preserves it; never leave the scaffold placeholder.
The **source-tree → cluster map** is the bulk analog of capture's per-atom `domain:` — approve/edit it
**once** here, and every atom under a subtree inherits that placement. Routing still flows through the
normal promote precedence (root→space match, `default_space` fallback); the map is a *default*, not a bypass.
**A map may target ONE new space (the create-space gate).** The map normally targets *existing* spaces
(a missing *cluster* inside one is cheap). If a subtree clearly deserves its own new space — a distinct
`repo`/`area`/`client` in the graph's words — the map may name it, but a new **space** is never silent:
**call it out in this gate** with its one-line charter ("new **repo** `checkout-svc` — Checkout service
knowledge?"), and on approval the bulk promote creates it FIRST, then lands its clusters:
`promote_begin(space=space-<name>, ingest_batch=<id>, create_space="<charter>")` (MCP) / `mnx_promote.py`
begin with the same. The engine scaffolds the space before taking the lock (immediately routable), then
the batch drains into it — honoring the graph's `space_creation` config (`off` refuses; add the space
via the Console/CLI first). Re-ingest deltas into the same space are the normal idempotent path.
Keep it to **one** new space per run; spawning a taxonomy of empty spaces is the H3 sprawl pattern one
level up. Speak the space in the graph's label, never hard-code "team".
Confirm scope (or trim it) and the `code_extract` policy. **`--dry-run` stops here** (probe only, nothing
staged).
## PASS 1 — extract + glean + entity-resolve (per subtree, bulk batches)
Mint one **ingest batch id** for the run (e.g. `ing-<date>-<rand>`). Work **per source subtree**, in
bounded batches, honoring `ingest_max_atoms_per_run` (excess resumes next run via `--resume`).
**1a — Distil candidate atoms + entities (kind-aware, LLM judgment).** For each unit, decide what durable
knowledge it holds — *"a fact (the *what*) or a prescriptive pattern (the *how*) a future agent would want
months from now without the source open?"* Kind-aware policy:
- **doc** → domain facts from sections; **patterns** from ADRs / decisions / "gotchas" / runbooks / CONTRIBUTING.
**Every `type: pattern` atom MUST carry a `trigger`** — the one-line "when does this pattern fire?"
(e.g. `"when sizing expiry windows in a connector"`). `mnx_stage.add` refuses a pattern without one,
so distil the trigger from the section's context at extraction time, not later.
- **interface** → the *contract* (public API signature, `.proto`/GraphQL/OpenAPI message, exported type).
- **code-doc** → the intent/semantics the author wrote down (docstrings, module/dir headers).
- **config** → declared knobs + their meaning. **Secrets are never read.**
- **Code value-gate (non-negotiable noise control):** a code-derived atom is staged **only** if it is a
public/exported contract, **or** carries an author-written docstring/comment explaining *why*, **or** is a
config knob with a declared meaning. A bare private helper with no doc is **not** an atom. Function
*bodies* are never transcribed — only the distilled semantics. Emit `[[wiki-links]]` inline by name.
**1b — Glean (checklist mode — coverage outranks importance).** A single extraction pass under-captures.
Unlike a session, a corpus unit is never skipped for being *unimportant* — only for holding nothing
durable, and then only on the record. Build the candidate-unit list as the coverage checklist and run the
shared *gleanings* loop:
```
mnx_glean.py coverage --units <units.json> --staged <staged-ledger.json> [--waived <waived.json>] --pass <k> --graph <graph_root>
```
It returns `uncovered` (the zero-atom units — your re-ask worklist), `waived`, `coverage_pct` and a `stop`
signal (`complete` = every unit covered or waived, `cap` = `ingest_glean_passes`, default 4). A unit is
*covered* when a staged atom carries its `anchor` in provenance. Re-examine each uncovered unit once —
*"what durable fact/entity did this unit contain that I did not extract?"* — then re-run. A unit that truly
holds nothing durable (lockfile, generated code, boilerplate) goes in `--waived` as `{"<unit-id>": "<why>"}`;
an unreasoned waiver is **refused** and the unit stays uncovered. **Aim for `complete`, not `cap`.**
Re-staging identical content stays an idempotent no-op (DP10) reporting `action: already-staged` — count
those separately from fresh `staged`. The judgment stays here; `mnx_glean` only bounds/bookkeeps.
**1c — Assemble the in-batch entity catalog + entity-resolve (dedup, DP5).** Collect the candidate entities
(canonical name + aliases + type) across the whole delta corpus, then run ER over `{new atoms ∪ existing
graph pages}`:
```
mnx_er.py resolve --graph <graph_root> --atoms <candidates.json> [--team <t>]
```
It blocks (via `mnx_simindex.pairs --with --intra`), scores, clusters, and proposes a disposition per
cluster: **CREATE** (no graph match) · **MERGE** (folds into an existing page, keeps its id) · **COLLAPSE**
(intra-batch duplicates → one CREATE). The `possible` band is the **only** place you (the LLM judge) rule
on a merge — everything at/above `match` or below `possible` is deterministic. The output is your deduped,
canonical entity set (aliases unioned). **One entity → one node**: many corroborating sources collapse into
one well-provenanced node, never duplicate nodes.
## PASS 2 — wikify (rewrite atom bodies against the catalog) + stage
Resolving each name greedily as you meet it fragments a large graph. Instead, link every atom against the
**one** catalog you just built (∪ the team phonebook — `mnx_phonebook.py resolve` / `resolve-batch`):
- For each mention of a catalog/graph entity in an atom body → emit `[[canonical-name]]` (piped display if
the surface form differs). A mention with **no** catalog/graph entity → still `[[bracket]]` it → a
**red-link** that heals the moment its page is created (later batches in this same run knit the mesh).
- **Precision discipline (DP6):** an **exact** catalog/phonebook match links deterministically; a
**fuzzy/semantic** near-match is a `⚠ suggested` link surfaced at gate #2, **never** auto-written. A wrong
link is a false edge. High recall in Pass 1 (bracket generously — red-links are cheap); high precision in
Pass 2 (only *confident* links go live).
**Stage each atom under the bulk label** with source-anchored provenance so it is promotable **cold**:
```
mnx_stage.py add --json <<'JSON'
{ "type": "domain", "summary": "Settlement cut-off is 23:00 UTC; post-cutoff addenda ride in field 124",
"aliases": ["field 124", "settlement cutoff"], "domain": ["settlement"], "score": "later",
"ingest_batch": "ing-2026-07-11-a1b2",
"provenance": { "source_repo": "github.com/acme/payments-service", "commit_sha": "9f3c1a…",
"source_path": "settlement/reconcile.md", "anchor": "Cut-off handling", "kind": "doc",
"rationale": "distilled from settlement design doc" },
"body": "Settlement cut-off is 23:00 UTC. The batch is reconciled before any leg is posted, so a mismatch is caught while it is still cheap to fix.\n\nAddenda arriving after cut-off are not rejected — they ride in [[iso8583-field124]] and apply against the NEXT batch. That is why field 124 is populated on only a minority of messages yet must never be dropped by a parser: it is the sole carrier for a late correction." }
JSON
```
- **`provenance.anchor` = the unit's `anchor` from `probe` output, verbatim** (bare heading text, no
leading `#`s) — the glean-coverage key. **`provenance.source_path` is what the manifest ties node ids
back to**, so an atom missing it can never be surfaced when its source file is deleted. A `pattern`
atom additionally carries the top-level `"trigger"` field (see the kind-aware policy above).
**Hub atoms — make the mesh knit (do this, or the import lands ~all-red).** Fact atoms link to entity
*names* ([[ilp-address]]), but no fact atom IS the entity page, so without hubs nearly every link stays
a red-link and the whole import scores in-degree ≈ 0 (inert tiers, orphan-flood). For each **catalog
entity** (Pass 1c output) that (a) is mentioned by **≥3** staged atoms and (b) has **no** graph match
(ER said CREATE, not MERGE), stage **one hub atom**: `type: domain`, the entity's canonical name +
aliases, `volatility: timeless` when definitional, a 1–3 sentence body that says what the entity *is*
and `[[links]]` to its closest siblings. Red-links to it heal deterministically at promote (which
re-derives every node in the space), knitting the mesh in the same run. Below the mention threshold, leave the
red-link latent — a hub nobody points at is noise.
The `--ingest-batch` label sets `bulk: true` and partitions these atoms from any hand-captures (DP8) — the
per-session nag never fires, and the batch has its own large cap. Re-staging identical content is a no-op.
## DRAIN — hand off to bulk promote
Ingest **never writes the graph.** Drain the staged batch with the existing writer:
`/mnemex:mnx-promote --bulk --ingest-batch <id>` (gate #2 = the bulk summary; it auto-accepts plain
CREATE/MERGE and stops only on contradictions + new-cluster creation). Hand it probe's `files`: its step 5
records the manifest (`source_path@commit → node_ids`) from those plus apply's `dispositions`, so the next
ingest diffs correctly and a deleted file can name its orphans.
## REPORT
Summarize by cluster: created / merged / superseded / dropped-dup / held, atoms staged this run, the
final **coverage** (`coverage_pct`, waived-with-reason, and any unit still uncovered at `cap`), the
**orphan candidates** (deleted source files — surfaced for the human, never auto-tombstoned), and whether
the cost ceiling forced a `--resume`. If `--resume <ingest-batch>` was given, continue from the manifest +
remaining staged atoms rather than restarting.
## Never
- Never write into `graph_root` — ingest **only stages**; `/mnemex:mnx-promote` is the sole writer.
- Never mutate the source corpus, and never read a secret (the walk skips + counts them; you never open them).
- Never transcribe a file body into a node — distill; zero atoms from a file is valid.
- Never stage 5 atoms for one entity — ER collapses intra-batch duplicates **before** staging (one entity → one node).
- Never auto-write a fuzzy link (⚠ suggested → gate #2) and never auto-tombstone a deleted file's nodes (orphan candidate).
- Never do per-atom review — two gates only (scope up front, bulk summary at the end).
- Never let community detection mint structure — path-based routing is the default; Leiden may only *propose* at gate #1.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!