- TypeScript 99.2%
- JavaScript 0.6%
- Shell 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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]> |
||
| bin | ||
| config | ||
| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| LICENSE | ||
| llms.txt | ||
| mudah.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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
- Install Node 26+
- Put
malaikaton your PATH, or setMCODE_MALAIKAT - Install mcode:
npm install -g @malaikat/mcode
- Install the classifiers:
mcode install(needsgitanduv) - 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
Verifierjudges prompt vs output. The default adapter runs~/.mcode/bin/mverify check. SetMCODE_MVERIFYorverify.mverifyPathto pin a path. If that file is missing, a stub passes so the harness still works. Runmcode install mverifyto 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.