Skip to content

v2.20.0

Choose a tag to compare

@nokhodian nokhodian released this 29 Sep 21:44
· 596 commits to main since this release

[2.20.0] — 2026-09-29

Added

  • agent exec reports subagents on every runtime, not only claude (#387, capability agent-exec-subagent-events, agent-exec protocol rev 24). On any other runtime, a tool call whose tool_activity has kind:"task" is now followed by a subagent started event, and its end by finished with status (completed, failed, denied, or stopped on cancel/timeout) and a summary of up to 500 characters of its output. id and tool_use_id are the call's tool_activity id. The events are synthesized from tool_activity, so they have no progress phase and no usage. opencode (task), cline (spawn_agent, team_*) and dsh (subagent) mark such calls themselves; other runtimes get task from the shared tool-name table (Task, Agent, spawn_subagent, invoke_subagent, browser_subagent, …). claude keeps its SDK-based events and gets no duplicates. New golden fixture doc/agent-exec-protocol/fixtures/subagent-synth.ndjson.

  • agent test --json takes --sandbox and --env, and reports native_sandbox (#474, agent-exec protocol rev 25, capability agent-test-sandbox). --sandbox read-only|workspace-write|full and repeatable --env KEY=V mean what they mean for agent exec, and are checked the same way. The result has a new native_sandbox field with the sandbox the vendor CLI really ran with, taken from the turn's start event (null when the turn never started). A mode the runtime does not have, such as --sandbox workspace-write on pi, returns status: "error" with error.code: "unsupported". The test turn keeps scoped access, and neither flag can loosen the sandbox: an org git level below push still caps codex and grok at workspace-write. Callers such as mono-agent can now test a runtime inside the sandbox their real turns use.

  • agent exec --sandbox has real restricted modes on more runtimes, and --sandbox-fallback makes an unsupported mode non-fatal (#482, agent-exec protocol rev 26, capabilities agent-exec-sandbox-restricted and agent-exec-sandbox-fallback). 2.19.0 offered read-only/workspace-write only on codex, grok and dsh, and any other mode was a fatal unsupported, so a caller sending one --sandbox value to every runtime broke claude and copilot chats. New mode restricted: the CLI's own approval rules apply and every call they would ask about is refused (a rule set, not a file-system boundary). New per runtime, each checked with a live turn that tried a write and a shell command: copilot read-only (--deny-tool=write --deny-tool=shell), workspace-write (--allow-tool=write --deny-tool=shell, edits confined to the cwd and temp dir, no shell) and restricted; antigravity restricted (no --dangerously-skip-permissions); opencode restricted (edit/bash/task/external_directory ask, every ask rejected); pi and pi-rpc read-only; claude read-only and workspace-write with --access scoped or read, enforced by monomind's own tool gate (native_sandbox: "monomind"). --sandbox-fallback fail|strictest|run (also on agent test): fail stays the default; strictest runs the closest mode at least as strict as the request (else the strictest there is); run runs the runtime default. A replaced mode emits a status notice, and start now carries sandbox_requested and sandbox_applied. agent scan --json entries gain sandbox_mode_reports, and the agent test --json result gains sandbox_applied. Without --sandbox, every runtime's argv is unchanged.

Changed

  • The dashboard no longer starts at every session start unless you opt in (#423). The SessionStart hook control-start.cjs started the Control Room server (:4242) for every initialised project, and the servers stayed resident: one machine had 8, two at 0.7–0.9 GB RSS. The hook now does nothing, and prints nothing, unless MONOMIND_DASHBOARD_AUTOSTART=1 is set or .monomind/dashboard.json has {"autostart": true} (written by the new monomind init --dashboard); MONOMIND_DASHBOARD_AUTOSTART=0 turns it off for a project that opted in. A server that is already running is left alone. monomind ui starts it by hand and monomind org run still starts it unless MONOMIND_DASHBOARD_AUTOSTART=0. Existing projects: the hook ships with the package helpers, so updating turns auto-start off there too; run monomind init --dashboard in a project to keep it. Without a running dashboard the per-prompt Second Brain lookup uses its keyword fallback.

  • monomind route task is monomind pick --agents, and the routing hooks carry pick's verdict (#430). route task (and bare monomind route "<task>") ranked through the picker but always named an agent: the top of a ranking pick called unconfident (shown as e.g. 72%), or coder when nothing matched, while pick said "no confident match". It now runs pick and prints exactly its output, text or --json; data.agentId is set only when pick is confident. hooks_route, hooks_pre-task and hooks_explain return pick's confident flag (hooks_route also its summary), pre-task names a primary agent only when pick is confident, and explain says when the top is below the bar. The hooks no longer load .claude/helpers/router.cjs, which no hook used for selection; the file stays for scripts and doctor. route semantic and the route layer (@monoes/routing) are marked deprecated in help and docs, not removed.

Removed

  • Hooks and helpers that ran every session, turn or prompt and did nothing (#417). monomind init no longer registers the SessionStart auto-memory-hook.mjs import and Stop auto-memory-hook.mjs sync hooks (both only printed "skipped — AutoMemoryBridge removed"), the SubagentStart hook-handler.cjs status hook or the Notification hook-handler.cjs notify hook (both only printed [OK] Hook: …). With no hooks left, it writes no Stop or Notification event, and the init wizard no longer offers them. The helpers auto-memory-hook.mjs, learning-service.mjs (nothing called record()) and memory-palace.cjs (nothing wrote its drawers, so its session-start wakeUp injection was always empty) are no longer installed. The UserPromptSubmit route hook no longer runs the [INTELLIGENCE] lookup, which rewrote .monomind/data/ranked-context.json (~870 KB) on every prompt and matched nothing; intelligence.cjs drops getContext() and bootstrapFromDb(). init upgrade leaves the old hooks and files of an existing install in place (they stay harmless no-ops); init --force removes auto-memory-hook.mjs and its settings hooks. intelligence-outcomes.jsonl is still written: session-end success detection, doctor's route-outcome correlation and hooks intelligence train read it.

Fixed

  • A role's git guard no longer leaks into repositories its own tests create (#481, follow-up to #298). Roles below policy.git: 'push' got core.hooksPath and core.excludesFile as process-wide GIT_CONFIG_* entries, so every git call in the process tree saw them, including a test's throwaway repo in $TMPDIR: git add .claude/settings.json there failed as ignored by the guard's excludes file (cleanup-after-init-e2e.test.ts). The guard now writes both settings to a gitconfig file in its state dir and exports only includeIf.gitdir:<common dir> and includeIf.gitdir:<common dir>/worktrees/ entries that include it, so they apply in the org's protected repositories and all their linked worktrees and nowhere else. Inside those repositories the guard is unchanged: pushes and, at read/none, ref updates are still refused. The transport and credential settings (protocol.file.allow=never, the reset credential.helper, failing askpass/ssh commands) stay process-wide, so a scratch repository still cannot push.

  • org events --follow, org logs --follow and org watch exit when nobody is reading (#433). An idle follower only ever stopped on SIGINT/SIGTERM, so one orphaned by its parent (or whose reader had closed the pipe) polled a finished run's bus.jsonl twice a second forever. The follow loop now stops when the process is re-parented (its parent pid changes), when stdout errors (EPIPE) or closes, and on SIGHUP. It also exits 0 once the followed run has closed: runtime.json records the run as stopped or crashed, or the run's org-stopped event was seen and runtime.json belongs to a newer run; any remaining events are drained first. A live run is followed as before. Each tick now reads only the bytes appended since the last one instead of the whole file.

  • The README no longer says installing runs no download scripts (#428). The Trust & Security table said "No post-install scripts that download code" and called tree-sitter a native addon. A fresh npm install monomind@2.18.5 adds 362 packages and 1.3 GB of node_modules, and three install scripts download binaries: puppeteer (657 MB of Chrome into ~/.cache/puppeteer), onnxruntime-node (CUDA libraries from NuGet on Linux x64) and better-sqlite3 (a prebuilt addon). The table now gives those numbers, and doc/privacy.md has a new "Install-time downloads" section listing each install script, what it fetches, and the PUPPETEER_SKIP_DOWNLOAD=1 ONNXRUNTIME_NODE_INSTALL=skip setting that brings the install down to 981 MB with no Chrome.

  • The mastermind "Paperclip" admin skills say what they actually do (#422). mastermind-approval-detail read .monomind/orgs/<org>-approvals.json, which does not exist (the queue is .monomind/orgs/<org>/approvals.json), so it stopped with "No approvals file" on every real org. It now lists, shows, approves and denies through monomind org approvals, org approve and org deny, which reach a running org's daemon; the comment, revision and resubmit actions nothing backed are gone. mastermind-secrets, -environments, -plugin-manager, -invites, -invite-landing, -join-queue and -access write side files no runtime code reads. Each now says "bookkeeping only — not enforced by the Org Runtime" in its description and at the top of its body, and the claims of effects that never happen (secrets "consumed by agents", environments controlling where agents run, a disabled plugin not loading) are removed. 43 skills said they were "invoked by mastermind:X" for slash commands that do not exist; those lines now name only the skill's own /mastermind-X entry.

  • cleanup --force keeps the helpers a git-tracked settings file still runs (#448). Cleanup never edits a git-tracked file, so when a team committed .claude/settings.json but not .claude/helpers/, the #401 fix could not strip the hooks and cleanup deleted the helpers they run: every prompt and tool call then failed. Cleanup now keeps the whole helpers directory those hooks and the statusLine run (listed as "kept: referenced by tracked .claude/settings.json"), and both the preview and --force print the dangling commands with the manual edit that completes the uninstall: delete them from the settings file, commit, re-run cleanup --force.

  • doctor's Routing Learning check no longer reports accuracy from too little or one-sided data (#425). It printed "routing accuracy 100%" from 13 outcomes that were all marked successful. It now reports the sample size and the PICK follow rate (how often the agent used matched the recommendation), shows "insufficient data" as info (not pass) below 30 outcomes, and prints no accuracy when every outcome carries the same flag (warn). The routing-feedback.jsonl fallback follows the same rules.

  • monomind init runs its doctor pass once, after all its writes, and prints only problems (#425). The embedded doctor ran before init's last writes and printed each check, then its post-fix re-check, so one run said "No .gitignore found" and then "All … gitignored" (and "1 stale" then "match"). init now runs it at the end, applies the same fixes silently, prints only the warnings and failures left after them, then a one-line Health check: N passed, W warning(s), F failed summary.

  • monoswarm, autopilot and agent spawn say they start nothing (#418). monoswarm init recorded "status": "running" with an empty roster and monoswarm status showed a hard-coded 5% progress for a swarm with no tasks. The state file now records "initialized", status reads recorded (no agents started) and shows progress only from recorded task files (n/a when there are none), and monoswarm start shows a "Suggested Agent Roster (not started)" instead of an "Agent Deployment Plan". agent spawn prints "recorded in the agent store" and that no process was started (pointing at monomind agent exec); the agent_spawn MCP tool returns status: "registered" with the same note. monoswarm and autopilot are marked deprecated in help, autopilot status and the docs; autopilot status says when no task source has tasks, since nothing writes swarm-tasks.json. Removing both commands is left for a later release.

  • Grep/search [MONOGRAPH_HINT]s no longer point at test mocks (#447). The pre-bash and pre-search hint lookups in hook-handler.cjs took the first matching graph row, which was often a test (grep bridgeStoreEntry hinted …/__tests__/hooks-core-commands.test.ts:17), and since #409 those hints reach the model. Symbol hints now skip test files, using the same test-path heuristic as the monograph node resolver (#441), so a symbol matched only in tests gets no hint; file-name hints rank non-test files first.

  • The Bash destructive-ops gate no longer blocks commands that only mention a dangerous string (#427). The PreToolUse gate matched the raw command text, so grep -rn "rm -rf" src/, git commit -m "document git push --force risks", echo 'DROP TABLE' > notes.txt and heredoc documentation were blocked. The patterns now run per command segment (split on &&, ||, ;, |, & and newlines outside quotes). Quoted literals that contain whitespace, and heredoc bodies, are masked only in segments whose command is on a short allowlist of commands that do not execute their arguments (grep/egrep/fgrep/rg/ag/ack, echo, printf, cat, tee, head, tail, wc, jq, yq, touch, mkdir, ls, git commit/tag/notes/log/show/grep/diff, gh issue/pr/api/release), and only when the rest of the pipeline is allowlisted too and, if anything writes a file, the whole command is. Options that run a program (printf -v, rg --pre, ack --pager, git grep -O) disqualify a segment. Every other segment is scanned with the quote characters removed and the text kept, so npm exec -c '…', nodemon --exec "…" or any unknown runner still blocks. Assignment values (X="rm -rf ~"; $X) and quoted words without spaces ("rm" -rf ~) are never masked, and the whole raw command is scanned when a quote is unterminated, when there is a command substitution or a $X command word, or when a word names a known string executor (bash -c, eval, xargs, ssh, interpreters, database clients such as psql, git's -c/--exec/rebase/config). The monofence-ai layer on Bash now only warns (on stderr, logged to the security audit as warn) unless the command also pipes downloaded content into a shell (curl … | sh, bash <(curl …), sh -c "$(curl …)"), which still blocks.

  • The statusline renders in about 50 ms instead of about 220 ms (#429). Every render spawned npm config get prefix (for the version), git (branch/status, remote, graph freshness, common dir), sqlite3 (monograph and memory counts) and curl (dashboard probe). Those results are now cached per project in .monomind/cache/statusline.json (git and the dashboard probe for 5 s, DB-derived counts for 30 s, version, remote and git dir for 10 min), and an expired segment is recomputed by the render that finds it expired, so a warm render only reads files. The rows and segments shown are unchanged. This covers both .claude/helpers/statusline.cjs and the script init generates, which also drops its whole-output cache in ~/.monomind/statusline-cache.json: that cache was keyed only by mode, so another project's statusline could show for up to 5 s. The generated script also imports spawnSync again, so its memory count shows instead of silently failing.

  • The graph gate no longer blocks the first grep of a session; it adds a one-time reminder instead (#413). The PreToolUse Bash and Grep/Glob hooks blocked the first grep, rg or find of every session until a monograph tool was called. That cost a round trip, and in most blocked sessions the agent just ran grep again. The block also caught greps that were not code searches, such as … | grep. The search now goes through, and the first source-code search of a session carries a reminder to try monograph_query/monograph_suggest as PreToolUse additionalContext. The reminder appears once per session, only when the graph is fresh and non-empty, and never for piped greps or searches over dependencies, build output, logs, docs/data files or paths outside the project. The init fallback hook-handler behaves the same way. Security gates still block as before. Also fixed: the node count the reminder quotes closed the hook's shared graph handle, which made every later [MONOGRAPH_HINT] lookup in that call fail.

  • The per-prompt Second Brain lookup is cheaper and injects only strong matches; its background reindex uses the installed CLI on the docs only (#416). At a 0.35 floor with up to 3 excerpts, 2 of 27 injected excerpts were relevant (~180 tokens per prompt), and a hung dashboard server held every qualifying prompt for the full 900 ms timeout. The route hook now skips the POST when the pid in .monomind/control.json is not alive, waits at most 250 ms, raises the semantic floor to 0.65 (the keyword fallback stays at 0.5) and injects one excerpt, plus a runner-up only when it scores within 0.03 of the top; nothing is injected when no hit clears the floor. relevanceFloor and injectionLimit in .monomind/second-brain.json still override. The session-start reindex ran npx -y monomind@latest doc ingest . (a different version than installed, over 2,402 files including package READMEs and duplicates); it now runs the project's node_modules/.bin/monomind, a monomind on PATH or the monorepo's CLI entry, and skips when none exists, and it ingests only docs/, doc/ and top-level *.md (dot-directories such as .agents, .gemini, .monomind and .mail are never walked; identical content shares one set of chunks). Only changes inside that scope trigger it. monomind doc ingest now takes several paths and ingests them in turn.

  • agent test --json reports crush and pi sign-in failures as auth (#473). Without credentials, crush says "No providers configured - please run 'crush' to set up a provider interactively." and pi says "No API key found for the selected model. Use /login to log into a provider via OAuth or API key." Both came back as status:"error" (runner-error). They are now auth, and crush, pi and pi-rpc have a login_hint (also shown by agent scan --json). Unknown-model and quota errors are classified as before.

  • The dashboard server no longer duplicates itself, hangs on SIGTERM or grows to ~2 GB (#477). A second server.mjs for a project whose dashboard is already running (on the requested port, or on the port .monomind/control.json records, confirmed through /api/identity) now prints that dashboard's URL and exits 0; another project's server on the port is still skipped as before, and monomind ui reports the running dashboard instead of waiting. SIGTERM, SIGINT and SIGHUP now close every SSE stream (the mastermind and per-org streams were left open) and every socket, then exit 0, with a forced exit after 5 s. Sockets were never force-closed because the port-fallback loop removed Node's own listening listener, so the HTTP server tracked no connections and server.close() waited on open keep-alive and SSE sockets indefinitely, leaving a process with no port that ignored SIGTERM. The memory growth came from fs.watch({ recursive: true }), which Linux Node emulates with one inotify watch and one retained fs.Stats per file: the .monomind watcher alone held ~109,000 watches on one machine. Watchers now watch directories (capped at 2,000) instead of files: 100,000 files under .monomind now cost 206 watches and 106 MB RSS at start, down from 100,208 watches and 312 MB. SSE clients that stop reading are dropped once 1 MB is queued, the per-run digest cache is capped at 64 runs, and nothing (watchers, the document sweep) starts before the port is bound.

  • Each org role session gets its own TMPDIR (#480). Every role of an org run shared the daemon's TMPDIR (the release org uses $HOME/mrg-tmp), so a bare mktemp -d landed in that shared root as tmp.XXXXXXXXXX, and one role's rm -rf tmp.* there deleted other running roles' scratch too: 13 directories matched where 3 were meant on the 2.19.0 release run. Each role session (each task session with session_scope: "task") now runs with TMPDIR, TMP and TEMP set to its own <base>/<org>-<role>-XXXXXX/ (mode 0700), where <base> is the TMPDIR it had before, so the sandbox and file-tool roots still cover it. The runtime removes a task session's directory when its task closes, the rest when the role's session ends and at org stop, each by exact path and only with this org's and role's prefix; at start it sweeps only this org's directories whose owning process is gone. A TMPDIR that a role-specific env overlay sets explicitly still wins. Every role's prompt now says its $TMPDIR is private and that cleanup globs must never run in a shared directory.

  • monomind search finds documents doc ingest just indexed, without search scan (#479). Which content types search looks at comes from the directory fingerprint, and that is refreshed only by search scan or once it is a day old. A fingerprint taken before any document existed (as init writes one) kept the documents type off, so doc search found a freshly ingested file and search did not. The documents type was also off in a tree where documents are under 10% of the files, even right after a scan. Now, when the project's doc store has live documents, search turns the documents type on for that run. This covers everything that writes the project store: doc ingest, the MCP knowledge_ingest tool, and the session-start auto-ingest. There is no rescan and the saved fingerprint is not rewritten, so an unchanged store costs one read of doc-metadata.jsonl. search still reads documents from the working tree, so a file ingested from outside it, or into the global brain or a profile store, is found by doc search only. search --help now says what search scan is still needed for: other content types added since the last scan.

  • config get prints object and array values instead of [object Object] (#478). monomind config get memory printed memory = [object Object] because the value went straight into a template string. An object or array value is now printed as indented JSON after key = ; scalars print as before, and --format json was already correct. The full-config table (config get with no key) printed arrays with String(), so an array of objects showed [object Object]; arrays there are now compact JSON (["a","b"], []).

npm: https://www.npmjs.com/package/monomind/v/2.20.0 · https://www.npmjs.com/package/@monoes/monomindcli/v/2.20.0