13 default entries · 12 state machines · 166 test files green · 10 languages

Malong·Liuhe Toolkit

MALONG · LIUHE

LLM-native code operations

Not a human tool with an API bolted on.

“Why should an LLM learn to use human tools?
Human tools assume a user with hands, eyes, and memory. An LLM has none of the three. Rather than teach the model to accommodate the tool, reshape the tool into what it should have been.
13
default entries (Six-He line)
12
state machines on disk
4.86k
tokens/turn catalog tax
0
fake “done”
The Wrong Assumption

Human tools were designed for people with hands, eyes, and memory

This is not a matter of adding an API to a tool. Every interaction assumption built into human tools fails when the user is an LLM.

What human tools assume
The LLM reality
What the Six-He line does
has hands: point, drag, confirm, trial and error
One call decides everything; a mistake must be reversible
transaction + journal + 24h undo
has eyes: one glance gives the structure
Sees only the text stuffed into context
code graph + budget truncation
has memory: remembers what it just changed
Context gets truncated, sessions get restarted
12 state machines on disk + reprojection
judges for itself that it is “done”
It will declare completion with total confidence
tools report mechanical facts, judgment handed back to the host

So the Six-He line is not a “stronger tool”. It is three compensation mechanisms plus a task line that hands control back.

The Graph

“Graph” is not a metaphor. It is a data structure in the index.

Not a diagram for the pitch deck. The index stores symbols, references, and file dependencies — nodes, edges, cycles — that you can query and run impact analysis on.

test cycle config.js change point resolve.js env.js parse.js index.js cli.js test-parse.js util.js change point / impact dependency edge cycle (resolve ↔ parse) test Illustration: the blast radius of one change, computed live from the index by dep_graph / impact_analysis / call_chain

One entry, 13 ways to ask

The default tier does not require memorizing 44 tool names. Every read-only lookup lives in code_query:

outline · symbols
body · search
map · impact
references · callers/callees
tests · validate
capabilities · capability
dep_graph(scope=project) → return shape
{
  "nodes": [ ... ],
  "edges": [ ... ],
  "cycles": [
    ["resolve.js", "parse.js"]
  ]
}

Boundaries stated plainly

Dynamic dispatch, reflection, and cross-language references are invisible — a static-analysis boundary, written into the limitations of every response. No pretending to be omniscient.

Better to say “can't see it” than let the model decide on a fake graph.

The Six-He Line

The default tier is one line: from a sentence to a piece of evidence

A five-step main chain, four return loops, six side exits. Every step names the node it reached; next_action gives the next one.

cancelled → code_control(action="reopen") ① ORIENT code_task goal → candidates / structure impact; sets route, no edit/verify next: code_read / template ② READ code_read symbol / file body + version issues read_ref as pre-write anchor budget truncation flagged ③ EDIT code_patch · code_batch code_maintain single file / N edits / imports default dry_run · journaling ④ CHECK code_check test / typecheck / lint, auto-picked records evidence, skipped ≠ passed long suites: async 600s ⑤ CONTROL code_control status / resume / cancel / reopen switch_route / expand / job validated → hand back node=orient node=read node=edit node=check done / cancelled failed → fix, then re-run (code_check, same scope) stale: file_change / goal_change → evidence goes stale indeterminate: crash / unconfirmed cancel → re-read first Every call returns the same read-only projection: task.lifecycle · route · node · observation.progress · next_action
Side exits code_query · read-only lookup code_status · state on demand code_audit · review route code_worktree · isolated changes invoke_capability · deep exit edit_transaction · recovery exit
read · 4
code_task · code_read
code_status · code_query

Before doing anything: know where you are, what you can read, and how far the blast reaches.

write · 8
code_patchcode_batch code_maintaincode_check code_controlcode_audit code_worktreeinvoke_capability

Each one previews, leaves evidence, and can be undone. Failure never turns into “done” on its own.

shared · 1
edit_transaction

Cross-file transactions and undo. Also part of classic 44 — the catalog is frozen byte for byte, so it stays as is.

Deep exit: invoke_capability(name=…) calls one classic tool per invocation; list them with code_query(mode="capabilities"). Success ≠ verified.
Switching tiers: MALONG_INTERFACE=classic | diagnostic. Under the default tier, classic tool names in responses are rewritten into callable graph calls.
One Real Run

One real task, ten round trips

Every arrow below is a call that actually happens. In between: one failed check, one fix, one re-check — not a single step relies on the model believing it is done.

host / LLM sets intent and business judgment Six-He entry 13 by default state layer task · evidence · journal workspace files · git index SQLite · Rust/tree-sitter 1 code_task(goal="add .env fallback to parseConfig") task_id · route=change · node=orient · progress=open 2 candidates / structure / impact 7 candidates · 3 affected files 3 create task goal_revision=1 task persisted 4 code_read(locator) — anchor before any write body + read_ref@v1 (pre-write baseline) 5 code_patch(dry_run=true, read_ref=@v1) diff preview · nothing written 6 code_patch(dry_run=false) write file committed · undo(24h) · progress=partial 7 code_check(run=true) run pytest 1 failed · evidence(result=failed) · progress=partial next_action = code_check(same scope) · note: fix first, then re-run — never call failure done 8 code_patch(fix) committed · progress=partial 9 code_check(run=true) — re-check, same scope passed · result_kind=validated · progress=done 10 code_status(task_id) — confirm once before closing next_action: null · mechanical_requirements_met: true goal_judgment: "host_required" — the tool reports mechanical facts; business judgment goes back to the host progress open → partial → partial → done steps 1 / 6 / 7 / 9
An anchor before any write

No read_ref, no write. If the file changes externally in between, you get VERSION_CONFLICT — never a silent overwrite.

Failure is never called done

After a failed test, progress stays partial and next_action points back to the same check scope — fix first, then prove.

Closing is a no-op

next_action:null + host_required: the tool states that mechanical requirements are met; whether the business goal is met is the host's call.

When Is It Done

It ran. That does not mean it is right.

“Done” is not a feeling. It is an inspectable projection. All state machines collapse into one read-only verdict, resolved by fixed priority — the first match is the fact.

Common “done”
The problem
What the Six-He line says
test exit code = 0
✕ Passing tests prove that tests pass
✓ validated + not stale + nothing in flight → done
skipped still counts as green
✕ Not run means not verified
✓ skipped ≠ passed, never counted as validated
timeout = the code is buggy
✕ The model goes and breaks code that was right
✓ a timeout is budget truncation → async 600s, non-defect
the model says it is done
✕ It will be very confident
✓ goal_judgment: host_required, judgment handed back

The progress priority waterfall

Top to bottom, the first state that matches is the current fact. Read-only, not writable.

cancelledtask cancelled → reopen only
terminalfinal (completed / done / abandoned)
in_flighta job / operation is running → wait
indeterminatecrash / unconfirmed cancel → host reconciles
stalegoal_change > file_change > pending_work
partialwritten but unverified → go to code_check
donevalidated, not stale, nothing in flight
openfresh task; route gives the first step

What the model actually receives at close

{
  "progress": "done",
  "next_action": null,
  "mechanical_requirements_met": true,
  "goal_judgment": "host_required"
}

These four lines are a contract, not copy: next_action:null means the tool has no next step; whether the business goal is truly met is not something the tool signs off on.

The review route is the exception

Green tests are not enough — completion is driven by audit. It enters awaiting_decision only when audit evidence applies to the current goal and files.

Twelve State Machines

Models forget. The disk does not.

12 state machines live in the workspace and are reprojected on every call. Context truncated, session restarted — the task does not get lost. That is the real patch for having no memory.

Task lifecycletask
active → cancelled ⇢ reopen returns to active
Cancel is terminal and never auto-recovers; aliases completed / done / abandoned
see task.lifecycle
Routeroute
understand change diagnose review maintain document isolate system
No longer eight copies of the pipeline; it only picks recommendations and completion criteria
see task.route
Nodenode
orient read edit check maintain worktree review cancelled
Advanced by operation / evidence events
see task.node
Progress projectionprojection
cancelled > terminal > in_flight > indeterminate
> stale > partial > done > open
Read-only, not writable; first match is the fact
see observation.progress
Evidence freshnessfreshness
goal_change > file_change > pending_work
Old evidence never beats new facts
see result_stale · stale_reason
Result kindresult_kind
validated partial analysis_only
Only a passing check plus a real patch counts as validated
see observation.result_kind
Check resultevidence
passed failed skipped unavailable unknown
Neither skipped nor unavailable counts as a pass
see evidence[].result
Background jobjob
running → cancelling → cancelled | succeeded failed
Orphaned process → indeterminate, never faked as done
see code_control(job)
Operationoperation
prepared succeeded failed indeterminate abandoned
One record per write / check, with hashes
see observation.operations
Write journaljournal
created → staged → committed / committed_recovered
rolled_back · needs_review · abandoned · undone
Reconciled after a crash by three-way hashes
see undo.token (24h)
Symbol conflictconflict
CLEAN · FILE_CHANGED_SYMBOL_STABLE
FILE_CHANGED · SYMBOL_CHANGED · SYMBOL_SIGNATURE_CHANGED
SYMBOL_DELETED · AMBIGUOUS_SYMBOL · NO_BASE
→ allow / allow_warn / reject
see conflict in write errors
Delegation ledgerledger
prepared succeeded failed indeterminate
request_id is idempotent; replays don't run blind
see receipt

The journal and delegation ledger exist mainly for crash reconciliation and idempotency; every other state appears in the response fields the LLM receives.

Architecture

One entry layer, four foundation layers

The entry layer can stay small because the complexity sits where it belongs: state on disk, index and parsing outside the process.

host / LLM any MCP client · decides intent and business judgment JSON-RPC over stdio · 120s hard limit MCP server · 13 entries by default read ×4 write ×8 shared ×1 state layer · on disk task.json · evidence · journal jobs · operation ledger · write locks crash reconcile: intended / pre / post hashes index layer SQLite (better-sqlite3 / sql.js) symbols · refs · files · deps WAL · integrity_check · per-workspace isolation sync after write workspace files · git · atomic write temp+rename · single writer Rust malong-parse tree-sitter · 10 languages · catch_unwind isolation Async jobs get their own 600s budget; MCP's 120s is a protocol hard limit — a timeout is not a code defect.
State lives outside the process

If the MCP process dies, the task does not go with it. On restart it reconciles against the journal: recover what can be recovered, mark the rest indeterminate.

Parsing lives outside the process

tree-sitter runs in a Rust service; panics are caught by catch_unwind and returned as error codes instead of taking the whole MCP process down.

Writes go through one writer

Write locks, batch locks, atomic writes. On the sql.js backend a second writer process is explicitly rejected — not quietly allowed to overwrite.

NOTE

The Six-He line is not a task drawn as a graph. On the task side there is only route, node, and next_action computed fresh each call — no persisted edges. The real graph is on the code side: {nodes, edges, cycles}. We put that distinction in the docs rather than blur it.

Toolbox

13 is the default, not the ceiling

The three tiers are parallel catalogs, not nested ones. Everything is plain regex / AST — zero LLM calls.

Six-He line (default)13
Graph + state layer. The one entry you use daily: code_task → code_read → code_patch/batch/maintain → code_check → code_control, plus 6 side exits.
MALONG_INTERFACE unset
classic (full)44
The full set of classic primitives: symbol read/write, index search, impact analysis, transactional edits, quality gates, test orchestration. Catalog frozen byte for byte against the golden.
MALONG_INTERFACE=classic
diagnostic (audit)18
The 13 Six-He entries + 5 analyzers: security_review / sweep_dead_code / dependency_gatekeeper / config_drift / repo_map.
MALONG_INTERFACE=diagnostic
read · 4
code_task
Start of any change: goal → candidates / structure / impact, then the route
code_read
Read a symbol / file; issues a read_ref version anchor
code_status
Task state, materials, and evidence at a glance
code_query
Read-only lookup with no task; 13 modes
write · 8
code_patch
Single-file patch / replace_symbol, dry_run by default
code_batch
N edits in one call (all or nothing) — N−1 fewer round trips
code_maintain
Import fixes; plan preview by default, dead code never auto-deleted
code_check
test/typecheck/lint with evidence recorded; async for long suites
code_control
status / resume / cancel / reopen / switch_route / expand / job / cancel_job
code_audit
Read-only analyzers + a false-positive ledger
code_worktree
Isolated worktree: preview / apply / status
invoke_capability
Deep exit: one classic tool per call
shared · 1
edit_transaction
Multi-file transaction + rollback / undo; also part of classic 44 (catalog frozen byte for byte)
Expand: the classic 44 deep catalog (7 groups)▸
I/O primitives ×3 — read_symbol · write_symbol · write_symbols
Index & search ×5 — reindex · symbol_search · code_search · repo_map · read_outline
Analysis & understanding ×8 — impact_analysis · call_chain · references · dep_graph · inspect · trace_symbol · active_todos · code_quality
Edit & refactor ×7 — edit_batch · edit_transaction · edit_collision_guard · git_worktree · rename_symbol · fix_imports · sandbox_validate
Quality & security ×8 — code_review · security_review · sweep_dead_code · guard_patterns · exception_guard · config_drift · mock_sync · naming_consistency
Engineering & verification ×8 — test_bridge · verify_pipeline · debug_runner · patch_parser · diff_facts · find_tests · spec_gen · style_sniffer
Dependencies & system ×5 — dependency_gatekeeper · tsc_check · health · gc · feedback
Proof

Context is the most expensive resource an LLM has

The tool catalog is re-sent with context on every turn. Do that math before talking throughput.

Default-tier catalog tax

16.5KB ≈ 4.86k tokens/turn
descriptions 2.0 KB ≈ 0.60k inputSchema 14.5 KB ≈ 4.26k (88%)

The counterintuitive part: trimming descriptions saves at most 0.6k. The only way to cut the catalog tax is to cut the parameter surface — which is exactly why 13 entries absorb the schemas of 44 tools.

The fixed edit path: 6 steps → 3 steps

Same scripted path — not a host-side baseline, not an agentic conversation.

old six-step chain6 calls · 7,673 chars
new primitives3 calls · 2,662 chars
token ↓65.3% calls ↓50.0%
BATCH EDITS

Multi-site edits save nine full rounds of context

code_batch packs N edits into one call (all or nothing). Edit site by site, and every turn re-sends the system prompt, tool catalog, conversation history, and the files already read; a batch sends once — N−1 fewer turns, multiplied by the whole context, not by the catalog tax — and N−1 fewer model waits.

saves ≈ 9 × full per-turn context (10 edit sites)
per-turn context 20k / 50k / 100k → ≈ 180k / 450k / 900k tokens
ESTIMATES; the 4.86k/turn catalog tax is only the fixed part
edit site by site 10 turns · full context re-sent each
code_batch 1 turn · sent once
saves 9 full-context re-sends
Same 10 edit sites; if the batch fails it rolls back whole — no half-applied change.
1ms
small-file read P95
7ms
small-file write P95
98ms
repo_map, whole repo
588/s
single-thread throughput
134MB
RSS @ 512MB cgroup
128
concurrency, zero OOM
166
JS test files
6,000+
end-to-end assertions
347
classic-44 contract assertions (byte-for-byte golden)
0
failures · 4 platform artifacts
Coverage write chain & transactions crash recovery (kill -9) dual-process concurrency MCP half-frame protocol 10-language parsing platform differences golden contract failure paths have dedicated cases
30+ rounds of “Malong auditing Malong” · r12–r30 dogfood, end to end
every review round came with more assertions — untested ground is where bugs hide
Get Started

Wire it up now

No build artifacts. Clone, install dependencies, point your MCP client at it.

bashstandard install (full better-sqlite3 backend)
# 1) clone
git clone https://github.com/wulun811/LiuHe.git

# 2) install (includes better-sqlite3)
cd LiuHe/malong && npm ci

# 3) start the MCP server
node --max-old-space-size=512 mcp-server.js \
  --workspace /path/to/project
opencode.json
{
  "mcp": {
    "malong": {
      "type": "local",
      "command": ["node", "--max-old-space-size=512",
                  "malong/mcp-server.js",
                  "--workspace", "."],
      "enabled": true
    }
  }
}
tree-sitter · 10 languages
parser runs in a Rust process
JavaScript TypeScript TSX Python Go Rust C C++ Java Bash