cyberine
Providers

Muse Code (Meta) — headless muse exec

  • Kind: headless coding agent (subprocess)
  • Official docs: https://dev.meta.ai/ · muse --help, muse exec --help (the binary's own help is the authority used here)
  • Cyberine adapter: CliAgentAdapter via makeMuseAdapter (build worker muse)
  • Researched/verified: 2026-09-04 against Muse Code 1.0.2 (1.0.2-R2040.1), macOS aarch64 — confidence high for everything marked MEASURED; anything not so marked is read off the binary's help text and not exercised. Updated 2026-09-18 (kskdf): MCP client config (settings.json mcpServers schema) measured against muse-bin-1.3.0-R3233.1 via strings on the installed binary plus muse mcp --help/muse config --help — see "MCP servers" below.

Every claim below was checked against the installed binary or a real dispatch. Where a measurement contradicted the expectation, the measurement is what is recorded and the expectation is named so a later reader can see which way the surprise went.

Cyberine invocation

muse exec --json --workspace <task.cwd> --session-id <minted uuid> \
  --trust-workspace --approval-mode never --approval-judge off \
  --no-foreign-personal-context --worktree off --user-input-auto-resolve \
  --model <id> --reasoning-effort <effort> [--max-model-steps <n>] \
  [--disable-sandbox] [--image <path> ...] "<prompt>"

Built by museBuildArgs (exported so selftest:muse-trust-flags can assert the whole contract without a spawn). Config/data dirs allow-listed by the sandbox: ~/.config/muse and ~/.local/share/muse.

Why each flag is there

Four of these are load-bearing — dropping one produces no error, just a silently wrong run.

FlagDrop it and…
--approval-mode never + --approval-judge offthe worker STALLS to its timeout. The adapter closes the child's stdin unconditionally and Muse's default on-request approval blocks on stdin.
--trust-workspacethe worker silently stops following the project's rules. An untrusted workspace loads neither its rules nor its skills, with no error and nothing in the run log — and every isolate: true worktree path carries a fresh run id, so it is never in the operator's trust.json, on every dispatch. Scoped to the one run; Cyberine never writes trust.json (that file is write-denied, see Security). Same shape as makeGeminiAdapter's per-run --skip-trust.
--worktree offthe muse candidate loses every race, silently. The engine owns the worktree; captureDiff stages and diffs the tree Cyberine provisioned, so a Muse-created nested worktree makes it return empty, and DiffJudge requires a non-empty raw.diff to consider a candidate at all.
--session-id <uuid>token usage becomes unreadable — it is the only deterministic handle on the durable log (see Usage).

--no-foreign-personal-context is for correctness, not cost. By default a run ingests the operator's ~/.claude/CLAUDE.md and ~80 of their Claude/.agents skills — instructions written for a different agent, which is a good way to get confidently off-brief work. MEASURED, the token cost of that inheritance is marginal: ~580–660 tokens on a ~32,000 input floor, about 2%. A figure of 26,791 circulates in earlier research; it was measured over MSP (muse serve), a different code path, and does not describe muse exec.

--yolo is never passed. It sets approval, sandbox and trust in one flag, and the three axes must move independently — a single collapsed flag makes the untrusted posture below inexpressible. selftest:muse-trust-flags asserts its absence in all three postures.

Auth

Two mechanisms, and the CLI's own help states the precedence: "META_API_KEY always takes priority over the account login."

  1. Account login — muse login (OAuth device code; approve a code in the browser). The token lands in the macOS Keychain, not on disk.
  2. API key — META_API_KEY in the environment. Also muse auth set --api-key-stdin (stdin only, never an argv, so it stays out of shell history — and therefore unusable from a headless adapter with stdin closed; Cyberine passes the env var instead).

~/.config/muse/auth.json holds no credential. MEASURED, it is a login-STATE record:

{ "schema_version": 2,
  "providers": { "meta": { "mechanism": "oauth", "storage": "keychain",
                           "obtained_via": "device_code",
                           "api_base_url": "https://api.meta.ai/v1",
                           "user_full_name": "…", "user_email": "…" } } }

storage: "keychain" names where the real token lives; a scan for any key containing token/key/secret returns empty. That is why checkMuse() may read this file at all — and why it builds its reason string from the fixed enum fields only: user_full_name and user_email sit in the same object and are PII that must not reach a status line.

MUSE_AUTH_PATH does not exist. It appears zero times in the binary; XDG_CONFIG_HOME (51 occurrences) and XDG_DATA_HOME (39) are the only roots, which is why cliConfigDirsFor('muse') reads them.

MODEL_API_KEY is a different product's credential — the raw Meta Model API at api.meta.ai/v1, used by the (not-yet-built) loop worker. It appears nowhere in the CLI binary. Do not bridge the two: lifting the CLI's Keychain OAuth token as a bearer against the HTTP API is a forbidden pattern in this repo (tombstone ksmpy, alongside the Anthropic/OpenAI/Google equivalents).

Models

Every catalog row is identical on context, output and cost, so the model choice is a generation and a data policy, never a capability tier.

Model idNotes
muse-spark-1.3Cyberine default (is_current). 1,007,997 in / 128,000 out.
muse-spark-1.3-contributorSame model, CONTRIBUTOR terms. The catalog's own is_default.
muse-spark-1.2Previous generation.
muse-spark-1.2-contributorPrevious generation, CONTRIBUTOR terms.

cost is null on every row: flat-rate against the CLI subscription, $0 marginal per run. Recorded verbatim rather than translated into a number.

Selection is on is_current, deliberately NOT is_default — is_default is true on a -contributor row, whose terms let Meta use the run's content (including inter-session messages) for product improvement. Inheriting the catalog default would opt the operator in silently. makeMuseAdapter warns if a -contributor id is pinned on purpose.

Meta's public docs list a fifth id, muse-spark-1.1, absent from this machine's catalog; forcing it returns API error 429 … Service temporarily unavailable. Worth distinguishing from the typo error: "does not exist or you lack access" means typo or no entitlement, "Service temporarily unavailable" means entitled but not served.

--model validation is NON-DETERMINISTIC — the local gate is mandatory

Two independent sessions measured the same bogus id on the same machine with the same credential, hours apart, and got opposite results: 20 of 21 runs answered normally with exit 0 (echoing the typo back in run.model.configured), while another 6 of 7 failed with model `no-such-model-xyz` does not exist or you lack access. --yolo, --json and --session-id were each tested as the trigger and all three ruled out; the failures carry a request_id, so the split is server-side.

The conclusion is stronger than either "it validates" or "it doesn't": it cannot be relied on either way. So makeMuseAdapter validates against the LOCAL catalog in its setup hook — ~10ms, fails the dispatch before spawning — and treats the API error only as a fallback. A preflight that leaned on the API's rejection would pass roughly 20 times in 21 and let a run answer on a model nobody asked for.

The catalog is at <data root>/model-catalog/<hex>__p<hex>.json, one file per provider+profile. Glob the directory; never construct the filename — it is hex-encoded (6d657461 = "meta", 746268 = the "tbh" profile).

Reasoning effort — pinned, not inherited

--reasoning-effort none|minimal|low|medium|high|xhigh|ultra, default high per the binary's own help.

Cyberine passes high explicitly. Omitting the flag makes muse fall back to reasoning_effort in ~/.config/muse/settings.json — a TUI preference this repo does not control (it reads xhigh on this machine), so inheriting it would silently put every fleet dispatch on a slower tier because of a setting nobody revisited. CYBERINE_MUSE_REASONING_EFFORT=inherit opts back in deliberately.

Effort costs nothing against the subscription, which meters prompts: a minimal job and an xhigh job each spend one. The only thing a higher tier buys is wall-clock time. And there is no measured correctness benefit — across ten runs on two executably-verifiable tasks, every level from minimal to ultra got the right answer, so escalation here is reasoning, not measurement. ultra is not simply "more than xhigh": it clamps to xhigh on the wire while enabling proactive multi-agent workflow guidance, so treat it as a behaviour change and never a default.

Output parsing

muse exec --json emits NDJSON. The envelope is uniform and shares no field with the other CLIs — the discriminator is payload_type and there is no type, tool or tool_name anywhere on it:

{ "schema_version": 1, "id": "…", "stream": {"kind":"session","id":"<session-id>"},
  "sequence": 3, "recorded_at": "…", "record_type": "…", "durability": "ephemeral",
  "causation_id": "…", "payload_type": "run.output.delta",
  "payload_schema_version": 1, "payload": { "kind": "run_output_delta", "text": "Read [" } }

19 payload types were observed across every capture. The ones parseMuseStdout acts on:

payload_typeCarries
run.terminal.completed / run.terminal.failedpayload.terminal, payload.text, payload.reason — the final answer
run.output.deltapayload.text — a streaming text delta
tool.resultpayload.correlation_facts.tool_name + .outcome
session.workspace_branch.observedpayload.record.reference.name, .commit, .vcs, .dirty

stream.id is the run's session id, on every line — which is how the usage reader finds the log without threading state from the argv builder through a mutable field that two concurrent dispatches could swap.

Four parse rules, each a real defect

RuleWhy
R1 Match the run.terminal. PREFIX, then read payload.terminala failure arrives as a different payload_type with text: "". Code waiting for .completed turns every failure into a silent empty success.
R2 Empty terminal text is a LEGAL successmeasured: a --no-foreign-personal-context run returned completed with text: "".
R3 Drain to EOF, never stop at the terminalsession.workspace_branch.observed fires twice — early with dirty absent from the record entirely, and again after the terminal with it resolved. Stopping early keeps the branch name and leaves dirty unusable, and dirty is what says whether a diff exists.
R4 An unknown payload_type is skippable, not exceptionalthe exec envelope is a raw-log layer Muse's own published MSP schema marks reserved/deferred, so the observed type list is not closed.

A failing shell command inside the run still yields terminal: "completed" on the run itself — the bash tool's own exit code is nested inside its tool.result.text as JSON. Don't conflate the two.

Usage — not on stdout

Grepping a full exec --json stream for any *token* key returns zero hits. Usage lives in the durable session log the run's --session-id pins:

<data root>/sessions/<YYYY>/<MM>/<DD>/<session-id>/session.jsonl

readMuseUsage() reads it at payload_type == "runtime.session" → payload.event.kind == "goal_usage_attribution" → payload.event.record.quantity:

{ "unit": "tokens", "reported": true, "input_tokens": 33804, "output_tokens": 482,
  "cached_tokens": 0, "reasoning_tokens": 390, "main_llm_steps": 1 }

Two ways this returns a silent wrong answer, both guarded:

  • THE HOP. payload.event has no quantity key; payload.event.record does. Skipping .record does not throw — every field reads undefined, so a summer returns clean zeros while still counting the right number of records. Reproduced: a session whose true total was 539,080 input tokens across 22 records returned all zeros with records: 22.
  • reported: false. MEASURED on a real fleet dispatch, a 3-record session carried one record with reported: false and every counter zero. So unreported records are counted, never summed, and reportedRecords === 0 is the signal for "the provider gave us nothing" rather than "the answer is zero".

Either way a failed read yields usage: undefined plus a printed reason — never a confident zero.

record.owner.requester_kind separates main from subagent usage, so per-subagent accounting is possible later.

Two traps in the same log: resource_usage_sampled events hold rss/cpu/fds under a key literally named usage (filter on event.kind), and some lines have no payload_type at all — retained_frame wrappers whose real records are nested as JSON strings in children[].record_json. Across all 146 session logs on this machine there were 390 such lines, carrying only runtime.session.permission_* records and zero usage records, so skipping them loses nothing on 1.0.2 — but they are counted, because that is an observation and not a contract.

muse export --session <id> --out <file> is the offline alternative. Note session_terminated_abnormally: true is normal for every exec run (exec exits without a clean session end) and must not be surfaced as an error.

MCP servers (client config, added kskdf, measured 2026-09-18)

Muse Code is also an MCP client. There is no public Meta doc page for it (Official docs above is dev.meta.ai, which does not cover MCP client config), so this section is MEASURED against the installed muse-bin-1.3.0-R3233.1 binary rather than sourced from a vendor page — same methodology this file uses throughout. muse mcp --help states directly: "<server> is a streamable-HTTP entry under mcpServers in settings.json" — confirming the top-level key is camelCase mcpServers, not mcp_servers. strings on the installed binary confirms the full per-server shape (validation error text quoted verbatim): enabled (bool), mode (McpServerModeSetting: required default | optional), transport (stdio | streamableHttp), and for stdio: command (required — "MCP transport stdio requires field command"), args, env, framing (contentLength | lineDelimitedJson); for streamableHttp: url (required — "MCP transport streamableHttp requires field url"), headers. The file is ~/.config/muse/settings.json (or $XDG_CONFIG_HOME/muse/settings.json) and must carry a top-level "schema_version": 1 or every muse command fails at startup with a malformed-settings error (muse config --help).

Contradicts third-party sources (a blog post and a GitHub PR both describing a snake_case mcp_servers key with streamable_http, "schema_version": 1 required) — those were checked first via WebFetch, then verified wrong against the installed binary's own validation strings and muse mcp --help's own wording; the binary is trusted here (.claude/rules/development.md: source over docs). mcp login/mcp logout only manage OAuth for a streamableHttp entry — there is no muse mcp add or muse mcp list, so a Muse MCP registration cannot be listed or added via CLI; manual settings.json editing (or a tool that does the same) is the only path. Unverified: an actual end-to-end Muse dispatch calling a tool through a registered MCP server — this section covers the settings schema only, not a live exercised call.

cyberine mcp install --target muse writes a stdio entry (transport: "stdio", command, args) with mode: "optional" set deliberately — required (the binary's default) would abort the entire Muse run if the cyberine MCP server fails to start, which is worse than the tool simply being unavailable for that turn. It merges into any existing settings.json (preserving every other key, including a pre-existing schema_version; a fresh file gets "schema_version": 1 added), is idempotent (a mcpServers.cyberine key already present is a no-op), and backs the file up once to settings.json.bak-cyberine before its first write. --print shows the entry without touching the file.

Security posture — three independent axes

Approval, trust and sandbox are separate concerns, which is the whole reason --yolo is banned here.

Sandbox. --disable-sandbox is passed when the task is trusted, so hostSandbox is the single confinement layer — the one Cyberine can canonicalise paths for and already has smokes against. For CYBERINE_TASK_TRUSTED=false the flag is dropped and Muse's own sandbox runs inside Seatbelt as defence in depth.

That nested case was MEASURED rather than assumed, and the result contradicted the prediction: it works. A real dispatch on macOS with Seatbelt and CYBERINE_TASK_TRUSTED=false completed normally — non-empty captureDiff, DiffJudge 2/2 — in 10.5s against 9.0s for the same task single-layer, i.e. ~1.5s of nesting overhead and no failure. The pessimism came from two prior scars where an inner process died inside an outer profile with an errno that never said "sandbox" (opencode2's EPERM … posix_spawn on a denied /dev/null write; T640's AlreadyExists: FileSystem.makeDirectory). Muse hits neither. Scoped honestly: an observation on 1.0.2 with Seatbelt — the nested case on bubblewrap or a container backend is unmeasured.

Muse's own confinement, when enabled: filesystem writes outside the workspace fail ($HOME writes are refused, ~/.config/muse/settings.json reads are denied) — with one hole, /tmp writes succeed. Network: only restricted restricts. Measured curl https://example.com from the agent's shell — restricted → could not resolve host; proxy-only (the default) → HTTP 200; enabled → HTTP 200.

Config-dir protection. Adding ~/.config/muse to the sandbox's writable set opens two escapes, both closed in the same change that opened them:

  • trust.json ({schema_version, projects: {<abs path>: {decision}}}) is in PROTECTED_CONFIG_BASENAMES. Without that, a worker could mark arbitrary host paths trusted for the operator's next interactive muse session — structurally the same escape T702(a) closed for ~/.claude/settings.json.
  • .git is in PROTECTED_CONFIG_SUBDIRS (kz1yc). ~/.config/muse is a git repo on this machine, and a repo's .git/hooks/ is executed by the operator's own git commit, outside any sandbox. The whole subtree is denied because core.hooksPath in .git/config would re-open it from a different file.

auth.json is deliberately not denied: codex, opencode and cursor-agent all keep live refreshable OAuth state in a file of that name, the table is applied uniformly to every config dir, and denying it would break token refresh for three shipping workers. selftest:muse-auth-gate asserts that it stays writable so a well-meaning future edit cannot add it.

--permission-profile exists but no profiles do on this machine — they come from enterprise config planes that are all absent, and an unknown id hard-fails.

Modalities

ModalityVerdict
Text in / text out1,007,997 context, 128,000 output
Image IN (vision)Yes, --image <PATH>, repeatable. Accepts image/png, image/jpeg, image/gif, image/webp. ~245 input tokens per image on top of the ~32,000 floor. Forwarded by museImageArgs; the profile sets multimodal: true.
Image OUTNo native generation. It produces images by writing code (probing for PIL, then running a script through its bash tool) — good for charts, diagrams and geometric figures, useless for photographic or creative imagery, and dependent on what the sandbox has installed.
Audio in/outNo. No CLI flag and no audio/* MIME anywhere in the binary.
Video inNot from exec. The binary validates video/mp4 / video/quicktime but no exec flag reaches it.

Tool surface

27 tools, asked in-session: read_file, search, write_file, edit_file, bash, bash_input, web_fetch, web_search, read_memory, add_memory, edit_memory, cron_create, cron_delete, cron_list, get_goal, create_goal, update_goal, report_progress, subagent_spawn, subagent_status, subagent_send_message, subagent_wait, subagent_read_result, subagent_cancel, read_skill, write_todos, muse.workflow.

Four families have no Claude Code CLI counterpart: subagents, cron (persisted in a per-session cron.db), durable memory, and goals with progress reporting. None of them is reachable through Cyberine's one-shot exec path today beyond whatever the model chooses to use within a single run.

Rules and skills

Canary-tested, per file:

FileLoaded?
AGENTS.md at workspace rootyes — the canonical project rules file
CLAUDE.md at root, with AGENTS.md also presentno (stderr warning only)
CLAUDE.md at root, no AGENTS.mdyes — the fallback
~/.claude/CLAUDE.md (operator's global)yes by default — which is what --no-foreign-personal-context turns off
.muse/rules/, .claude/rules/, .agents/rules/no — there is no rules-directory concept
MUSE.md, nested sub/AGENTS.mdno

Trust gates all of it. This repo's .claude/rules/* therefore does not reach a muse worker; only a root AGENTS.md would.

Not wrapped

  • muse resume [--last | <uuid>], muse trace inspect, muse session-message, muse skills, muse sandbox, muse schema.
  • muse plugins … — gated off in this build (feature-config sets "plugins": false); MUSE_EXPERIMENTAL_PLUGINS=1 reveals the surface, runtime behaviour is UNVERIFIED, and plugins install from a git marketplace — weigh that supply chain before recommending it.
  • muse serve — the MSP JSON-RPC surface. Evaluated and deliberately deferred; the reasoning and the reviving conditions are the next section.

muse serve / MSP — evaluated and DEFERRED (krjy7, 2026-09-04)

muse serve hosts the Muse Session Protocol over stdio: 31 methods, 22 notifications, and a schema the binary exports itself (muse schema), so the wire contract needs no reverse-engineering. It is a genuinely richer surface than exec, and the decision below is a deferral on cost and fit rather than a dismissal — recorded here because an unrecorded deferral would waste the prototype (.local/research/musecode/proto/msp-client.mjs, which drives a full turn end to end) and the schema work behind it.

What MSP uniquely offers

  • Live token usage. session/tokenUsage fires per model completion with the provider's raw counters plus a server-derived cumulative block, and turn/completed.usage aggregates the turn. Item.usage on a subagent item carries transitive child usage, explicitly never folded into the parent's total.
  • Live context pressure, on the same channel.
  • A subagent control plane: 8 of the 31 methods (sendMessage, followupTask, interrupt, stop, close, resume, reopen, readResult), with a child's transcript readable through ordinary session/read — "child transcript drill-down without a second protocol".
  • An approval control plane (approval/requested -> approval/decide -> approval/resolved) with a stale-requirement race guard, and session/fork for durable history branching.

Why it does not earn its complexity here yet

  1. Worktree isolation has NO representation in the wire schema — and that is what this fleet is built around. There is no Worktree type and no worktree field on Session, SessionConfig (explicitly empty in v1), SessionStartParams, or any item kind. The closest wire artifact, session/branchChanged / BranchState, is a plain branch observation that says nothing about which worktree is in use or whether isolation succeeded. Cyberine's implementer contract is isolate: true -> provisionWorktree -> captureDiff -> DiffJudge, all keyed on the tree the ENGINE provisioned. An MSP adapter would still have to provision that worktree itself and point the session's workspace root at it — which is exactly what the exec adapter already does with --workspace. So on the one axis the fleet actually depends on, MSP buys nothing.
  2. This repo has no persistent-process adapter shape. All five adapter classes (CliAgentAdapter, LoopAgentAdapter, AnthropicAgentAdapter, BedrockAdapter, VertexAdapter) are dispatch-scoped; both spawn() sites in CliAgentAdapter live inside a run method, and even sessionResume — the warm-session feature — is a cold spawn with a resume flag, not a held process. A long-lived stdio JSON-RPC peer needs lifecycle ownership, reconnect, backpressure, a new shape of the T702(c) grandchild-reap problem, and a different sandbox posture (hostSandbox wraps a spawn; keeping a process inside Seatbelt for a whole session is not the same contract). That is a transport to maintain, not a flag to add.
  3. The safety posture cannot be pinned on the wire without guessing. The CLI's --approval-mode values (untrusted | on-request | never) are a DIFFERENT enumeration from the wire's ApprovalMode (allowAll | promptUnmatched | onRequest | denyUnmatched), which the schema describes as "select, never create" — a client names a mode the host's own configuration already defines and cannot construct a policy inline. No document in the research bundle states the mapping, so reading CLI never as wire allowAll is plausible by name and unconfirmed in schema. The exec adapter pins its posture with three explicit, verified flags; trading that for an inferred mapping on a security-relevant axis is not an acceptable exchange.
  4. What MSP is best at is not what a one-shot implementer needs. Interrupts, live steering, followup tasks and subagent control are multi-turn, interactive capabilities. The fleet's CLI worker is dispatch-shaped: take a task, leave a diff, be judged. Fan-out and judging are the engine's job already.
  5. The usage gap it would close is already closed. readMuseUsage reads the durable session log post-hoc and was verified on a real dispatch (68,156 input / 529 output over 2 reported records). Live usage is nicer, but nothing in the fleet consumes usage mid-run — the daily cap is admission-only — so it would be a better number arriving at a time no one reads it.

Reviving conditions (concrete, so this is re-decidable rather than re-argued)

Revive when the FIRST of these becomes true, AND the third holds:

  1. Cyberine grows a genuinely multi-turn worker shape — one where interrupt/steer/followup matter — rather than the current dispatch-and-judge shape; or
  2. spend enforcement moves from admission-time estimation to mid-run metering, at which point live session/tokenUsage stops being a nicety; or
  3. Muse's own subagents become something the FLEET wants to control (spawn, message, harvest individually) rather than something the worker manages internally within one run.

And in every case: the CLI-flag-to-wire ApprovalMode mapping must be documented upstream first, because otherwise point 3 above is unresolved and the posture would rest on an inference.

Nothing was thrown away: the prototype client, the exported schema (.local/research/musecode/schema/, whose stable and experimental exports are byte-identical on v1.0.2 — same SHA256 on both the JSON schema and the .d.ts, so there is nothing to gain by targeting the experimental surface) and the 29-row error table are all in the research bundle, and the handshake's two traps are recorded there: clientInfo.name must match ^[a-z0-9_]+$ (a hyphen is rejected outright) and the initialized notification is mandatory after the initialize result or every later request fails -32600 notInitialized.

Usage limits (ktwg5, measured 2026-09-23 against the session logs on this machine)

Proactive: NO. Muse publishes no used-percent or reset time anywhere measured: not on the exec --json stream, and not in the durable session logs. Across every session.jsonl under the data root, the only limit records are status events whose details.facets[] carry error_kind: "tier_limit" (4) or "rate_limited" (107) with http_status: 429 and a retry schedule ("retrying meta model stream in 5000ms (attempt 2/10)"); none names when the tier resets. So Cyberine cannot read muse headroom before dispatch, and a muse limit refusal gets the generic cooldown TTL. Revisit if a later muse release adds a reset field.

Gotchas

  • muse on PATH is not the agent. It is a 33KB bash launcher that checks for updates hourly in the background and then execs the real binary. Set MUSE_NO_AUTO_UPDATE=1 so a job cannot race a binary swap mid-flight.
  • Unknown-flag stderr is bare. Muse prints unknown option --x with no error: prefix and no quotes, then a usage line, exit 2 — matching none of the four patterns this repo had, so a fifth was added. Without it the flag diagnosis degrades to a bare exit-code message.
  • Feature gates are server-pushed with a 3600s TTL, so capabilities can change with no binary upgrade. Do not hardcode capability assumptions.
  • --base-url is inert under account auth: pointed at a dead host, a run still completed.
  • --preset miniswe fails on 1.0.2 (run.terminal.failed, reason unknown registered tool: web_search). native-basic works.
  • Session history looks empty after a login-method switch — Muse partitions history by login method and shows only the group matching the current config. Nothing was lost.