From 8c72aac2de310e4e58b4b90e386b0a1514934fc6 Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Thu, 6 Aug 2026 17:09:42 -0700 Subject: [PATCH 1/6] feat(detect): add muse agent with generic Pick blocked detection Add Muse agent manifests and runtime detection for idle/working/blocked states. Blocked Pick is generic on footer 'Enter to select' so any question title/message triggers, fixing missed waiting answer where previous AND-gated rule required specific hint combos. Cover process names muse, muse-bin, muse-code variants and YOLO footer. refs #herdr --- src/config/sound.rs | 3 ++ src/detect/manifest.rs | 1 + src/detect/manifests/muse.toml | 80 ++++++++++++++++++++++++++++++ src/detect/mod.rs | 12 ++++- website/agent-detection/index.toml | 4 ++ website/agent-detection/muse.toml | 80 ++++++++++++++++++++++++++++++ 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/detect/manifests/muse.toml create mode 100644 website/agent-detection/muse.toml diff --git a/src/config/sound.rs b/src/config/sound.rs index 217737d85..952e30f9b 100644 --- a/src/config/sound.rs +++ b/src/config/sound.rs @@ -44,6 +44,7 @@ pub struct AgentSoundOverrides { pub kilo: AgentSoundSetting, pub qodercli: AgentSoundSetting, pub maki: AgentSoundSetting, + pub muse: AgentSoundSetting, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] @@ -140,6 +141,7 @@ impl AgentSoundOverrides { Some(Agent::Kilo) => self.kilo, Some(Agent::Qodercli) => self.qodercli, Some(Agent::Maki) => self.maki, + Some(Agent::Muse) => self.muse, None => AgentSoundSetting::Default, } } @@ -179,6 +181,7 @@ impl Default for AgentSoundOverrides { kilo: AgentSoundSetting::Default, qodercli: AgentSoundSetting::Default, maki: AgentSoundSetting::Default, + muse: AgentSoundSetting::Default, } } } diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index ed3de5078..be912d006 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -252,6 +252,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("kimi", include_str!("manifests/kimi.toml")), ("kiro", include_str!("manifests/kiro.toml")), ("maki", include_str!("manifests/maki.toml")), + ("muse", include_str!("manifests/muse.toml")), ("opencode", include_str!("manifests/opencode.toml")), ("pi", include_str!("manifests/pi.toml")), ("qodercli", include_str!("manifests/qodercli.toml")), diff --git a/src/detect/manifests/muse.toml b/src/detect/manifests/muse.toml new file mode 100644 index 000000000..40bbd7839 --- /dev/null +++ b/src/detect/manifests/muse.toml @@ -0,0 +1,80 @@ +id = "muse" +version = "2026.08.06.1" +min_engine_version = 2 +updated_at = "2026-08-06T00:00:00Z" +aliases = ["muse-code", "muse-cli"] + +# Evidence: Live pane reads via `herdr pane read --source detection --format text` +# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr. +# +# Idle (prompt ready) bottom chrome: +# ── Voice input (⌥ + v to start) ───── ... ─ +# ⟩ +# ───────────────────────────────────── ... ─ +# muse-spark-1.2-contributor · xhigh · ~/Developer/herdr · YOLO +# The prompt is a single `⟩` (U+27E9) on its own line. The status footer always +# contains `·` separators and the model path `~/Developer/herdr` (cwd) plus +# `YOLO` when launched with --yolo. +# +# Working (agent running tools/commands): +# ◆ Running command · (0s · esc to interrupt) +# └ (ctrl+b to send to background) +# ── Voice input (⌥ + v to start) ───── +# ⟩ ... +# Any live activity line contains `esc to interrupt`. This is the invariant +# between idle and working – idle footer is `YOLO`/`xhigh` without `esc`. +# +# Blocked (approval) – Muse shows tool approval when not in --yolo. The +# footer gains acceptance hints. We encode generic blocked signals so --yolo +# idle is not misclassified. These patterns were not observed in the initial +# capture and are kept lower priority / generic. +# + +[[rules]] +id = "pick_request_blocked" +state = "blocked" +priority = 950 +region = "bottom_non_empty_lines(5)" +visible_blocker = true +contains = ["Enter to select"] + +[[rules]] +id = "working_esc_interrupt" +state = "working" +priority = 900 +region = "whole_recent" +visible_working = true +contains = ["esc to interrupt"] +not = [{ contains = ["Enter to select"] }] + +[[rules]] +id = "blocked_approval" +state = "blocked" +priority = 850 +region = "whole_recent" +visible_blocker = true +any = [ + { contains = ["Do you want to proceed?"] }, + { contains = ["Allow", "Always allow"] }, + { contains = ["waiting for approval", "run this command?"], any = [{ contains = ["proceed"] }] }, + { contains = ["(y) (enter)"] }, + { contains = ["do you want to allow"] }, +] + +[[rules]] +id = "idle_prompt" +state = "idle" +priority = 700 +region = "bottom_non_empty_lines(5)" +visible_idle = true +line_regex = ['^\s*⟩'] +not = [{ contains = ["esc to interrupt"] }] + +[[rules]] +id = "idle_status_fallback" +state = "idle" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_idle = true +contains = ["YOLO"] +not = [{ contains = ["esc to interrupt"] }] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index b9f9655f5..079137967 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -62,10 +62,11 @@ pub enum Agent { Kilo, Qodercli, Maki, + Muse, } impl Agent { - pub const ALL: [Self; 21] = [ + pub const ALL: [Self; 22] = [ Self::Pi, Self::Claude, Self::Codex, @@ -87,9 +88,10 @@ impl Agent { Self::Kilo, Self::Qodercli, Self::Maki, + Self::Muse, ]; - pub const SCREEN_MANIFEST_AGENTS: [Self; 19] = [ + pub const SCREEN_MANIFEST_AGENTS: [Self; 20] = [ Self::Pi, Self::Claude, Self::Codex, @@ -109,6 +111,7 @@ impl Agent { Self::Kilo, Self::Qodercli, Self::Maki, + Self::Muse, ]; } @@ -135,6 +138,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", Agent::Maki => "maki", + Agent::Muse => "muse", } } @@ -167,6 +171,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str { Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", Agent::Maki => "maki", + Agent::Muse => "muse", } } @@ -203,6 +208,8 @@ fn lookup_agent(name: &str) -> Option { "kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo), "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), "maki" => Some(Agent::Maki), + "muse" | "muse-code" | "muse-cli" => Some(Agent::Muse), + _ if name.starts_with("muse") => Some(Agent::Muse), _ => None, } } @@ -778,6 +785,7 @@ mod tests { (Agent::Kilo, "kilo"), (Agent::Qodercli, "qodercli"), (Agent::Maki, "maki"), + (Agent::Muse, "muse"), ]; assert_eq!(expected.len(), Agent::ALL.len()); for (agent, executable) in expected { diff --git a/website/agent-detection/index.toml b/website/agent-detection/index.toml index fba996ea3..06046b56b 100644 --- a/website/agent-detection/index.toml +++ b/website/agent-detection/index.toml @@ -60,6 +60,10 @@ path = "kiro.toml" id = "maki" path = "maki.toml" +[[agents]] +id = "muse" +path = "muse.toml" + [[agents]] id = "opencode" path = "opencode.toml" diff --git a/website/agent-detection/muse.toml b/website/agent-detection/muse.toml new file mode 100644 index 000000000..40bbd7839 --- /dev/null +++ b/website/agent-detection/muse.toml @@ -0,0 +1,80 @@ +id = "muse" +version = "2026.08.06.1" +min_engine_version = 2 +updated_at = "2026-08-06T00:00:00Z" +aliases = ["muse-code", "muse-cli"] + +# Evidence: Live pane reads via `herdr pane read --source detection --format text` +# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr. +# +# Idle (prompt ready) bottom chrome: +# ── Voice input (⌥ + v to start) ───── ... ─ +# ⟩ +# ───────────────────────────────────── ... ─ +# muse-spark-1.2-contributor · xhigh · ~/Developer/herdr · YOLO +# The prompt is a single `⟩` (U+27E9) on its own line. The status footer always +# contains `·` separators and the model path `~/Developer/herdr` (cwd) plus +# `YOLO` when launched with --yolo. +# +# Working (agent running tools/commands): +# ◆ Running command · (0s · esc to interrupt) +# └ (ctrl+b to send to background) +# ── Voice input (⌥ + v to start) ───── +# ⟩ ... +# Any live activity line contains `esc to interrupt`. This is the invariant +# between idle and working – idle footer is `YOLO`/`xhigh` without `esc`. +# +# Blocked (approval) – Muse shows tool approval when not in --yolo. The +# footer gains acceptance hints. We encode generic blocked signals so --yolo +# idle is not misclassified. These patterns were not observed in the initial +# capture and are kept lower priority / generic. +# + +[[rules]] +id = "pick_request_blocked" +state = "blocked" +priority = 950 +region = "bottom_non_empty_lines(5)" +visible_blocker = true +contains = ["Enter to select"] + +[[rules]] +id = "working_esc_interrupt" +state = "working" +priority = 900 +region = "whole_recent" +visible_working = true +contains = ["esc to interrupt"] +not = [{ contains = ["Enter to select"] }] + +[[rules]] +id = "blocked_approval" +state = "blocked" +priority = 850 +region = "whole_recent" +visible_blocker = true +any = [ + { contains = ["Do you want to proceed?"] }, + { contains = ["Allow", "Always allow"] }, + { contains = ["waiting for approval", "run this command?"], any = [{ contains = ["proceed"] }] }, + { contains = ["(y) (enter)"] }, + { contains = ["do you want to allow"] }, +] + +[[rules]] +id = "idle_prompt" +state = "idle" +priority = 700 +region = "bottom_non_empty_lines(5)" +visible_idle = true +line_regex = ['^\s*⟩'] +not = [{ contains = ["esc to interrupt"] }] + +[[rules]] +id = "idle_status_fallback" +state = "idle" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_idle = true +contains = ["YOLO"] +not = [{ contains = ["esc to interrupt"] }] From a2280d06925a4bed1f0857841d1e0123f0f19292 Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Sun, 9 Aug 2026 00:02:04 -0700 Subject: [PATCH 2/6] fix(detect): tighten muse manifest and agent lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manifests: change working_esc_interrupt and blocked_approval from whole_recent to bottom_non_empty_lines(8) to use captured bottom-buffer controls instead of incidental whole-pane text - manifests: anchor idle_prompt line_regex to ^\s*⟩\s*$ so working chrome "⟩ ..." does not match idle - runtime: restrict muse lookup to explicit aliases (muse|muse-bin|muse-code|muse-cli), remove starts_with("muse") fallback that misclassified museum/muse-helper/muser; add positive/negative identification tests - keep website mirror identical; verified with agent_detection manifest check --require-website refs #2489 refs #2456 --- src/detect/manifests/muse.toml | 6 +++--- src/detect/mod.rs | 10 ++++++++-- website/agent-detection/muse.toml | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/detect/manifests/muse.toml b/src/detect/manifests/muse.toml index 40bbd7839..b50513bec 100644 --- a/src/detect/manifests/muse.toml +++ b/src/detect/manifests/muse.toml @@ -42,7 +42,7 @@ contains = ["Enter to select"] id = "working_esc_interrupt" state = "working" priority = 900 -region = "whole_recent" +region = "bottom_non_empty_lines(8)" visible_working = true contains = ["esc to interrupt"] not = [{ contains = ["Enter to select"] }] @@ -51,7 +51,7 @@ not = [{ contains = ["Enter to select"] }] id = "blocked_approval" state = "blocked" priority = 850 -region = "whole_recent" +region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ { contains = ["Do you want to proceed?"] }, @@ -67,7 +67,7 @@ state = "idle" priority = 700 region = "bottom_non_empty_lines(5)" visible_idle = true -line_regex = ['^\s*⟩'] +line_regex = ['^\s*⟩\s*$'] not = [{ contains = ["esc to interrupt"] }] [[rules]] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 079137967..96d7dfaf0 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -208,8 +208,7 @@ fn lookup_agent(name: &str) -> Option { "kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo), "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), "maki" => Some(Agent::Maki), - "muse" | "muse-code" | "muse-cli" => Some(Agent::Muse), - _ if name.starts_with("muse") => Some(Agent::Muse), + "muse" | "muse-bin" | "muse-code" | "muse-cli" => Some(Agent::Muse), _ => None, } } @@ -717,6 +716,10 @@ mod tests { assert_eq!(identify_agent("kilo"), Some(Agent::Kilo)); assert_eq!(identify_agent("kilo-code"), Some(Agent::Kilo)); assert_eq!(identify_agent("maki"), Some(Agent::Maki)); + assert_eq!(identify_agent("muse"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-bin"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-code"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-cli"), Some(Agent::Muse)); } #[test] @@ -828,6 +831,9 @@ mod tests { assert_eq!(identify_agent("zsh"), None); assert_eq!(identify_agent("vim"), None); assert_eq!(identify_agent("node"), None); + assert_eq!(identify_agent("museum"), None); + assert_eq!(identify_agent("muse-helper"), None); + assert_eq!(identify_agent("muser"), None); } #[test] diff --git a/website/agent-detection/muse.toml b/website/agent-detection/muse.toml index 40bbd7839..b50513bec 100644 --- a/website/agent-detection/muse.toml +++ b/website/agent-detection/muse.toml @@ -42,7 +42,7 @@ contains = ["Enter to select"] id = "working_esc_interrupt" state = "working" priority = 900 -region = "whole_recent" +region = "bottom_non_empty_lines(8)" visible_working = true contains = ["esc to interrupt"] not = [{ contains = ["Enter to select"] }] @@ -51,7 +51,7 @@ not = [{ contains = ["Enter to select"] }] id = "blocked_approval" state = "blocked" priority = 850 -region = "whole_recent" +region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ { contains = ["Do you want to proceed?"] }, @@ -67,7 +67,7 @@ state = "idle" priority = 700 region = "bottom_non_empty_lines(5)" visible_idle = true -line_regex = ['^\s*⟩'] +line_regex = ['^\s*⟩\s*$'] not = [{ contains = ["esc to interrupt"] }] [[rules]] From 4d3669249cd6018cf72e2e93a4d8acd668823a24 Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Mon, 10 Aug 2026 20:20:00 -0700 Subject: [PATCH 3/6] fix(detect): address mjdouglas vendor findings for muse - manifests: widen pick_request_blocked to bottom_non_empty_lines(8) and add Enter to toggle for multi-select; add menu_request_blocked for user-opened pickers (enter confirm/esc go back/esc close/space toggle); add workspace_trust_blocked for Do you trust dialog; tighten blocked_approval to footer chrome Allow once/Always allow/Yes proceed; split idle_prompt into any gate for empty and typed draft and not-gate pick/menu anchors; change idle_status_fallback from YOLO to model \u00b7 effort \u00b7 cwd shape - runtime: replace bare muse-bin alias with muse-bin- digit check to match versioned launcher binary and avoid museum/muse-binary false positives - cross-checked with vendor-side captures reported on PR #2489 against strings in shipped muse-bin binary refs #2489 --- src/detect/manifests/muse.toml | 136 ++++++++++++++++++++++++------ src/detect/mod.rs | 20 ++++- website/agent-detection/muse.toml | 136 ++++++++++++++++++++++++------ 3 files changed, 240 insertions(+), 52 deletions(-) diff --git a/src/detect/manifests/muse.toml b/src/detect/manifests/muse.toml index b50513bec..e839ce756 100644 --- a/src/detect/manifests/muse.toml +++ b/src/detect/manifests/muse.toml @@ -1,42 +1,115 @@ id = "muse" -version = "2026.08.06.1" +version = "2026.08.10.1" min_engine_version = 2 -updated_at = "2026-08-06T00:00:00Z" +updated_at = "2026-08-10T00:00:00Z" aliases = ["muse-code", "muse-cli"] # Evidence: Live pane reads via `herdr pane read --source detection --format text` -# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr. +# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr, cross-checked with +# vendor-side captures reported by a Muse Code developer on PR #2489 against strings in the +# shipped `muse-bin` binary. # # Idle (prompt ready) bottom chrome: # ── Voice input (⌥ + v to start) ───── ... ─ # ⟩ # ───────────────────────────────────── ... ─ # muse-spark-1.2-contributor · xhigh · ~/Developer/herdr · YOLO -# The prompt is a single `⟩` (U+27E9) on its own line. The status footer always -# contains `·` separators and the model path `~/Developer/herdr` (cwd) plus -# `YOLO` when launched with --yolo. +# The prompt is a single `⟩` (U+27E9) on its own line, or `⟩ ` while composing (the +# draft can wrap to continuation lines). `YOLO` only appears when launched with --yolo, so +# idle_status_fallback keys on the invariant `model · effort · cwd` footer shape instead +# (`·` is U+00B7; effort is one of none/minimal/low/medium/high/xhigh/ultra). # # Working (agent running tools/commands): # ◆ Running command · (0s · esc to interrupt) # └ (ctrl+b to send to background) -# ── Voice input (⌥ + v to start) ───── -# ⟩ ... -# Any live activity line contains `esc to interrupt`. This is the invariant -# between idle and working – idle footer is `YOLO`/`xhigh` without `esc`. +# ◇/◆ Finishing up (Ns · esc to interrupt) +# Any live activity line contains `esc to interrupt` (lowercase). This is the invariant +# between idle and working. # -# Blocked (approval) – Muse shows tool approval when not in --yolo. The -# footer gains acceptance hints. We encode generic blocked signals so --yolo -# idle is not misclassified. These patterns were not observed in the initial -# capture and are kept lower priority / generic. +# Agent-initiated Pick / multi-select (blocked, waiting on the human): +# Enter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt (single-select) +# Enter to toggle · ↑/↓ to move · ... · Esc to interrupt (multi-select) +# Title/message vary; the footer chrome does not. On narrow panes the footer wraps to two +# lines, and up to four more lines of persistent prompt chrome (voice-input hint, `⟩`, rule +# line, status line) sit below it, so the footer can land as far as the 6th non-empty line +# from the bottom. `working_esc_interrupt`'s not-gate must cover the same region as this rule +# (both bottom_non_empty_lines(8)) since the picker footer's `Esc to interrupt` would +# otherwise satisfy the working rule's `esc to interrupt` contains match (contains is +# case-insensitive; regex/line_regex are not). # +# Startup workspace-trust dialog (blocked, first launch in an untrusted directory): +# Do you trust this workspace? +# > 1 Trust and continue +# 2 Quit +# Use Up/Down or 1/2, then Enter. Esc quits. +# No footer chrome (no `esc to interrupt`/`Enter to select`), so without a dedicated rule +# this falls through to the known-agent idle fallback while the agent is actually blocked. +# +# User-opened menus (blocked by convention here; the agent isn't waiting on an answer, but +# the pane needs input before anything else can happen): `/model`, `/theme`, `/export`, +# `/subagents`, `/skills`, etc. Footer verb varies by menu — captured live: `/model` says +# `enter confirm`, `/theme` says `enter save`, `/skills` (a filterable checkbox list) says +# `enter use - space toggle - ... - esc close - type filter`. "Enter to select"/"Esc to +# interrupt" never appear. Matched on whichever of "esc go back" / "esc close" / "enter +# confirm" / "space toggle" is present rather than trying to enumerate every verb; more menu +# types than these four captures likely exist. The highlighted row in both `/model` and +# `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant +# matches, so idle_prompt's not-gate excludes all four menu anchors too, even though +# menu_request_blocked's higher priority already wins that race today. +# +# Tool approval (no --approval-judge, or judge declines to auto-allow): footer gains +# acceptance hints `Allow once` / `Allow for this session` / `Always allow in this workspace`. +# Anchored on this footer chrome rather than sentence prose ("Do you want to proceed?", +# "waiting for approval") because Muse will quote that prose back in its own replies and tool +# output, which would false-positive blocked right after a normal turn. +# +# Network-access approval is a distinct captured variant (live repro, `--approval-judge off`, +# a shell command doing an HTTPS fetch): title `Would you like to allow this network access?`, +# options `Yes, proceed (y)` / `Yes, don't ask again this session (p)` / `No, and tell Muse +# Code what to do differently (esc)`, footer `Press enter to confirm or esc to cancel`. No +# "esc to interrupt" here (esc cancels, not interrupts), and no "Allow"/footer text from the +# tool-approval variant, so without `blocked_approval` covering it this fell through to +# idle_status_fallback. The title itself is a bad anchor: with a `requested by: $ ` +# body line it landed as the 10th non-empty line from the bottom, past this rule's own +# bottom_non_empty_lines(8) region (the same wrap/offset trap as the Pick footer above). +# Matched on the footer option `Yes, proceed` instead, which stays a fixed few lines from the +# true bottom regardless of how long the request body gets. + +[[rules]] +id = "workspace_trust_blocked" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(12)" +visible_blocker = true +contains = ["Do you trust this workspace?"] +any = [ + { contains = ["Trust and continue"] }, + { contains = ["Use Up/Down"] }, +] [[rules]] id = "pick_request_blocked" state = "blocked" priority = 950 -region = "bottom_non_empty_lines(5)" +region = "bottom_non_empty_lines(8)" visible_blocker = true -contains = ["Enter to select"] +any = [ + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, +] + +[[rules]] +id = "menu_request_blocked" +state = "blocked" +priority = 940 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +any = [ + { contains = ["enter confirm"] }, + { contains = ["esc go back"] }, + { contains = ["esc close"] }, + { contains = ["space toggle"] }, +] [[rules]] id = "working_esc_interrupt" @@ -45,7 +118,10 @@ priority = 900 region = "bottom_non_empty_lines(8)" visible_working = true contains = ["esc to interrupt"] -not = [{ contains = ["Enter to select"] }] +not = [ + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, +] [[rules]] id = "blocked_approval" @@ -54,11 +130,10 @@ priority = 850 region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ - { contains = ["Do you want to proceed?"] }, - { contains = ["Allow", "Always allow"] }, - { contains = ["waiting for approval", "run this command?"], any = [{ contains = ["proceed"] }] }, - { contains = ["(y) (enter)"] }, - { contains = ["do you want to allow"] }, + { contains = ["Allow once"] }, + { contains = ["Allow for this session"] }, + { contains = ["Always allow in this workspace"] }, + { contains = ["Yes, proceed"] }, ] [[rules]] @@ -67,8 +142,19 @@ state = "idle" priority = 700 region = "bottom_non_empty_lines(5)" visible_idle = true -line_regex = ['^\s*⟩\s*$'] -not = [{ contains = ["esc to interrupt"] }] +any = [ + { line_regex = ['^\s*⟩\s*$'] }, + { line_regex = ['^\s*⟩\s+\S'] }, +] +not = [ + { contains = ["esc to interrupt"] }, + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, + { contains = ["enter confirm"] }, + { contains = ["esc go back"] }, + { contains = ["esc close"] }, + { contains = ["space toggle"] }, +] [[rules]] id = "idle_status_fallback" @@ -76,5 +162,5 @@ state = "idle" priority = 500 region = "bottom_non_empty_lines(3)" visible_idle = true -contains = ["YOLO"] +line_regex = ['^\s*\S+ · (none|minimal|low|medium|high|xhigh|ultra) · '] not = [{ contains = ["esc to interrupt"] }] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 96d7dfaf0..a99887dc6 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -208,11 +208,22 @@ fn lookup_agent(name: &str) -> Option { "kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo), "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), "maki" => Some(Agent::Maki), - "muse" | "muse-bin" | "muse-code" | "muse-cli" => Some(Agent::Muse), + "muse" | "muse-code" | "muse-cli" => Some(Agent::Muse), + _ if is_muse_versioned_binary(name) => Some(Agent::Muse), _ => None, } } +/// Muse's install-dir launcher script resolves the active release and execs +/// `muse-bin-` (e.g. `muse-bin-0.1.0-R708.1`), so the running +/// process never carries a bare `muse`/`muse-bin` alias. Require a digit +/// immediately after the `muse-bin-` prefix so unrelated binaries such as +/// `muse-binary` or a bare `muse-bin` stay unmatched. +fn is_muse_versioned_binary(name: &str) -> bool { + name.strip_prefix("muse-bin-") + .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())) +} + /// Identify which agent is running from the process name. /// Returns `None` for plain shells or unrecognized programs. pub fn identify_agent(process_name: &str) -> Option { @@ -717,9 +728,10 @@ mod tests { assert_eq!(identify_agent("kilo-code"), Some(Agent::Kilo)); assert_eq!(identify_agent("maki"), Some(Agent::Maki)); assert_eq!(identify_agent("muse"), Some(Agent::Muse)); - assert_eq!(identify_agent("muse-bin"), Some(Agent::Muse)); assert_eq!(identify_agent("muse-code"), Some(Agent::Muse)); assert_eq!(identify_agent("muse-cli"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-bin-0.1.0-R708.1"), Some(Agent::Muse)); + assert_eq!(identify_agent("muse-bin-1.2.3"), Some(Agent::Muse)); } #[test] @@ -834,6 +846,10 @@ mod tests { assert_eq!(identify_agent("museum"), None); assert_eq!(identify_agent("muse-helper"), None); assert_eq!(identify_agent("muser"), None); + assert_eq!(identify_agent("musescore"), None); + assert_eq!(identify_agent("muse-bin"), None); + assert_eq!(identify_agent("muse-bin-"), None); + assert_eq!(identify_agent("muse-binary"), None); } #[test] diff --git a/website/agent-detection/muse.toml b/website/agent-detection/muse.toml index b50513bec..e839ce756 100644 --- a/website/agent-detection/muse.toml +++ b/website/agent-detection/muse.toml @@ -1,42 +1,115 @@ id = "muse" -version = "2026.08.06.1" +version = "2026.08.10.1" min_engine_version = 2 -updated_at = "2026-08-06T00:00:00Z" +updated_at = "2026-08-10T00:00:00Z" aliases = ["muse-code", "muse-cli"] # Evidence: Live pane reads via `herdr pane read --source detection --format text` -# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr. +# captured from Muse Code 0.1.0 (muse-spark-1.2-contributor) in herdr, cross-checked with +# vendor-side captures reported by a Muse Code developer on PR #2489 against strings in the +# shipped `muse-bin` binary. # # Idle (prompt ready) bottom chrome: # ── Voice input (⌥ + v to start) ───── ... ─ # ⟩ # ───────────────────────────────────── ... ─ # muse-spark-1.2-contributor · xhigh · ~/Developer/herdr · YOLO -# The prompt is a single `⟩` (U+27E9) on its own line. The status footer always -# contains `·` separators and the model path `~/Developer/herdr` (cwd) plus -# `YOLO` when launched with --yolo. +# The prompt is a single `⟩` (U+27E9) on its own line, or `⟩ ` while composing (the +# draft can wrap to continuation lines). `YOLO` only appears when launched with --yolo, so +# idle_status_fallback keys on the invariant `model · effort · cwd` footer shape instead +# (`·` is U+00B7; effort is one of none/minimal/low/medium/high/xhigh/ultra). # # Working (agent running tools/commands): # ◆ Running command · (0s · esc to interrupt) # └ (ctrl+b to send to background) -# ── Voice input (⌥ + v to start) ───── -# ⟩ ... -# Any live activity line contains `esc to interrupt`. This is the invariant -# between idle and working – idle footer is `YOLO`/`xhigh` without `esc`. +# ◇/◆ Finishing up (Ns · esc to interrupt) +# Any live activity line contains `esc to interrupt` (lowercase). This is the invariant +# between idle and working. # -# Blocked (approval) – Muse shows tool approval when not in --yolo. The -# footer gains acceptance hints. We encode generic blocked signals so --yolo -# idle is not misclassified. These patterns were not observed in the initial -# capture and are kept lower priority / generic. +# Agent-initiated Pick / multi-select (blocked, waiting on the human): +# Enter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt (single-select) +# Enter to toggle · ↑/↓ to move · ... · Esc to interrupt (multi-select) +# Title/message vary; the footer chrome does not. On narrow panes the footer wraps to two +# lines, and up to four more lines of persistent prompt chrome (voice-input hint, `⟩`, rule +# line, status line) sit below it, so the footer can land as far as the 6th non-empty line +# from the bottom. `working_esc_interrupt`'s not-gate must cover the same region as this rule +# (both bottom_non_empty_lines(8)) since the picker footer's `Esc to interrupt` would +# otherwise satisfy the working rule's `esc to interrupt` contains match (contains is +# case-insensitive; regex/line_regex are not). # +# Startup workspace-trust dialog (blocked, first launch in an untrusted directory): +# Do you trust this workspace? +# > 1 Trust and continue +# 2 Quit +# Use Up/Down or 1/2, then Enter. Esc quits. +# No footer chrome (no `esc to interrupt`/`Enter to select`), so without a dedicated rule +# this falls through to the known-agent idle fallback while the agent is actually blocked. +# +# User-opened menus (blocked by convention here; the agent isn't waiting on an answer, but +# the pane needs input before anything else can happen): `/model`, `/theme`, `/export`, +# `/subagents`, `/skills`, etc. Footer verb varies by menu — captured live: `/model` says +# `enter confirm`, `/theme` says `enter save`, `/skills` (a filterable checkbox list) says +# `enter use - space toggle - ... - esc close - type filter`. "Enter to select"/"Esc to +# interrupt" never appear. Matched on whichever of "esc go back" / "esc close" / "enter +# confirm" / "space toggle" is present rather than trying to enumerate every verb; more menu +# types than these four captures likely exist. The highlighted row in both `/model` and +# `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant +# matches, so idle_prompt's not-gate excludes all four menu anchors too, even though +# menu_request_blocked's higher priority already wins that race today. +# +# Tool approval (no --approval-judge, or judge declines to auto-allow): footer gains +# acceptance hints `Allow once` / `Allow for this session` / `Always allow in this workspace`. +# Anchored on this footer chrome rather than sentence prose ("Do you want to proceed?", +# "waiting for approval") because Muse will quote that prose back in its own replies and tool +# output, which would false-positive blocked right after a normal turn. +# +# Network-access approval is a distinct captured variant (live repro, `--approval-judge off`, +# a shell command doing an HTTPS fetch): title `Would you like to allow this network access?`, +# options `Yes, proceed (y)` / `Yes, don't ask again this session (p)` / `No, and tell Muse +# Code what to do differently (esc)`, footer `Press enter to confirm or esc to cancel`. No +# "esc to interrupt" here (esc cancels, not interrupts), and no "Allow"/footer text from the +# tool-approval variant, so without `blocked_approval` covering it this fell through to +# idle_status_fallback. The title itself is a bad anchor: with a `requested by: $ ` +# body line it landed as the 10th non-empty line from the bottom, past this rule's own +# bottom_non_empty_lines(8) region (the same wrap/offset trap as the Pick footer above). +# Matched on the footer option `Yes, proceed` instead, which stays a fixed few lines from the +# true bottom regardless of how long the request body gets. + +[[rules]] +id = "workspace_trust_blocked" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(12)" +visible_blocker = true +contains = ["Do you trust this workspace?"] +any = [ + { contains = ["Trust and continue"] }, + { contains = ["Use Up/Down"] }, +] [[rules]] id = "pick_request_blocked" state = "blocked" priority = 950 -region = "bottom_non_empty_lines(5)" +region = "bottom_non_empty_lines(8)" visible_blocker = true -contains = ["Enter to select"] +any = [ + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, +] + +[[rules]] +id = "menu_request_blocked" +state = "blocked" +priority = 940 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +any = [ + { contains = ["enter confirm"] }, + { contains = ["esc go back"] }, + { contains = ["esc close"] }, + { contains = ["space toggle"] }, +] [[rules]] id = "working_esc_interrupt" @@ -45,7 +118,10 @@ priority = 900 region = "bottom_non_empty_lines(8)" visible_working = true contains = ["esc to interrupt"] -not = [{ contains = ["Enter to select"] }] +not = [ + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, +] [[rules]] id = "blocked_approval" @@ -54,11 +130,10 @@ priority = 850 region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ - { contains = ["Do you want to proceed?"] }, - { contains = ["Allow", "Always allow"] }, - { contains = ["waiting for approval", "run this command?"], any = [{ contains = ["proceed"] }] }, - { contains = ["(y) (enter)"] }, - { contains = ["do you want to allow"] }, + { contains = ["Allow once"] }, + { contains = ["Allow for this session"] }, + { contains = ["Always allow in this workspace"] }, + { contains = ["Yes, proceed"] }, ] [[rules]] @@ -67,8 +142,19 @@ state = "idle" priority = 700 region = "bottom_non_empty_lines(5)" visible_idle = true -line_regex = ['^\s*⟩\s*$'] -not = [{ contains = ["esc to interrupt"] }] +any = [ + { line_regex = ['^\s*⟩\s*$'] }, + { line_regex = ['^\s*⟩\s+\S'] }, +] +not = [ + { contains = ["esc to interrupt"] }, + { contains = ["Enter to select"] }, + { contains = ["Enter to toggle"] }, + { contains = ["enter confirm"] }, + { contains = ["esc go back"] }, + { contains = ["esc close"] }, + { contains = ["space toggle"] }, +] [[rules]] id = "idle_status_fallback" @@ -76,5 +162,5 @@ state = "idle" priority = 500 region = "bottom_non_empty_lines(3)" visible_idle = true -contains = ["YOLO"] +line_regex = ['^\s*\S+ · (none|minimal|low|medium|high|xhigh|ultra) · '] not = [{ contains = ["esc to interrupt"] }] From 2eb12139da2ac17525dcbfec10b737ae00797f21 Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Mon, 10 Aug 2026 20:26:21 -0700 Subject: [PATCH 4/6] fix(detect): handle path-qualified muse-bin versioned binary lookup_agent now uses path_basename before matching so a launcher exec with absolute argv0 like /usr/local/lib/muse/muse-bin-0.1.0-R708.1 is correctly identified. is_muse_versioned_binary also checks basename so direct path-qualified names succeed without relying on the argv0_argv fallback path. Prevents missed Muse detection when screen state and sound overrides are not applied. refs #2489 --- src/detect/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/detect/mod.rs b/src/detect/mod.rs index a99887dc6..f9f75815b 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -186,6 +186,7 @@ pub(crate) fn parse_canonical_agent_label(label: &str) -> Option { } fn lookup_agent(name: &str) -> Option { + let name = path_basename(name); match name { "pi" => Some(Agent::Pi), "claude" | "claude-code" => Some(Agent::Claude), @@ -219,8 +220,11 @@ fn lookup_agent(name: &str) -> Option { /// process never carries a bare `muse`/`muse-bin` alias. Require a digit /// immediately after the `muse-bin-` prefix so unrelated binaries such as /// `muse-binary` or a bare `muse-bin` stay unmatched. +/// Accepts path-qualified `argv0` values by checking only the basename, since +/// the launcher may `exec` with an absolute install-dir path. fn is_muse_versioned_binary(name: &str) -> bool { - name.strip_prefix("muse-bin-") + path_basename(name) + .strip_prefix("muse-bin-") .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())) } From 6240e1b6285dc45c7276638abc06a1735c3fdda4 Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Mon, 10 Aug 2026 20:28:58 -0700 Subject: [PATCH 5/6] fix(detect): add enter save to muse menu detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evidence documents /theme with enter save footer which was not matched. Add enter save to menu_request_blocked any list and to idle_prompt not-gate (highlighted row ⟩ shares glyph) and keep website mirror in sync. Update evidence comment counts from four to five. refs #2489 --- src/detect/manifests/muse.toml | 12 +++++++----- website/agent-detection/muse.toml | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/detect/manifests/muse.toml b/src/detect/manifests/muse.toml index e839ce756..f5a87ff9e 100644 --- a/src/detect/manifests/muse.toml +++ b/src/detect/manifests/muse.toml @@ -50,11 +50,11 @@ aliases = ["muse-code", "muse-cli"] # `/subagents`, `/skills`, etc. Footer verb varies by menu — captured live: `/model` says # `enter confirm`, `/theme` says `enter save`, `/skills` (a filterable checkbox list) says # `enter use - space toggle - ... - esc close - type filter`. "Enter to select"/"Esc to -# interrupt" never appear. Matched on whichever of "esc go back" / "esc close" / "enter -# confirm" / "space toggle" is present rather than trying to enumerate every verb; more menu -# types than these four captures likely exist. The highlighted row in both `/model` and -# `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant -# matches, so idle_prompt's not-gate excludes all four menu anchors too, even though +# interrupt" never appear. Matched on whichever of "enter confirm" / "enter save" / "esc go +# back" / "esc close" / "space toggle" is present rather than trying to enumerate every verb; +# more menu types than these five captures likely exist. The highlighted row in both `/model` +# and `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant +# matches, so idle_prompt's not-gate excludes all five menu anchors too, even though # menu_request_blocked's higher priority already wins that race today. # # Tool approval (no --approval-judge, or judge declines to auto-allow): footer gains @@ -106,6 +106,7 @@ region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ { contains = ["enter confirm"] }, + { contains = ["enter save"] }, { contains = ["esc go back"] }, { contains = ["esc close"] }, { contains = ["space toggle"] }, @@ -151,6 +152,7 @@ not = [ { contains = ["Enter to select"] }, { contains = ["Enter to toggle"] }, { contains = ["enter confirm"] }, + { contains = ["enter save"] }, { contains = ["esc go back"] }, { contains = ["esc close"] }, { contains = ["space toggle"] }, diff --git a/website/agent-detection/muse.toml b/website/agent-detection/muse.toml index e839ce756..f5a87ff9e 100644 --- a/website/agent-detection/muse.toml +++ b/website/agent-detection/muse.toml @@ -50,11 +50,11 @@ aliases = ["muse-code", "muse-cli"] # `/subagents`, `/skills`, etc. Footer verb varies by menu — captured live: `/model` says # `enter confirm`, `/theme` says `enter save`, `/skills` (a filterable checkbox list) says # `enter use - space toggle - ... - esc close - type filter`. "Enter to select"/"Esc to -# interrupt" never appear. Matched on whichever of "esc go back" / "esc close" / "enter -# confirm" / "space toggle" is present rather than trying to enumerate every verb; more menu -# types than these four captures likely exist. The highlighted row in both `/model` and -# `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant -# matches, so idle_prompt's not-gate excludes all four menu anchors too, even though +# interrupt" never appear. Matched on whichever of "enter confirm" / "enter save" / "esc go +# back" / "esc close" / "space toggle" is present rather than trying to enumerate every verb; +# more menu types than these five captures likely exist. The highlighted row in both `/model` +# and `/skills` also renders as `⟩ `, the same glyph idle_prompt's typed-draft variant +# matches, so idle_prompt's not-gate excludes all five menu anchors too, even though # menu_request_blocked's higher priority already wins that race today. # # Tool approval (no --approval-judge, or judge declines to auto-allow): footer gains @@ -106,6 +106,7 @@ region = "bottom_non_empty_lines(8)" visible_blocker = true any = [ { contains = ["enter confirm"] }, + { contains = ["enter save"] }, { contains = ["esc go back"] }, { contains = ["esc close"] }, { contains = ["space toggle"] }, @@ -151,6 +152,7 @@ not = [ { contains = ["Enter to select"] }, { contains = ["Enter to toggle"] }, { contains = ["enter confirm"] }, + { contains = ["enter save"] }, { contains = ["esc go back"] }, { contains = ["esc close"] }, { contains = ["space toggle"] }, From 3f6a6ef09b9c02cdabb78153ccbc7ac62839ee5a Mon Sep 17 00:00:00 2001 From: Omer Hamid Kamisli Date: Mon, 10 Aug 2026 20:53:28 -0700 Subject: [PATCH 6/6] docs: add Muse to supported agents table Muse detection (manifest, process identification, sound) was added earlier but docs/next agents table omitted the row. Add Muse as screen manifest / none (no integration hook, consistent with Amp/Kiro/Maki) to en/ja/zh-cn translations. --- docs/next/website/src/content/docs/agents.mdx | 1 + docs/next/website/src/content/docs/ja/agents.mdx | 1 + docs/next/website/src/content/docs/zh-cn/agents.mdx | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/next/website/src/content/docs/agents.mdx b/docs/next/website/src/content/docs/agents.mdx index 58939ed4b..eb4569347 100644 --- a/docs/next/website/src/content/docs/agents.mdx +++ b/docs/next/website/src/content/docs/agents.mdx @@ -32,6 +32,7 @@ Automatic detection works out of the box for common coding agents. The table sho | Antigravity CLI | screen manifest | session | | Kiro CLI | screen manifest | none | | Maki | screen manifest | none | +| Muse | screen manifest | none | Detected but less thoroughly tested: Gemini CLI and Cline. Unsupported agents still run normally as terminal processes. They just may not get rich state unless you add an integration or report state over the socket API. diff --git a/docs/next/website/src/content/docs/ja/agents.mdx b/docs/next/website/src/content/docs/ja/agents.mdx index eff614e7b..708a53681 100644 --- a/docs/next/website/src/content/docs/ja/agents.mdx +++ b/docs/next/website/src/content/docs/ja/agents.mdx @@ -32,6 +32,7 @@ Herdr は複数のコーディングエージェントを同時に動かすた | Antigravity CLI | スクリーンマニフェスト | セッション | | Kiro CLI | スクリーンマニフェスト | なし | | Maki | スクリーンマニフェスト | なし | +| Muse | スクリーンマニフェスト | なし | 検出されるもののテストが薄いもの: Gemini CLI と Cline。未対応のエージェントも通常のターミナルプロセスとして問題なく動きます。ただし、インテグレーションを追加するかソケット API で状態を報告しない限り、詳細な状態は得られない可能性があります。 diff --git a/docs/next/website/src/content/docs/zh-cn/agents.mdx b/docs/next/website/src/content/docs/zh-cn/agents.mdx index 08dec2c4f..ac8e08871 100644 --- a/docs/next/website/src/content/docs/zh-cn/agents.mdx +++ b/docs/next/website/src/content/docs/zh-cn/agents.mdx @@ -32,6 +32,7 @@ Herdr 为同时运行多个编程智能体而生。每个智能体都待在一 | Antigravity CLI | 屏幕清单 | 会话 | | Kiro CLI | 屏幕清单 | 无 | | Maki | 屏幕清单 | 无 | +| Muse | 屏幕清单 | 无 | 可检测但测试较少: Gemini CLI 和 Cline。不受支持的智能体仍然可以作为普通终端进程正常运行,只是在你添加集成或通过 socket API 上报状态之前,可能得不到丰富的状态。