No description
  • TypeScript 99.2%
  • JavaScript 0.6%
  • Shell 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Simon Harms d781977d7c
Release 0.2.0: the harness carries the load
Bump to 0.2.0 and add a changelog for the release. The 0.2.0 line is the work
that makes a local model usable: the harness detects and runs the project's own
checks, hands failures back as the next turn's prompt, nudges a stuck model
instead of ending the turn, refuses blind edits, carries the work through
compaction, and keeps the prompt cache warm by splitting the context.

Verified before tagging: tsc clean, 286 tests pass, and the packed tarball
installs into a scratch project and runs `mcode --help`.

Co-authored-by: CommandCodeBot <[email protected]>
2026-09-23 21:22:43 -04:00
bin Ship MCP, prompt history, no-progress guards, and project validation 2026-09-23 17:20:51 -04:00
config Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00
scripts Ship the Laya sidecar, headless -p, and tool-call repair. 2026-09-22 15:31:42 -04:00
src Clear the status bars to the end of the row 2026-09-23 21:15:04 -04:00
test Clear the status bars to the end of the row 2026-09-23 21:15:04 -04:00
.gitignore Ignore the whole .mcode directory and .commandcode 2026-09-23 19:57:20 -04:00
AGENTS.md Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00
CHANGELOG.md Release 0.2.0: the harness carries the load 2026-09-23 21:22:43 -04:00
LICENSE Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00
llms.txt Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00
mudah.json Release 0.2.0: the harness carries the load 2026-09-23 21:22:43 -04:00
package-lock.json Release 0.2.0: the harness carries the load 2026-09-23 21:22:43 -04:00
package.json Release 0.2.0: the harness carries the load 2026-09-23 21:22:43 -04:00
README.md Refuse blind edits, and carry the work through compaction 2026-09-23 20:58:44 -04:00
tsconfig.json Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00
vitest.config.ts Ship mcode: a Mudah harness that starts malaikat for you. 2026-09-21 09:35:42 -04:00

mcode

A coding harness on Mudah. It starts malaikat for you. You do not start the server. You do not pick GGUF files by hand.

cd your-repo
npx @malaikat/mcode

Bare mcode opens the TUI. It runs malaikat serve for the last profile (default coding) if nothing is already healthy on the listen port. When Laya is enabled, that serve also starts the Laya sidecar on port 8081. Set MCODE_LAYA=0 to skip it.

Do now

  1. Install Node 26+
  2. Put malaikat on your PATH, or set MCODE_MALAIKAT
  3. Install mcode:
npm install -g @malaikat/mcode
  1. Install the classifiers: mcode install (needs git and uv)
  2. From a git repo, run mcode

The command name is mcode. npx @malaikat/mcode runs it without a global install.

From this checkout, ./scripts/install.sh links ~/.local/bin/mcode instead. If that directory is not on PATH, the script adds it to ~/.bashrc. Autoupdate stays off until @malaikat/mcode is on npm.

One-shot, no TUI. -p and --prompt do the same thing as ask:

mcode -p "find the grep tool and explain it" --yes

--cwd sets the repo. When you omit it, mcode uses the directory you launched from. --yes allows the ask class. The deny class stays blocked.

Slash commands

Command Action
/model Pick a malaikat profile (TUI). Or mcode model coding
/mode lean|minimal|full|plan Change the tool surface
/plan Inspect-only plan mode. /plan execute runs it
/goal <text> Durable goal. /goal complete runs the verification pass, then closes
/verify Run acceptance checks now. Failures report. No retry
/compact Compact the session now
/prefs Long-running preferences the classifier saved
/yes Allow every command for this session. The deny class stays blocked
/ask Ask again after /yes
/stop Stop the current turn. Escape does the same while a turn runs
/undo Restore files from write/edit and drop that turn. ctrl+z does the same. Shell side effects stay.
/quit or /exit Leave. Escape does the same when idle

ctrl+k opens the same actions in a palette.

Why this is not a thin wrapper

The tool surface follows the DeepSeek Harness local-GPU work. The system prefix is frozen so llama.cpp can cache it. Lean mode keeps read, write, edit, bash, grep, glob, find, skill, and criteria at about 2k tokens of schema. That is not a 7k "standard" dump. Minimal mode is too thin (edit+bash only). Full mode adds script so multi-step work can stay out of the prompt.

find is the one tool aimed at a model that is bad at guessing: you describe what you want in plain words ("where are wild encounters parsed") and it returns the files that cover the most of those words, code before prose, with sample lines. One call instead of five bad greps.

Context stays viable on smaller windows for four reasons.

Compaction reserves max_tokens in the pressure check. Old tool results get a short stub, not a full replay. When the window is full, the transcript is replaced by a memory that carries the work forward: the original request, the open task list, the acceptance checks, the decisions the model made, the files it touched, the commands it ran, and what has not passed yet. A small model loses the thread when the transcript becomes a file list; this keeps the thread.

edit needs a prior read. Editing a file the model has not read in this session — or one that changed on disk since it read — is refused with the reason and asked to read again. Blind editing against remembered text is how a small model loops forever: the file moves, the remembered snippet stops matching, and every retry fails. A full write is not blocked; it replaces the file rather than matching text in it.

The turn loop watches for progress rather than counting steps. Four signals feed one decision: the same tool call with the same result (fingerprinted after stripping a leading cd <dir> && and collapsing whitespace), a different call that did the same thing for the same result (Laya judges that one), the same reply text, and editing one file over and over without reading it back. Reaching a signal twice earns a nudge — the harness tells the model to read the current file, check the text it is matching, and try something else — and the turn keeps going, so a small model can unstick itself without you saying "you are stuck". Repeat through the nudge and the turn ends, naming the reason. maxSteps (default 200, MCODE_MAX_STEPS) is only a runaway fuse; there is no cost to use the local GPU longer.

Skills are an index. The skill tool loads a body on demand. AGENTS.md enters the session once and truncates if it is huge. The default turn does not load MCP servers.

The context is split so the cacheable prefix stays intact. What rarely changes — workspace, mode, skills, AGENTS.md — sits right after the frozen prompt. What moves — the goal, the acceptance checks, the plan, preferences — travels at the tail of each request, replaced in place rather than appended. Rewriting the block near the top would invalidate llama.cpp's prompt cache and re-prefill the whole session; moving the volatile part to the tail means only a small message changes. The powerline shows cache 12k when the server reports it reused the prefix, so you can see it working. Extra argv for malaikat serve (--cache-reuse, --no-mmap, whatever your machine wants) goes in MCODE_LLAMA_ARGS or llamaArgs in the config.

Preferences

A local naive-Bayes classifier scores each user turn (durable / session / task / correction). Durable lines such as "always use bun instead of npm" go into .mcode/preferences.json. The session block lists them. If ~/.mcode/bin/mclass is present, mcode uses that binary first. Bayes runs when that file is missing.

mcode install clones mverify and mclass into ~/.mcode/opt, runs uv sync, downloads weights from Hugging Face (thesimonharms/mverify, thesimonharms/mclass), and writes wrappers at ~/.mcode/bin/mverify and ~/.mcode/bin/mclass. mcode calls those paths. PATH stays unchanged. mcode install --status prints what is ready.

Permissions

The scanner reads every bash line. It denies rm -rf /, curl | sh, and force-push to main. Other mutating commands ask. --yes allows the ask class. It still blocks the deny class. If Laya is up, it can raise an allow to ask, and it can still prompt on a command that does not match the request even when --yes is set. Policy lives in ~/.config/mcode/config.json and .mcode/config.json.

The prompt offers four answers: allow, allow for this session (this exact command), allow all this session, and deny. The third one stops every later prompt in the session, not just this command. /yes does the same from the prompt line, /ask turns it back off, and allowAllSession in the policy config sets it at startup. A deny rule is never weakened, even with the session opened up.

The harness keeps its own state out of your history. It drops a .mcode/.gitignore holding *, so a wildcard git add never picks up sessions, prompt history, goals, or checks, and the scanner denies staging .mcode or .commandcode by path.

Laya

Laya is a System-1 sidecar from malaikat (POST /predict on :8081). mcode starts it with the chat profile (-secondary laya). It scores each user turn (intent, plan fit, correction, tools) and each bash command (harm, mismatch). Scores stay in the harness. They do not enter the frozen system prompt.

Every answer is held to confidenceThreshold: a low-confidence choice is dropped, and a high needs_plan can switch the turn into plan mode when autoPlan is on. A high harm or mismatch still prompts even under --yes. The harness tells "sidecar unreachable" apart from "busy" and "no answer", says which in the log, and only pauses checks after three real connection failures — a timeout means the GPU is busy sharing with the main model, not that Laya is gone, so the judge stays available exactly when a loop is likeliest. Checks the user is not waiting on get a longer budget than the ones that gate a turn or a command.

Set MCODE_LAYA to a URL to pin the sidecar. Set MCODE_LAYA=0 to turn it off. If the sidecar is missing, mcode continues without it.

Config in .mcode/config.json:

{
  "laya": {
    "enabled": true,
    "url": "",
    "confidenceThreshold": 0.7,
    "harmThreshold": 1.5,
    "autoPlan": false
  }
}

MCODE_LAYA_HARM sets harmThreshold and MCODE_LAYA_AUTOPLAN=1 sets autoPlan.

The powerline shows a short badge (q, fix, plan?) after a scored turn, with ! when the turn was a correction.

MCP

Put MCP servers in .mcode/mcp.json or ~/.mcode/mcp.json. A project server with the same name wins. mcode starts them on stdio, lists their tools, and asks before each call. A server that fails to start is skipped.

{
  "mcpServers": {
    "files": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    }
  }
}

Verification

The harness sets up validation as soon as a session starts. It reads the repo and builds the project's own checks: typecheck, lint, test, and build from package.json (run with the lockfile's manager), plus cargo test, go test ./..., pytest -q, and a Makefile test target. Those run on every verify pass, before the model's checks, and the session block lists them. The startup line prints what was found. If nothing is found, the criteria tool is the only source.

The agent adds sharper checks with the criteria tool. The harness splits them:

  • Code checks — file exists, file matches a pattern, a command exits 0, an HTTP endpoint responds. The harness runs these first.
  • Fuzzy checks — everything else. A Verifier judges prompt vs output. The default adapter runs ~/.mcode/bin/mverify check. Set MCODE_MVERIFY or verify.mverifyPath to pin a path. If that file is missing, a stub passes so the harness still works. Run mcode install mverify to install it.

/goal complete runs the pass. Failures reopen the goal, append the failed checks, and retry up to verify.maxRetries (default 3). After that budget, the goal stays open. /verify runs the same pass and reports only.

The harness also verifies by itself. After any turn that changed files it runs the command checks — the detected project checks plus any the model wrote that pass the scanner's allow class — and hands a failure straight back to the model as the next turn's prompt, autoRetries times (default 1). That is the whole trick behind running local: the model does not have to remember to run the tests, and it is told what it broke instead of being trusted. The failure is digested to the lines that look like errors, not the tail of a test runner's progress output, so the model gets the signal and not the noise. The model's untouched fuzzy checks are skipped here (they need a verifier and cost time), and so is any check that would need approval, so nothing the model wrote can run unattended past the allow class. Turn it off with autoRun: false or MCODE_VERIFY_AUTORUN=0.

Config in .mcode/config.json:

{
  "verify": {
    "enabled": true,
    "maxRetries": 3,
    "confidenceThreshold": 0.7,
    "mverifyPath": "~/.mcode/bin/mverify",
    "autoRun": true,
    "autoRetries": 1
  }
}

Set enabled to false to skip the pass. The powerline shows v-ok or v-N after a run. The session log stores each report.

mverify answers one narrow question: does this output satisfy this prompt. Tests and builds stay with the harness.

Layout

Mudah commands live in src/commands. The agent loop, tools, malaikat launcher, and TUI live under src/harness. Tests use Vitest and Mudah TestApp / TestTui. llama.cpp output goes to ~/.local/share/mcode/logs/malaikat.log, not the chat. The desk uses an Airline-style status line. ctrl+k opens the command palette. F1 opens the key list. Up and down cycle the prompts you have already sent, so a repeat is one key away.

mcode creates .mcode/ in the workspace you launch it from and keeps the project's state there: sessions/ (a JSONL log per turn, and a .hung.json when a turn stalls), goals/, plans/, preferences.json, criteria.json, and history (the prompts you sent, oldest first, for the up/down keys). The global ~/.mcode keeps the installed companions and their binaries instead.