{"id":51586062,"url":"https://github.com/harehare/minder","last_synced_at":"2026-07-11T10:32:19.385Z","repository":{"id":369388363,"uuid":"1288268099","full_name":"harehare/minder","owner":"harehare","description":"A coding-agent harness in Rust.","archived":false,"fork":false,"pushed_at":"2026-07-05T02:44:30.000Z","size":424,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-05T04:18:41.811Z","etag":null,"topics":["agent","ai","harness","llm"],"latest_commit_sha":null,"homepage":"https://harehare.github.io/minder/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/harehare.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-03T12:36:43.000Z","updated_at":"2026-07-05T02:44:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/harehare/minder","commit_stats":null,"previous_names":["harehare/minder"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/harehare/minder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/harehare%2Fminder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/harehare%2Fminder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/harehare%2Fminder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/harehare%2Fminder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/harehare","download_url":"https://codeload.github.com/harehare/minder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/harehare%2Fminder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35360371,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","harness","llm"],"created_at":"2026-07-11T10:32:19.312Z","updated_at":"2026-07-11T10:32:19.375Z","avatar_url":"https://github.com/harehare.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"assets/logo.svg\" style=\"width: 128px; height: 128px;\"/\u003e\n\n\u003ch1\u003eminder\u003c/h1\u003e\n\n**A coding-agent harness in Rust.**\n\n[![CI](https://github.com/harehare/minder/actions/workflows/ci.yml/badge.svg)](https://github.com/harehare/minder/actions/workflows/ci.yml)\n[![Security audit](https://github.com/harehare/minder/actions/workflows/audit.yml/badge.svg)](https://github.com/harehare/minder/actions/workflows/audit.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n[Website](https://harehare.github.io/minder/)\n\n\u003c/div\u003e\n\nMulti-provider (Anthropic, OpenAI, Gemini, Ollama) coding-agent harness with policy/observability\nhooks written in [`mq`](https://github.com/harehare/mq)'s embeddable query language rather than a\ngeneral-purpose scripting language — `mq-lang` has no builtin for file writes, network requests, or\nprocess execution, so hooks can observe, block, or transform agent behavior without being able to\ncause side effects themselves.\n\nThe agent loop is a standard ReAct-style tool-calling loop; hooks only answer narrow policy\nquestions at five fixed interception points and never drive the loop itself. Every tool call and\nresult streams live to the terminal — see [Live execution display](#live-execution-display).\n`mq-lang` also powers the harness's [autonomous loop mode](#autonomous-loop-mode): `minder loop\nTODO.md` re-queries a Markdown checklist after each turn and keeps working through unchecked items,\nwith no user in the loop and no external `mq` binary required.\n\n\u003e [!IMPORTANT]\n\u003e This project is under active development and has not been thoroughly tested end to end yet. Providers, tools, and hooks work individually in unit tests, but the full agent loop hasn't seen broad real-world verification — expect rough edges.\n\nSee `crates/minder-core`, `crates/minder-providers`, `crates/minder-tools`, `crates/minder-tools-wasm`, `crates/minder-tools-mcp`, `crates/minder-hooks`, `crates/minder-cli`.\n\n## Contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Providers](#providers)\n- [Tools](#tools)\n- [Skills](#skills)\n- [Subagents](#subagents)\n- [Hooks](#hooks)\n- [Tool plugins (WASM)](#tool-plugins-wasm)\n- [MCP servers (optional)](#mcp-servers-optional)\n- [Autonomous loop mode](#autonomous-loop-mode)\n- [Project layout](#project-layout)\n- [Development](#development)\n\n## Install\n\nRequires a recent stable Rust toolchain (`rustup` recommended).\n\n```sh\ncargo install minder-cli\nminder \"...\"\n```\n\nOr build from a clone:\n\n```sh\ngit clone https://github.com/harehare/minder.git\ncd minder\ncargo build --workspace --release\n```\n\nRun the CLI in place with `cargo run -p minder-cli --`, or install the `minder` binary onto your\n`PATH`:\n\n```sh\ncargo install --path crates/minder-cli\nminder \"...\"\n```\n\nPrebuilt binaries for Linux/macOS/Windows are also attached to each [GitHub\nRelease](https://github.com/harehare/minder/releases).\n\nThe rest of this README uses `minder \"...\"` for brevity — substitute `cargo run -p minder-cli --\n\"...\"` if you'd rather not install the binary.\n\n## Quick start\n\nminder takes a single task string as its only argument and runs it to completion.\n\n```sh\n$ export ANTHROPIC_API_KEY=sk-ant-...\n$ cd path/to/some/project\n$ minder \"list the top-level files and summarize what this project does\"\nloaded hooks from .agent/            # only printed if .agent/hooks/*.mq exist\n→ ls recursive=false\n✓ ls: Cargo.toml  README.md  crates/  ...\nThe project is a Rust workspace with six crates under crates/... (etc.)\n```\n\nEvery run's transcript is saved under `.agent/sessions/` (gitignored automatically) so you can\npick the conversation back up later instead of starting over:\n\n```sh\nminder chat                          # interactive session: type tasks, one process, shared context\nminder --continue \"and now add tests\" # resume the most recent session in this project, run one more task\nminder --continue                     # resume the most recent session interactively\nminder --resume \u003cid\u003e \"...\"            # resume a specific session by id (or an unambiguous prefix)\n```\n\nEach turn: the model reads the prompt, optionally calls a tool (`read_file`, `bash`, `grep`, ...),\nthe CLI runs it in the current working directory, and the result feeds back — repeating until the\nmodel replies without requesting another tool call. Everything the agent touches — files\nread/written, commands run — is scoped to the directory `minder` was launched from.\n\n### Live execution display\n\nEvery tool call streams to the terminal as it happens, not just the final answer — useful for\nwatching the agent work and for debugging a stuck turn. Output is split across two streams so\npiping `minder`'s answer elsewhere stays clean:\n\n- **stdout** — the conversation itself: any assistant text, including commentary the model emits\n  on turns where it also calls a tool (previously dropped silently, now shown live).\n- **stderr** — the execution trace: `● tool_name(key=value)` before each call, then either a diff\n  stat line (`+N -N`) followed by a colorized, indented unified diff (for `write_file`/`edit_file`\n  — capped at 40 lines with a `… N more line(s)` trailer so one big rewrite can't flood the\n  terminal) or a `✓`/`✗` one-line result summary.\n\n```sh\n$ minder \"fix the off-by-one in the pagination helper\"\n● grep(pattern=page_size)\n  ✓ src/pagination.rs:42:    let end = start + page_size;\n● edit_file(path=src/pagination.rs)\n  ✓ +1 -1\n  --- a/src/pagination.rs\n  +++ b/src/pagination.rs\n  @@ -40,2 +40,2 @@\n  - let end = start + page_size;\n  + let end = start + page_size - 1;\nFixed the off-by-one: `end` was one past the last valid index.\n```\n\nColors turn off automatically when stderr isn't a terminal (e.g. redirected to a file) or when\n`NO_COLOR` is set. Every line here is also overridable per-project from `.agent/hooks/*.mq` — see\n[Customizing the display](#customizing-the-display) under Hooks.\n\nWhen stderr is a terminal, a spinner also runs while the model is thinking or a tool call is\nstill executing (`⠋ Thinking (2.3s)`, `⠋ Running bash (0.8s)`), so a long step never looks stalled;\nit's replaced by the normal `✓`/`✗` line as soon as that step finishes.\n\nA few real tasks to try:\n\n```sh\nminder \"run the tests and summarize any failures\"\nminder \"find all TODO comments under src/ and turn them into a checklist\"\nminder \"explain what crates/minder-hooks/src/lib.rs does\"\nminder \"check git status and stage+commit the pending changes with a sensible message\"\n```\n\nA baseline policy is always active, even with no `.agent/hooks/` at all: it blocks `bash rm -rf`,\nand reading/searching paths under `.env`, `node_modules`, or `.git`. `write_file`/`edit_file` and\neverything else stay unrestricted by default — drop a hook (see [Hooks](#hooks)) into\n`.agent/hooks/` for any project where you want more policy between the model and your\nfilesystem/shell before pointing it at real work. A project's own `on_tool_call` fully replaces the\nbaseline (call `default_on_tool_call(call)` from yours to keep it and layer more checks on top).\n\n## Providers\n\nSelected via `MINDER_PROVIDER`; `MINDER_MODEL` overrides the default model for whichever provider\nis active.\n\n| `MINDER_PROVIDER` | Required env | Default model | Notes |\n|---|---|---|---|\n| `anthropic` (default) | `ANTHROPIC_API_KEY` | `claude-sonnet-5` | |\n| `openai` | `OPENAI_API_KEY` | `gpt-5.4-mini` | |\n| `gemini` | `GEMINI_API_KEY` | `gemini-3.5-flash` | |\n| `ollama` | none | `llama3.2` | needs a local `ollama serve`; override the endpoint with `OLLAMA_BASE_URL` |\n\n```sh\n# Anthropic (default)\nANTHROPIC_API_KEY=... minder \"run the tests and summarize failures\"\n\n# OpenAI\nMINDER_PROVIDER=openai OPENAI_API_KEY=... MINDER_MODEL=gpt-5.4 minder \"...\"\n\n# Gemini\nMINDER_PROVIDER=gemini GEMINI_API_KEY=... minder \"...\"\n\n# Ollama (local, no key needed)\nMINDER_PROVIDER=ollama MINDER_MODEL=llama3.2 minder \"...\"\nOLLAMA_BASE_URL=http://localhost:11434 MINDER_PROVIDER=ollama minder \"...\"\n```\n\n### Running with gpt-oss\n\n[gpt-oss](https://openai.com/index/introducing-gpt-oss/) (OpenAI's open-weight models,\n`gpt-oss-20b`/`gpt-oss-120b`) runs through the existing `ollama` provider — no minder changes\nneeded, since Ollama handles the gpt-oss-specific translation over its generic `/api/chat`\nendpoint.\n\n1. Install Ollama v0.11.4+ (needed for correct gpt-oss tool-calling support):\n   \u003chttps://ollama.com/download\u003e, or:\n\n   ```sh\n   # macOS\n   brew install ollama\n   # Linux\n   curl -fsSL https://ollama.com/install.sh | sh\n   ```\n2. Start the server (skip this if your install already runs it as a background service):\n\n   ```sh\n   ollama serve\n   ```\n3. Pull a gpt-oss model (`20b` needs ~16GB RAM/VRAM; `120b` needs ~65GB+, multi-GPU/datacenter-class\n   hardware) — start with `20b` unless you know you have the headroom:\n\n   ```sh\n   ollama pull gpt-oss:20b\n   # or, if your hardware can take it:\n   ollama pull gpt-oss:120b\n   ```\n4. Point minder at it:\n\n   ```sh\n   MINDER_PROVIDER=ollama MINDER_MODEL=gpt-oss:20b minder \"...\"\n\n   # remote/non-default Ollama host:\n   OLLAMA_BASE_URL=http://your-ollama-host:11434 MINDER_PROVIDER=ollama MINDER_MODEL=gpt-oss:20b minder \"...\"\n   ```\n\ngpt-oss's reasoning effort (low/medium/high) isn't configurable through minder today — it runs at\nOllama's default for the model. `minder loop` (see below) works the same way with `gpt-oss` as\nwith any other provider, since it drives `AgentSession::run_turn` generically.\n\n## Tools\n\nAlways registered:\n\n| Tool | Does |\n|---|---|\n| `read_file` | Reads a file, optionally restricted to a 1-indexed inclusive line range |\n| `write_file` | Creates or overwrites a file, creating parent directories as needed |\n| `edit_file` | Replaces `old_string` with `new_string` in a file (must match exactly once unless `replace_all`) |\n| `bash` | Runs a shell command and returns combined stdout/stderr (default 120s timeout) |\n| `glob` | Finds files matching a glob pattern, e.g. `**/*.rs` |\n| `grep` | Searches file contents by regex, honoring `.gitignore`, up to 200 matches |\n| `ls` | Lists a directory, honoring `.gitignore`; `recursive` for a tree view, up to 500 entries |\n| `git_diff` | Shows `git diff` output, against a ref, staged or unstaged, optionally path-scoped |\n| `git_log` | Shows commit history |\n| `git_status` | Shows `git status` |\n| `git_commit` | Creates a commit |\n| `worktree_add` | Creates a new git worktree (`git worktree add`), optionally on a new or existing branch |\n| `worktree_list` | Lists every worktree linked to the repo (`git worktree list`) |\n| `worktree_remove` | Removes a worktree and its directory (`git worktree remove`), leaving its branch intact |\n| `web_fetch` | Fetches an http(s) URL as text; rejects non-http(s) schemes and literal loopback/private-network hosts (partial SSRF guard, not a complete one — use a hook for stronger guarantees) |\n| `agent` | Delegates a task to a named subagent — always available via a built-in `general-purpose` subagent, no project config required; see [Subagents](#subagents) |\n\nRegistered only when configured:\n\n| Tool | Enabled by |\n|---|---|\n| `web_search` | `TAVILY_API_KEY` set — omitted entirely otherwise, so the model never sees a tool it can't use |\n| `skill` | one or more `.agent/skills/*/SKILL.md` files present — see [Skills](#skills) |\n\nWorktree tools let the agent check out a second branch into its own directory without disturbing\nthe current one — e.g. running `main`'s test suite for comparison while mid-edit on a feature\nbranch, or fanning a review out into a [subagent](#subagents) that works in isolation. minder\nitself doesn't switch its own working directory between worktrees; point a fresh `minder` process\n(or a `worktree_add`-created path passed to `bash`) at the new directory to actually work inside it.\n\nAdditional tools can be supplied per-project as WASM plugins — see [Tool plugins (WASM)](#tool-plugins-wasm)\n— or from MCP servers, behind an opt-in feature — see [MCP servers (optional)](#mcp-servers-optional).\n\n## Skills\n\n```\n.agent/skills/commit-messages/SKILL.md\n```\n\n```markdown\n---\nname: commit-messages\ndescription: Writes commit messages in this repo's conventional-commit style\n---\n# Commit messages\n\nUse Conventional Commits: `\u003ctype\u003e(\u003cscope\u003e): \u003csummary\u003e`, imperative mood...\n```\n\nEach skill is a directory with a `SKILL.md`: `---`-delimited frontmatter (`name`, `description`)\nfollowed by instructions as the body. minder discovers every `.agent/skills/*/SKILL.md` at startup\nand registers a single `skill` tool listing each skill's name/description — cheap to keep in\ncontext every turn. The model calls `skill` with a `name` to pull that skill's full body into the\nconversation only when it's actually relevant.\n\nSkill names must be unique, and startup fails if a `SKILL.md` is missing frontmatter or the\n`name`/`description` fields. See `skills/commit-messages/SKILL.md` for a runnable example (copy\n`skills/` to `.agent/skills/` in a project to try it).\n\n## Subagents\n\n```\n.agent/agents/code-reviewer/AGENT.md\n```\n\n```markdown\n---\nname: code-reviewer\ndescription: Reviews a diff for correctness bugs, not style\ntools: read_file, grep, glob, git_diff, git_log\n---\n# Code reviewer\n\nReview the diff you're handed for correctness bugs only...\n```\n\nEach subagent is a directory with an `AGENT.md`: the same `---`-delimited frontmatter shape as a\n[skill](#skills) (`name`, `description`), plus an optional `tools` field (comma-separated tool\nnames) and a body that becomes that subagent's system prompt. minder always registers a single\n`agent` tool listing every available subagent's name/description — the model calls it with\n`{name, task}` to delegate a self-contained piece of work. Unlike `skill`, this isn't gated behind\nany project config: a built-in `general-purpose` subagent (full tool access, no `AGENT.md`\nrequired) is always in the list, the same role a fresh standalone `minder` process would play. A\nproject can override `general-purpose` or add more by defining `.agent/agents/\u003cname\u003e/AGENT.md`\nwith a matching `name`.\n\nDelegating runs a brand-new `AgentSession` in-process, to completion, with its own history (it\nstarts with no memory of the parent conversation, so `task` needs to carry enough context to act\non) and its own system prompt (the subagent's `AGENT.md` body, not minder's default). What it\nshares with the parent rather than rebuilding: the LLM provider/client (no second API handshake),\nthe project's [hooks](#hooks) (the same policy layer applies inside a subagent's tool calls), and\nthe live [execution display](#live-execution-display) (a subagent's tool calls stream to the\nterminal exactly like the parent's).\n\nA subagent's tool list is either every tool the parent has (`tools` omitted) or restricted to the\ncomma-separated names in `tools` (unknown names are silently dropped, not an error — lets a\nfrontmatter list stay stale-safe as tools are renamed). Either way, the `agent` tool itself is\nalways excluded from a subagent's own tool list, so a subagent can delegate work but can never\nspawn a further subagent — one level of delegation, no unbounded recursion.\n\nIf the model requests more than one `agent` call in the same turn, they run concurrently rather\nthan one after another — each delegation starts with no shared history with any other call in the\nsame batch, so unlike e.g. two `write_file` calls that might touch the same path, there's nothing\nfor them to race on. Every other tool call still runs strictly sequentially, in the order the model\nrequested it.\n\nSubagent names must be unique, and startup fails if an `AGENT.md` is missing frontmatter or the\n`name`/`description` fields, same as skills. See `agents/code-reviewer/AGENT.md` for a runnable\nexample (copy `agents/` to `.agent/agents/` in a project to try it).\n\n## Hooks\n\n```\n.agent/hooks/security.mq\n```\n\n```mq\ndef on_tool_call(call):\n  if (call[\"name\"] == \"bash\" \u0026\u0026 contains(call[\"arguments\"][\"command\"], \"rm -rf\")):\n    {\"action\": \"block\", \"reason\": \"destructive bash command blocked by policy\"}\n  else:\n    {\"action\": \"allow\", \"value\": call};\n```\n\nEvery hook returns `{\"action\": \"allow\", \"value\": ...}` or `{\"action\": \"block\", \"reason\": \"...\"}`\n(the gate-only hook `before_compact` returns `{\"action\": \"allow\"}` with no `value`). Hooks are\noptional — if a hook function isn't defined, the corresponding interception point is a no-op. A\nbuggy `on_tool_call` fails **closed** (blocks the action); every other hook point fails **open**\n(the buggy transform is skipped).\n\n| Hook point | mq function | Fires |\n|---|---|---|\n| `before_agent_start` | `before_agent_start(prompt)` | Once, before the first LLM call |\n| `on_context` | `on_context(messages)` | Before every LLM call |\n| `on_tool_call` | `on_tool_call(call)` | Before a tool executes (fails closed) |\n| `on_tool_result` | `on_tool_result(result)` | Before a tool's result re-enters history |\n| `before_compact` | `before_compact(messages)` | Before history is truncated under context pressure |\n\nSee `hooks/security.mq` for a runnable example (copy it to `.agent/hooks/` in a project to try it).\n\n### The `agent` module\n\nA small set of convenience functions is always loaded before any hook file, bare-callable with no\n`import` needed (an `agent_` prefix keeps them out of your own functions' way — redefine one\nyourself and your version simply shadows it):\n\n| Function | Returns |\n|---|---|\n| `agent_content_blocks(messages)` | Every message's `content` array, flattened into one array of blocks |\n| `agent_tool_calls(messages)` | All `tool_use` blocks so far, unwrapped to `{id, name, arguments}` |\n| `agent_tool_results(messages)` | All `tool_result` blocks so far, unwrapped to `{tool_call_id, content, is_error}` |\n| `agent_assistant_texts(messages)` | Every assistant-authored text string, in order |\n| `agent_tool_names(messages)` | Distinct tool names called so far |\n| `agent_error_count(messages)` | Count of tool results with `is_error: true` |\n| `agent_consecutive_errors(results)` | Trailing streak of `is_error: true` results (feed it `agent_tool_results(messages)`) |\n| `agent_last_n(items, n)` | The last `n` items of any array |\n\n```mq\n# .agent/hooks/circuit_breaker.mq -- stop the turn once 3 tool calls in a row have failed.\n# on_context sees the full history (on_tool_call only sees the one call about to run), so\n# that's where a check like this belongs.\ndef on_context(messages):\n  if (agent_consecutive_errors(agent_tool_results(messages)) \u003e= 3):\n    {\"action\": \"block\", \"reason\": \"3 consecutive tool failures -- pausing for a human\"}\n  else:\n    {\"action\": \"allow\", \"value\": messages};\n```\n\n### Default policy\n\n`default_policy.mq` loads right after the `agent` module, before any project hook file, and\ndefines a baseline `on_tool_call`: it blocks `bash` commands containing `rm -rf`, and any\n`read_file`/`grep`/`glob`/`ls` call whose `path`/`pattern` touches `.env`, `node_modules`, or\n`.git`. This is active in every project, with zero `.agent/` setup.\n\nDefining your own `on_tool_call` fully replaces it (same shadowing rule as the `agent` module) --\ncall `default_on_tool_call(call)` from yours to keep the baseline checks and layer your own on top:\n\n```mq\ndef on_tool_call(call):\n  if (call[\"name\"] == \"web_fetch\"):\n    {\"action\": \"block\", \"reason\": \"no network in this project\"}\n  else:\n    default_on_tool_call(call); # still blocks rm -rf, .env, node_modules, .git\n```\n\n### Overriding a tool's result\n\n`on_tool_call` can also `override`: `{\"action\": \"override\", \"value\": {\"content\": \"...\",\n\"is_error\": false, \"metadata\": null}}` supplies the tool's result directly. The real tool never\nruns, but the result still flows through `on_tool_result` afterward like any other, so\npost-processing hooks stay uniform either way. Useful for mocking a tool in tests, or for\nshort-circuiting it once some condition (like `agent_consecutive_errors` above) is met without\njust erroring out:\n\n```mq\ndef on_tool_call(call):\n  if (call[\"name\"] == \"web_fetch\"):\n    {\"action\": \"override\", \"value\": {\"content\": \"(network disabled in this environment)\", \"is_error\": false, \"metadata\": None}}\n  else:\n    {\"action\": \"allow\", \"value\": call};\n```\n\n### Customizing the display\n\nThe [live execution display](#live-execution-display) is driven by two more optional hook\nfunctions, checked before minder's own built-in formatting — same files, same loading, nothing\nextra to set up. Both fail **open**: a broken or undefined render function falls back to the\nbuilt-in look, since a display bug should never be able to affect what the agent actually does.\n\n| Function | Called with | Controls |\n|---|---|---|\n| `render_tool_call(call)` | the upcoming `ToolCall` | how the `● name(...)` header line prints |\n| `render_tool_result(arg)` | `{\"call\": ToolCall, \"outcome\": ToolExecOutcome}` | how the result/diff line(s) print |\n\nEach returns `{\"action\": \"default\"}` (use the built-in formatting), `{\"action\": \"hide\"}` (print\nnothing), or `{\"action\": \"text\", \"value\": \"...\", \"style\": \"...\"}` (print this instead — `style` is\none of `green`/`red`/`yellow`/`cyan`/`dim`/`bold`, or omitted/anything else for no styling):\n\n```mq\n# .agent/hooks/display.mq -- quiet git_status noise, and prefix bash calls with a shell-style `$`\ndef render_tool_call(call):\n  if (call[\"name\"] == \"git_status\"):\n    {\"action\": \"hide\"}\n  else:\n    {\"action\": \"default\"};\n\ndef render_tool_result(arg):\n  if (arg[\"call\"][\"name\"] == \"bash\"):\n    {\"action\": \"text\", \"value\": \"$ \" + arg[\"call\"][\"arguments\"][\"command\"], \"style\": \"cyan\"}\n  else:\n    {\"action\": \"default\"};\n```\n\nBoth `render_tool_call` and (via `arg[\"call\"]`) `render_tool_result` only see the one call/outcome\nin front of them, not the conversation — if a display decision needs history, reach for the\n[`agent` module](#the-agent-module) inside `on_context`/`before_compact` (which do see `messages`)\nand have that hook stash whatever's needed back onto the call/result some other way (e.g. blocking\nbefore it ever reaches the display layer).\n\n## Tool plugins (WASM)\n\nTools can also be provided by sandboxed WASI plugins, discovered from `.agent/tools/`:\n\n```\n.agent/tools/weather.wasm\n.agent/tools/weather.toml\n```\n\nEvery `.wasm` needs a sidecar `.toml` manifest of the same name declaring its capabilities — a\nplugin with no manifest fails to load rather than silently running with zero capabilities:\n\n```toml\nnetwork = false          # grants the one host-mediated fetch primitive (see below)\n\n[[fs]]\nhost_dir = \"./data\"      # resolved relative to the working directory\nguest_dir = \"/data\"\nread_only = true\n\n[limits]\ntimeout_secs = 30\nmax_memory_pages = 256\nfuel = 5_000_000\n```\n\nPlugins are plain `wasm32-wasip1` modules (no component model) exporting `minder_tool_name`,\n`minder_tool_description`, `minder_tool_parameters_schema`, `minder_tool_execute`, plus\n`minder_alloc`/`minder_dealloc` for passing JSON across linear memory — see\n`crates/minder-tools-wasm/tests/fixtures/echo_plugin` for a minimal example (`regenerate.sh`\nalongside it has the build command). Filesystem access is granted per-plugin via WASI preopens\n(none by default); network isn't a raw socket — `network = true` grants a single `host_web_fetch`\nimport reusing the built-in `web_fetch`'s SSRF guard. Execution is metered with wasmtime fuel, so a\nrunaway plugin traps instead of hanging.\n\n## MCP servers (optional)\n\nMCP is a client/server protocol built around subprocesses and long-lived JSON-RPC sessions, which\ndoesn't fit the WASI sandbox above (no arbitrary process execution by design), so it's wired in on\nthe host side instead, behind an opt-in `mcp` Cargo feature so the `rmcp` dependency and its\nsubprocess-spawning code aren't part of the binary unless you ask for them:\n\n```sh\ncargo install --path crates/minder-cli --features mcp\n```\n\nWith the feature enabled, minder discovers `.agent/mcp.toml`, launches each configured server as a\nchild process over the stdio transport, and registers every tool it advertises as an\n`agent_core::Tool` named `mcp__\u003cserver\u003e__\u003ctool\u003e`:\n\n```toml\n# .agent/mcp.toml\n[[server]]\nname = \"filesystem\"\ncommand = \"npx\"\nargs = [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/path/to/dir\"]\n\n[[server]]\nname = \"github\"\ncommand = \"docker\"\nargs = [\"run\", \"-i\", \"--rm\", \"ghcr.io/github/github-mcp-server\"]\nenv = { GITHUB_PERSONAL_ACCESS_TOKEN = \"...\" }\n```\n\nBuilt without `--features mcp`, minder ignores `.agent/mcp.toml` entirely (the `mcp` tool /\n`minder-tools-mcp` crate is compiled out, not just disabled at runtime). A configured server that\nfails to start, initialize, or list its tools is a hard error at startup, same as a broken wasm\nplugin or hook file. Remote tool calls are opaque to `on_tool_call`/`on_tool_result` hooks in the\nsame way built-in and wasm tool calls are — nothing MCP-specific bypasses the hook layer.\n\n## Autonomous loop mode\n\n```sh\nminder loop TODO.md\nminder loop TODO.md \"ship the v2 pagination rewrite\"   # optional overall-goal hint\n```\n\n`minder loop \u003cfile\u003e [\"\u003cgoal\u003e\"]` drives the same `AgentSession` turn after turn against a Markdown\nchecklist, with no user in the loop, and keeps watching the file for new work once it's clear:\n\n1. Query `\u003cfile\u003e` for GFM checklist lines that are still unchecked, entirely inside `mq-lang` —\n   embedded the same way as the [hooks](#hooks) engine, just pointed at a file on disk instead of\n   the conversation:\n\n   ```\n   read_file(path) | split(., \"\\n\") | filter(., fn(line): is_regex_match(line, \"^\\\\s*[-*+]\\\\s+\\\\[ \\\\]\") end)\n   ```\n\n   This matches lines with a regex rather than walking markdown's list/checkbox AST, since\n   mq-lang's only builtin that parses markdown into that AST (`collection`) works over an entire\n   directory tree — overkill for re-checking one file every few seconds.\n2. If nothing comes back, the file is done: minder logs that it's idle and polls `\u003cfile\u003e` on an\n   interval for new items.\n3. Otherwise the remaining items are folded into a prompt (\"pick the first unfinished item,\n   implement it, then check it off in `\u003cfile\u003e`\") and handed to `run_turn`.\n4. Repeat — the item just finished no longer shows up as unchecked, so the next prompt derives\n   naturally from the file's current state, not a stale plan.\n\n```markdown\n\u003c!-- TODO.md --\u003e\n## Backend\n- [x] Set up database schema\n- [ ] Add user authentication endpoint\n- [ ] Write tests for auth endpoint\n```\n\n```sh\n$ minder loop TODO.md\n[loop 1/50] 2 item(s) remaining in TODO.md\n→ write_file path=src/auth.rs\n...\n[loop 2/50] 1 item(s) remaining in TODO.md\n→ edit_file path=tests/auth_test.rs\n...\n[loop] TODO.md has no unchecked items -- polling every 5s for new work (Ctrl-C to stop)\n```\n\nAt that point the process just keeps running: add a new `- [ ] ...` line to `TODO.md` (by hand, or\nhave something else append to it) and the next poll picks it up automatically, no restart needed.\nStop it with Ctrl-C when you're done.\n\nSafety limits keep a stuck agent from spinning forever, all overridable via env vars:\n\n| Env var | Default | Guards against |\n|---|---|---|\n| `MINDER_LOOP_MAX_ITERATIONS` | 50 | Runaway spend — a lifetime cap on actual working turns (idle polling doesn't count against it) |\n| `MINDER_LOOP_POLL_INTERVAL_SECS` | 5 | How often to re-check the file while idle |\n| `MINDER_LOOP_QUERY` | `read_file(path) \\| split(., \"\\n\") \\| filter(., fn(line): is_regex_match(...) end)` (see above) | Lets you point at a differently-structured file (a custom \"done\" marker, a different bullet convention, ...) |\n\nIf the unchecked count doesn't drop for two consecutive working iterations, the loop stops with an\nerror rather than burning turns on a task the model isn't making progress on.\n\n### Resilience for unattended runs\n\nSince a loop is meant to run with nobody watching, minder guards against the ways an unattended\nprocess actually dies:\n\n- **Transient provider errors don't end the run.** Rate limits, 5xx responses, and network blips\n  are retried with backoff (honoring the provider's `Retry-After` when it sends one) before ever\n  reaching your prompt or the checklist logic — a multi-hour loop survives the kind of hiccup that\n  used to kill the whole process on turn one.\n- **The session survives a restart.** `minder loop \u003cfile\u003e` keys its session by `\u003cfile\u003e`'s canonical\n  path and saves after every turn (see [Quick start](#quick-start)), so re-running the exact same\n  command after a crash, a Ctrl-C, or a container restart resumes the conversation instead of\n  starting over — at most the in-flight turn is lost.\n- **A durable log, even off-terminal.** Set `MINDER_LOG_FILE=path/to/log` to also append every\n  turn/tool-call/tool-result/retry as a plain-text line to that file, independent of the terminal —\n  useful for a loop launched under `nohup`, `tmux`, or systemd, where stderr isn't being watched\n  live.\n\n## Project layout\n\n| Crate | Responsibility |\n|---|---|\n| `minder-core` | Session/turn loop, tool-calling protocol, hook port trait |\n| `minder-providers` | Anthropic/OpenAI/Gemini/Ollama client implementations |\n| `minder-tools` | Built-in tools (file, shell, git, web) |\n| `minder-tools-wasm` | WASI plugin loader and sandboxed host runtime |\n| `minder-tools-mcp` | MCP client — spawns configured servers, exposes their tools (opt-in `mcp` feature on `minder-cli`) |\n| `minder-hooks` | `mq`-based hook engine (`.agent/hooks/*.mq`) |\n| `minder-cli` | The `minder` binary — wires providers/tools/hooks together |\n\n## Development\n\nRequires [`just`](https://github.com/casey/just). The same recipes run in CI ([`ci.yml`](.github/workflows/ci.yml)):\n\n```sh\njust test-all   # fmt --check, clippy -D warnings, doc tests, nextest\njust fmt         # cargo fmt --all -- --check\njust lint        # cargo clippy --all-targets --all-features --workspace -- -D clippy::all\njust test        # cargo nextest run --workspace --all-features\njust deps        # cargo machete (unused dependencies)\njust audit       # cargo deny check (licenses/bans/sources/advisories)\n```\n\nEach provider's live round-trip test needs real credentials and is `#[ignore]`d by default:\n\n```sh\nANTHROPIC_API_KEY=... cargo test -p minder-providers -- --ignored anthropic\nOPENAI_API_KEY=... cargo test -p minder-providers -- --ignored openai\nGEMINI_API_KEY=... cargo test -p minder-providers -- --ignored gemini\ncargo test -p minder-providers -- --ignored ollama  # needs `ollama serve` running locally\n```\n\n### CI and releases\n\nEvery push/PR to `main` runs tests (Linux; the full Linux/macOS/Windows matrix is available via\n`workflow_dispatch`), `rustfmt`, `clippy`, and `cargo-deny`. Separate scheduled/PR-triggered\nworkflows cover `cargo audit`, CodeQL, spell-checking (`typos`), unused-dependency detection\n(`cargo-machete`), and Actions-workflow security linting (`zizmor`).\n\nPushing a `vX.Y.Z` tag builds the `minder` binary for Linux (gnu/musl, x86_64/aarch64), macOS\n(aarch64), and Windows (x86_64) and attaches them — with checksums — to a draft GitHub Release\n(review and publish it manually). See [`release.yml`](.github/workflows/release.yml).\n\nAll crates publish to crates.io under the `minder-*` prefix (`cargo install minder-cli`), and a\n`vX.Y.Z` tag push also runs [`cargo-publish.yml`](.github/workflows/cargo-publish.yml) (needs a\n`CARGO_REGISTRY_TOKEN` secret). Prefer a GitHub Release binary if you don't want to build from\nsource (see [Install](#install)).\n\n## License\n\nMIT\n\u003c/content\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fharehare%2Fminder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fharehare%2Fminder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fharehare%2Fminder/lists"}