diff --git a/docs/next/website/src/content/docs/session-state.mdx b/docs/next/website/src/content/docs/session-state.mdx index f6d1b95ef6..896caf7f27 100644 --- a/docs/next/website/src/content/docs/session-state.mdx +++ b/docs/next/website/src/content/docs/session-state.mdx @@ -88,6 +88,8 @@ Unsupported, missing, invalid, duplicated, or stale session references restore a If native agent session restore applies to a pane, Herdr resumes the agent session instead of replaying saved pane history for that pane. +Herdr also remembers compatible options from the detected agent process, such as model, permission, approval, and sandbox choices. Each agent manifest declares zero-value flags and one-value options that are safe to replay; all other arguments are dropped. The active bundled, downloaded, or local override manifest is checked again when restoring, so removing an option from a manifest revokes it for future resumes. + ## Live handoff Live handoff is for update and remote attach flows that need to replace a running Herdr server. It asks the old server to transfer live panes to the new server, so pane processes can keep running across the server replacement. diff --git a/scripts/agent_detection_manifest_check.py b/scripts/agent_detection_manifest_check.py index f180318bd1..171c2cd58f 100644 --- a/scripts/agent_detection_manifest_check.py +++ b/scripts/agent_detection_manifest_check.py @@ -16,7 +16,15 @@ DEFAULT_WEBSITE_DIR = PROJECT_ROOT / "website" / "agent-detection" ENGINE_SOURCE = PROJECT_ROOT / "src" / "detect" / "manifest_update.rs" -MANIFEST_KEYS = {"id", "version", "min_engine_version", "updated_at", "aliases", "rules"} +MANIFEST_KEYS = { + "id", + "version", + "min_engine_version", + "updated_at", + "aliases", + "resume_options", + "rules", +} RULE_KEYS = { "id", "state", @@ -52,6 +60,8 @@ MAX_MATCHERS_PER_GATE = 32 MAX_TOTAL_MATCHERS = 1024 MAX_MATCHER_CHARS = 512 +MAX_RESUME_OPTIONS_PER_MANIFEST = 128 +MAX_RESUME_OPTION_CHARS = 128 # Keep engine-2 clients on the OSC-capable manifest until an engine-3 release # can consume top_non_empty_lines. Remove this entry when the website publishes @@ -143,9 +153,32 @@ def validate_manifest(path: Path, engine_version: int) -> dict: if not isinstance(aliases, list) or not all(isinstance(item, str) for item in aliases): raise CheckError(f"{path}: aliases must be an array of strings") - rules = manifest.get("rules") - if not isinstance(rules, list) or not rules: - raise CheckError(f"{path}: rules must be a non-empty array") + resume_options = manifest.get("resume_options", {}) + if not isinstance(resume_options, dict) or set(resume_options) - {"flags", "options"}: + raise CheckError(f"{path}: resume_options may contain only flags and options") + seen_resume_options: set[str] = set() + for key in ("flags", "options"): + values = resume_options.get(key, []) + if not isinstance(values, list) or not all(isinstance(item, str) for item in values): + raise CheckError(f"{path}: resume_options.{key} must be an array of strings") + for value in values: + if len(value) > MAX_RESUME_OPTION_CHARS: + raise CheckError(f"{path}: resume option exceeds {MAX_RESUME_OPTION_CHARS} characters") + if not value.startswith("-") or value in {"-", "--"} or "=" in value: + raise CheckError(f"{path}: invalid resume option {value!r}") + if value in seen_resume_options: + raise CheckError(f"{path}: duplicate resume option {value!r}") + seen_resume_options.add(value) + if len(seen_resume_options) > MAX_RESUME_OPTIONS_PER_MANIFEST: + raise CheckError( + f"{path}: manifest exceeds max resume option count {MAX_RESUME_OPTIONS_PER_MANIFEST}" + ) + + rules = manifest.get("rules", []) + if not isinstance(rules, list): + raise CheckError(f"{path}: rules must be an array") + if not rules and not seen_resume_options: + raise CheckError(f"{path}: manifest must contain rules or resume options") if len(rules) > MAX_RULES_PER_MANIFEST: raise CheckError(f"{path}: manifest exceeds max rule count {MAX_RULES_PER_MANIFEST}") complexity = {"gates": 0, "matchers": 0} @@ -327,7 +360,7 @@ def validate_catalog( stages_new_engine_manifest = ( staged_manifest == (bundled_manifest["version"], manifest["version"], website_digest) - and bundled_manifest["min_engine_version"] == engine_version + and bundled_manifest["min_engine_version"] <= engine_version and manifest["min_engine_version"] < bundled_manifest["min_engine_version"] ) if cmp < 0 and not stages_new_engine_manifest: diff --git a/src/agent_resume_options.rs b/src/agent_resume_options.rs new file mode 100644 index 0000000000..3f79bbfcbd --- /dev/null +++ b/src/agent_resume_options.rs @@ -0,0 +1,90 @@ +pub(crate) fn filter(args: &[String], flags: &[String], options: &[String]) -> Vec { + let mut filtered = Vec::new(); + let mut index = 0; + + while let Some(arg) = args.get(index) { + if arg == "--" { + break; + } + if flags.iter().any(|flag| flag == arg) { + filtered.push(arg.clone()); + index += 1; + continue; + } + if let Some((name, value)) = arg.split_once('=') { + if options.iter().any(|option| option == name) && !value.is_empty() { + filtered.push(arg.clone()); + } + index += 1; + continue; + } + if options.iter().any(|option| option == arg) { + let Some(value) = args.get(index + 1) else { + break; + }; + if value == "--" || value.starts_with('-') { + index += 1; + continue; + } + filtered.push(arg.clone()); + filtered.push(value.clone()); + index += 2; + continue; + } + index += 1; + } + + filtered +} + +#[cfg(test)] +mod tests { + use super::*; + + fn strings(values: &[&str]) -> Vec { + values.iter().map(|value| (*value).to_string()).collect() + } + + #[test] + fn resume_options_filter_keeps_only_declared_flags_and_option_values() { + let args = strings(&[ + "--dangerously-skip-permissions", + "--model", + "opus", + "--unknown", + "unknown-value", + "--permission-mode=bypassPermissions", + "fix the bug", + "--model", + "sonnet", + ]); + + assert_eq!( + filter( + &args, + &strings(&["--dangerously-skip-permissions"]), + &strings(&["--model", "--permission-mode"]), + ), + strings(&[ + "--dangerously-skip-permissions", + "--model", + "opus", + "--permission-mode=bypassPermissions", + "--model", + "sonnet", + ]) + ); + } + + #[test] + fn resume_options_filter_drops_missing_values_and_stops_at_argument_separator() { + let args = strings(&["--model", "--", "--dangerously-skip-permissions", "prompt"]); + + assert!(filter( + &args, + &strings(&["--dangerously-skip-permissions"]), + &strings(&["--model"]), + ) + .is_empty()); + } +} diff --git a/src/app/actions.rs b/src/app/actions.rs index 07f7278ba4..d6d510cc74 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -2792,6 +2792,26 @@ impl AppState { }) .into_iter() .collect(), + AppEvent::AgentResumeOptionsDetected { + pane_id, + agent, + options, + } => { + let terminal_id = self.workspaces.iter().find_map(|workspace| { + workspace + .pane_state(pane_id) + .map(|pane| pane.attached_terminal_id.clone()) + }); + let changed = terminal_id.and_then(|terminal_id| { + self.terminals + .get_mut(&terminal_id) + .map(|terminal| terminal.set_agent_resume_options(agent, options)) + }); + if changed == Some(true) { + self.mark_session_dirty(); + } + Vec::new() + } AppEvent::StateChanged { pane_id, agent, @@ -4862,6 +4882,37 @@ mod tests { state.assert_invariants_for_test(); } + #[test] + fn detected_agent_resume_options_are_cached_and_mark_session_dirty() { + let mut state = app_with_workspaces(&["test"]); + let pane_id = state.workspaces[0].active_tab().unwrap().root_pane; + state.session_dirty = false; + + state.handle_app_event(AppEvent::AgentResumeOptionsDetected { + pane_id, + agent: Agent::Claude, + options: vec!["--model".into(), "opus".into()], + }); + + let terminal_id = state.workspaces[0].active_tab().unwrap().panes[&pane_id] + .attached_terminal_id + .clone(); + let cached = state.terminals[&terminal_id] + .agent_resume_options + .as_ref() + .unwrap(); + assert_eq!(cached.options, ["--model", "opus"]); + assert!(state.session_dirty); + + state.session_dirty = false; + state.handle_app_event(AppEvent::AgentResumeOptionsDetected { + pane_id, + agent: Agent::Claude, + options: vec!["--model".into(), "opus".into()], + }); + assert!(!state.session_dirty); + } + #[test] fn state_changed_updates_pane() { let mut state = app_with_workspaces(&["test"]); diff --git a/src/app/agent_resume.rs b/src/app/agent_resume.rs index 29c2c31768..84a4f97844 100644 --- a/src/app/agent_resume.rs +++ b/src/app/agent_resume.rs @@ -796,12 +796,14 @@ mod tests { let argv = vec![ "claude".to_string(), "--resume".to_string(), - "session with ' quote".to_string(), + "session-id".to_string(), + "--name".to_string(), + "worker seven's".to_string(), ]; assert_eq!( shell_command_from_argv(&argv).as_deref(), - Some("claude --resume 'session with '\\'' quote'") + Some("claude --resume session-id --name 'worker seven'\\''s'") ); assert_eq!(shell_command_from_argv(&[]), None); } diff --git a/src/app/api.rs b/src/app/api.rs index 5d347cf7d9..5ec48065c4 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -69,7 +69,7 @@ impl App { segment_index, result, } => self.handle_tab_bar_command_finished(generation, segment_index, result), - ev @ AppEvent::TerminalBell { .. } => { + ev @ (AppEvent::TerminalBell { .. } | AppEvent::AgentResumeOptionsDetected { .. }) => { self.handle_internal_event(ev); false } @@ -306,6 +306,7 @@ impl App { None }; let terminal_cwd_reported = matches!(ev, AppEvent::TerminalCwdReported { .. }); + let resume_options_detected = matches!(ev, AppEvent::AgentResumeOptionsDetected { .. }); let previous_toast = self.state.toast.clone(); let pane_updates = self.state.handle_app_event(ev); if let Some(agents) = manifest_update_agents { @@ -330,6 +331,9 @@ impl App { self.render_dirty.request_generic(); self.render_notify.notify_one(); } + if resume_options_detected { + return pane_updates; + } for update in &pane_updates { self.refresh_new_herdr_toast_context_for_update(update, &previous_toast); self.emit_pane_state_update(update); diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index 4ef77d2a28..84d7e5ce94 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -146,9 +146,20 @@ pub(crate) struct AgentManifest { #[serde(default)] aliases: Vec, #[serde(default)] + resume_options: ManifestResumeOptions, + #[serde(default)] rules: Vec, } +#[derive(Debug, Default, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +struct ManifestResumeOptions { + #[serde(default)] + flags: Vec, + #[serde(default)] + options: Vec, +} + #[derive(Debug, Deserialize, Clone)] #[serde(deny_unknown_fields)] struct ManifestRule { @@ -253,6 +264,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("kiro", include_str!("manifests/kiro.toml")), ("maki", include_str!("manifests/maki.toml")), ("muse", include_str!("manifests/muse.toml")), + ("omp", include_str!("manifests/omp.toml")), ("opencode", include_str!("manifests/opencode.toml")), ("pi", include_str!("manifests/pi.toml")), ("qodercli", include_str!("manifests/qodercli.toml")), @@ -269,6 +281,8 @@ const MAX_TOTAL_GATES: usize = 512; const MAX_MATCHERS_PER_GATE: usize = 32; const MAX_TOTAL_MATCHERS: usize = 1024; const MAX_MATCHER_CHARS: usize = 512; +const MAX_RESUME_OPTIONS_PER_MANIFEST: usize = 128; +const MAX_RESUME_OPTION_CHARS: usize = 128; pub(crate) fn reload_manifests() -> Vec { let _reload_guard = MANIFEST_RELOAD_LOCK @@ -295,7 +309,7 @@ pub(crate) fn reload_manifests_for_agents(agents: &[Agent]) { .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); let lock = manifest_cache(); - let replacements = Agent::SCREEN_MANIFEST_AGENTS + let replacements = Agent::MANIFEST_AGENTS .into_iter() .filter(|agent| agents.contains(agent)) .map(|agent| (agent, load_manifest_uncached(agent))) @@ -321,7 +335,7 @@ fn manifest_cache() -> &'static RwLock { fn build_manifest_cache() -> ManifestCache { ManifestCache { - manifests: Agent::SCREEN_MANIFEST_AGENTS + manifests: Agent::MANIFEST_AGENTS .into_iter() .map(|agent| (agent, load_manifest_uncached(agent))) .collect(), @@ -888,6 +902,17 @@ pub(crate) struct ParsedRemoteManifest { pub(crate) version: ManifestVersion, } +pub(crate) fn filter_resume_options(agent: Agent, args: &[String]) -> Vec { + let Some(loaded) = load_manifest(agent) else { + return Vec::new(); + }; + crate::agent_resume_options::filter( + args, + &loaded.manifest.resume_options.flags, + &loaded.manifest.resume_options.options, + ) +} + pub(crate) fn parse_manifest(content: &str) -> Result { let manifest = toml::from_str::(content).map_err(|err| err.to_string())?; validate_manifest(&manifest)?; @@ -922,9 +947,36 @@ pub(crate) fn parse_remote_manifest_for_agent( Ok(ParsedRemoteManifest { manifest, version }) } +fn validate_resume_options(resume_options: &ManifestResumeOptions) -> Result<(), String> { + if resume_options.flags.len() + resume_options.options.len() > MAX_RESUME_OPTIONS_PER_MANIFEST { + return Err(format!( + "manifest contains too many resume options, max is {MAX_RESUME_OPTIONS_PER_MANIFEST}" + )); + } + let mut seen = std::collections::HashSet::new(); + for option in resume_options.flags.iter().chain(&resume_options.options) { + if option.chars().count() > MAX_RESUME_OPTION_CHARS { + return Err(format!( + "resume option exceeds {MAX_RESUME_OPTION_CHARS} characters" + )); + } + if !option.starts_with('-') || option == "-" || option == "--" || option.contains('=') { + return Err(format!("invalid resume option {option:?}")); + } + if !seen.insert(option.as_str()) { + return Err(format!("duplicate resume option {option:?}")); + } + } + Ok(()) +} + fn validate_manifest(manifest: &AgentManifest) -> Result<(), String> { - if manifest.rules.is_empty() { - return Err("manifest must contain at least one rule".to_string()); + validate_resume_options(&manifest.resume_options)?; + if manifest.rules.is_empty() + && manifest.resume_options.flags.is_empty() + && manifest.resume_options.options.is_empty() + { + return Err("manifest must contain at least one rule or resume option".to_string()); } if manifest.rules.len() > MAX_RULES_PER_MANIFEST { return Err(format!( diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index edffa2bb59..bf1fd98f72 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -16,6 +16,18 @@ contains = ["{contains}"] ) } +fn resume_options_manifest(flags: &[&str], options: &[&str]) -> String { + format!( + r#" +id = "codex" + +[resume_options] +flags = {flags:?} +options = {options:?} +"# + ) +} + fn local_manifest(state: &str, contains: &str) -> String { format!( r#" @@ -289,9 +301,66 @@ fn detection_uses_cached_manifest_until_explicit_reload() { }); } +#[test] +fn claude_resume_options_keep_name_but_drop_system_prompt() { + let args = [ + "--name", + "worker-7", + "--append-system-prompt", + "review database changes", + ] + .map(str::to_string); + + assert_eq!( + filter_resume_options(Agent::Claude, &args), + ["--name", "worker-7"].map(str::to_string) + ); +} + +#[test] +fn resume_options_follow_the_cached_manifest_and_hot_reload() { + with_manifest_dirs("resume-options-hot-reload", || { + write_local_codex(&resume_options_manifest(&["--full-auto"], &["--model"])); + let args = ["--full-auto", "--model", "o3", "prompt"].map(str::to_string); + assert_eq!( + filter_resume_options(Agent::Codex, &args), + ["--full-auto", "--model", "o3"].map(str::to_string) + ); + + let path = override_path(Agent::Codex).unwrap(); + std::fs::write(&path, resume_options_manifest(&[], &["--model"])).unwrap(); + assert_eq!( + filter_resume_options(Agent::Codex, &args), + ["--full-auto", "--model", "o3"].map(str::to_string) + ); + + reload_manifests(); + assert_eq!( + filter_resume_options(Agent::Codex, &args), + ["--model", "o3"].map(str::to_string) + ); + }); +} + +#[test] +fn manifest_validation_rejects_ambiguous_resume_options() { + assert!(parse_manifest(&resume_options_manifest(&["--model"], &["--model"])).is_err()); + assert!(parse_manifest(&resume_options_manifest(&["--"], &[])).is_err()); + assert!(parse_manifest(&resume_options_manifest(&[], &["--model=value"])).is_err()); +} + +#[test] +fn resume_option_length_counts_characters_like_the_website_validator() { + let at_limit = format!("-{}", "é".repeat(MAX_RESUME_OPTION_CHARS - 1)); + assert!(parse_manifest(&resume_options_manifest(&[&at_limit], &[])).is_ok()); + + let over_limit = format!("-{}", "é".repeat(MAX_RESUME_OPTION_CHARS)); + assert!(parse_manifest(&resume_options_manifest(&[&over_limit], &[])).is_err()); +} + #[test] fn all_bundled_manifests_parse_and_validate() { - for agent in Agent::SCREEN_MANIFEST_AGENTS { + for agent in Agent::MANIFEST_AGENTS { assert!( bundled_manifest(agent).is_some(), "missing bundled manifest for {}", diff --git a/src/detect/manifest_update.rs b/src/detect/manifest_update.rs index 4e165fe918..9593053b59 100644 --- a/src/detect/manifest_update.rs +++ b/src/detect/manifest_update.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use super::{agent_label, parse_agent_label, Agent}; -pub(crate) const MANIFEST_ENGINE_VERSION: u32 = 3; +pub(crate) const MANIFEST_ENGINE_VERSION: u32 = 4; const DEFAULT_CATALOG_URL: &str = "https://herdr.dev/agent-detection/index.toml"; const CATALOG_URL_ENV: &str = "HERDR_AGENT_DETECTION_MANIFEST_CATALOG_URL"; const MAX_FETCH_BYTES: usize = 256 * 1024; diff --git a/src/detect/manifests/claude.toml b/src/detect/manifests/claude.toml index 696a197890..0712dcdd08 100644 --- a/src/detect/manifests/claude.toml +++ b/src/detect/manifests/claude.toml @@ -1,9 +1,30 @@ id = "claude" version = "2026.08.21.1" -min_engine_version = 2 +min_engine_version = 4 updated_at = "2026-08-21T00:00:00Z" aliases = ["claude-code"] +[resume_options] +flags = [ + "--dangerously-skip-permissions", + "--allow-dangerously-skip-permissions", + "--bare", + "--safe-mode", + "--chrome", + "--no-chrome", + "--disable-slash-commands", + "--ide", + "--verbose", +] +options = [ + "--agent", + "--autocompact", + "--effort", + "--model", + "--name", + "--permission-mode", +] + [[rules]] id = "osc_title_working" state = "working" diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index 9169e10848..ebc3833d16 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,27 @@ id = "codex" -version = "2026.08.09.1" -min_engine_version = 3 -updated_at = "2026-08-09T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" + +[resume_options] +flags = [ + "--approve-for-me", + "--dangerously-bypass-approvals-and-sandbox", + "--dangerously-bypass-hook-trust", + "--oss", + "--search", + "--strict-config", + "--no-alt-screen", +] +options = [ + "-m", + "--model", + "-s", + "--sandbox", + "-a", + "--ask-for-approval", + "--local-provider", +] [[rules]] id = "osc_title_blocked" diff --git a/src/detect/manifests/cursor.toml b/src/detect/manifests/cursor.toml index ee03e6db9d..94f9d552fb 100644 --- a/src/detect/manifests/cursor.toml +++ b/src/detect/manifests/cursor.toml @@ -1,9 +1,13 @@ id = "cursor" -version = "2026.08.03.1" -min_engine_version = 1 -updated_at = "2026-08-03T01:08:04Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["cursor-agent"] +[resume_options] +flags = ["-f", "--force", "--yolo", "--auto-review", "--trust"] +options = ["--model", "--mode", "--sandbox"] + [[rules]] id = "write_file_approval" state = "blocked" diff --git a/src/detect/manifests/droid.toml b/src/detect/manifests/droid.toml index c41d71b43b..0d4f008fca 100644 --- a/src/detect/manifests/droid.toml +++ b/src/detect/manifests/droid.toml @@ -1,7 +1,10 @@ id = "droid" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" + +[resume_options] +options = ["--auto"] [[rules]] id = "execute_selection_blocker" diff --git a/src/detect/manifests/github-copilot.toml b/src/detect/manifests/github-copilot.toml index 4233c563eb..016f9baa85 100644 --- a/src/detect/manifests/github-copilot.toml +++ b/src/detect/manifests/github-copilot.toml @@ -1,9 +1,36 @@ id = "copilot" -version = "2026.07.07.1" -min_engine_version = 1 -updated_at = "2026-07-07T14:15:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["github-copilot", "ghcs"] +[resume_options] +flags = [ + "--allow-all", + "--allow-all-tools", + "--allow-all-urls", + "--autopilot", + "--plan", + "--disallow-temp-dir", + "--enable-memory", + "--enable-reasoning-summaries", + "--experimental", + "--no-experimental", + "--no-ask-user", + "--no-custom-instructions", + "--yolo", +] +options = [ + "--agent", + "--effort", + "--reasoning-effort", + "--max-ai-credits", + "--max-autopilot-continues", + "--mode", + "--model", + "--stream", +] + [[rules]] id = "selection_blocker" state = "blocked" diff --git a/src/detect/manifests/hermes.toml b/src/detect/manifests/hermes.toml index 1754218497..bd4bf6f10d 100644 --- a/src/detect/manifests/hermes.toml +++ b/src/detect/manifests/hermes.toml @@ -1,9 +1,23 @@ id = "hermes" -version = "2026.07.24.1" -min_engine_version = 2 -updated_at = "2026-07-24T19:12:54Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["hermes-agent"] +[resume_options] +flags = [ + "--yolo", + "--accept-hooks", + "--ignore-user-config", + "--ignore-rules", + "--safe-mode", + "--no-restore-cwd", + "--pass-session-id", + "--tui", + "--cli", +] +options = ["-m", "--model", "--provider", "-t", "--toolsets", "--skills", "-s"] + [[rules]] id = "osc_title_blocked" state = "blocked" diff --git a/src/detect/manifests/kimi.toml b/src/detect/manifests/kimi.toml index b4d0100fba..11555d1725 100644 --- a/src/detect/manifests/kimi.toml +++ b/src/detect/manifests/kimi.toml @@ -1,9 +1,13 @@ id = "kimi" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["kimi-code", "kimi code"] +[resume_options] +flags = ["-y", "--yolo", "--auto", "--plan"] +options = ["-m", "--model", "--agent"] + [[rules]] id = "current_approval_panel" state = "blocked" diff --git a/src/detect/manifests/omp.toml b/src/detect/manifests/omp.toml new file mode 100644 index 0000000000..1a76f6c72b --- /dev/null +++ b/src/detect/manifests/omp.toml @@ -0,0 +1,33 @@ +id = "omp" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" +aliases = ["oh-my-pi"] + +[resume_options] +flags = [ + "--prewalk", + "--no-prewalk", + "--plan-yolo", + "--allow-home", + "--no-tools", + "--no-lsp", + "--no-pty", + "--hide-thinking", + "--advisor", + "--no-extensions", + "--no-skills", + "--no-rules", + "--no-title", + "--auto-approve", +] +options = [ + "--model", + "--smol", + "--slow", + "--plan", + "--provider", + "--thinking", + "--max-time", + "--approval-mode", +] diff --git a/src/detect/manifests/opencode.toml b/src/detect/manifests/opencode.toml index 5245238371..2303508083 100644 --- a/src/detect/manifests/opencode.toml +++ b/src/detect/manifests/opencode.toml @@ -1,9 +1,13 @@ id = "opencode" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["open-code", "herdr:opencode"] +[resume_options] +flags = ["--pure", "--auto", "--mini", "--no-replay"] +options = ["-m", "--model", "--agent", "--replay-limit"] + [[rules]] id = "permission_required" state = "blocked" diff --git a/src/detect/manifests/pi.toml b/src/detect/manifests/pi.toml index a58d30e903..19cfbfcfe3 100644 --- a/src/detect/manifests/pi.toml +++ b/src/detect/manifests/pi.toml @@ -1,9 +1,34 @@ id = "pi" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["herdr:pi"] +[resume_options] +flags = [ + "--no-tools", + "-nt", + "--no-builtin-tools", + "-nbt", + "--no-extensions", + "-ne", + "--no-skills", + "-ns", + "--no-context-files", + "-nc", + "--approve", + "-a", + "--no-approve", + "-na", + "--offline", +] +options = [ + "--provider", + "--model", + "--thinking", + "--tui-mode", +] + [[rules]] id = "working_literal" state = "working" diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 0f956ee311..a383881a18 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -93,7 +93,7 @@ impl Agent { Self::Muse, ]; - pub const SCREEN_MANIFEST_AGENTS: [Self; 21] = [ + pub const MANIFEST_AGENTS: [Self; 22] = [ Self::Pi, Self::Claude, Self::Codex, @@ -102,6 +102,7 @@ impl Agent { Self::Devin, Self::Antigravity, Self::Cline, + Self::Omp, Self::OpenCode, Self::GithubCopilot, Self::Kimi, @@ -241,6 +242,12 @@ pub fn identify_agent(process_name: &str) -> Option { } pub fn identify_agent_in_job(job: &crate::platform::ForegroundJob) -> Option<(Agent, String)> { + identify_agent_process_in_job(job).map(|(agent, name, _)| (agent, name)) +} + +fn identify_agent_process_in_job( + job: &crate::platform::ForegroundJob, +) -> Option<(Agent, String, &crate::platform::ForegroundProcess)> { if let Some(process) = job .processes .iter() @@ -248,26 +255,58 @@ pub fn identify_agent_in_job(job: &crate::platform::ForegroundJob) -> Option<(Ag { let candidate = normalized_process_name(process); if let Some(agent) = identify_agent(&candidate) { - return Some((agent, candidate)); + return Some((agent, candidate, process)); } } - let mut best: Option<(u8, Agent, String)> = None; - + let mut best: Option<(u8, Agent, String, &crate::platform::ForegroundProcess)> = None; for process in &job.processes { let candidate = normalized_process_name(process); let Some(agent) = identify_agent(&candidate) else { continue; }; let score = process_priority(process, &candidate); - match &best { - Some((best_score, _, _)) if *best_score >= score => {} - _ => best = Some((score, agent, candidate)), + Some((best_score, _, _, _)) if *best_score >= score => {} + _ => best = Some((score, agent, candidate, process)), } } + best.map(|(_, agent, name, process)| (agent, name, process)) +} - best.map(|(_, agent, name)| (agent, name)) +pub fn resume_options_in_job( + job: &crate::platform::ForegroundJob, + agent: Agent, +) -> Option> { + let (detected_agent, _, process) = identify_agent_process_in_job(job)?; + (detected_agent == agent) + .then(|| resume_options_for_process(process, agent)) + .flatten() +} + +fn resume_options_for_process( + process: &crate::platform::ForegroundProcess, + agent: Agent, +) -> Option> { + let argv = process.argv.as_deref()?; + let label = agent_label(agent); + let options_start = argv + .iter() + .position(|token| agent_name_from_path_token(token).as_deref() == Some(label)) + .map(|agent_token| agent_token + 1); + #[cfg(windows)] + let options_start = options_start.or_else(|| { + (agent == Agent::Cursor && cursor_agent_name_from_bundled_node_argv(argv).is_some()) + .then_some(2) + }); + if let Some(options_start) = options_start { + return Some(manifest::filter_resume_options( + agent, + &argv[options_start..], + )); + } + + command_wrapper_text(argv).and_then(|command| command_text_resume_options(command, agent)) } /// Detect the state of an agent from the live terminal tail snapshot. @@ -399,8 +438,13 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) let runtime_name = normalized_agent_lookup_name(path_basename(runtime)); match runtime_name.as_str() { - "node" => cursor_agent_name_from_bundled_node_argv(argv) - .or_else(|| script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[])), + "node" => { + #[cfg(windows)] + if let Some(agent) = cursor_agent_name_from_bundled_node_argv(argv) { + return Some(agent); + } + script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]) + } "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), name if is_python_runtime(name) => script_arg_agent_name(argv, &["-c"], &["-m"]), "sh" | "bash" | "zsh" | "fish" => script_arg_agent_name(argv, &["-c"], &[]), @@ -411,6 +455,7 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) } } +#[cfg(windows)] fn cursor_agent_name_from_bundled_node_argv(argv: &[String]) -> Option { let (runtime_parent, runtime_name) = path_parent_and_basename(argv.first()?)?; let (script_parent, script_name) = path_parent_and_basename(argv.get(1)?)?; @@ -434,6 +479,7 @@ fn cursor_agent_name_from_bundled_node_argv(argv: &[String]) -> Option { .then(|| agent_label(Agent::Cursor).to_string()) } +#[cfg(windows)] fn path_parent_and_basename(path: &str) -> Option<(&str, &str)> { let split = path.rfind(['/', '\\'])?; let parent = path[..split].trim_end_matches(['/', '\\']); @@ -441,16 +487,25 @@ fn path_parent_and_basename(path: &str) -> Option<(&str, &str)> { (!parent.is_empty() && !basename.is_empty()).then_some((parent, basename)) } +fn command_wrapper_text(argv: &[String]) -> Option<&str> { + let runtime = normalized_agent_lookup_name(path_basename(argv.first()?)); + match runtime.as_str() { + "cmd" => windows_cmd_command_text(argv), + "powershell" | "pwsh" => powershell_command_text(argv), + _ => None, + } +} + fn windows_cmd_arg_agent_name(argv: &[String]) -> Option { + windows_cmd_command_text(argv).and_then(command_text_agent_name) +} + +fn windows_cmd_command_text(argv: &[String]) -> Option<&str> { let mut args = argv.iter().skip(1); while let Some(arg) = args.next() { let flag = arg.trim_matches('"').to_lowercase(); match flag.as_str() { - "/c" | "/k" => { - return args - .next() - .and_then(|command| command_text_agent_name(command)) - } + "/c" | "/k" => return args.next().map(String::as_str), "/d" | "/s" | "/q" | "/a" | "/u" | "/e:on" | "/e:off" | "/f:on" | "/f:off" | "/v:on" | "/v:off" => continue, _ => {} @@ -486,7 +541,50 @@ fn powershell_arg_agent_name(argv: &[String]) -> Option { None } +fn powershell_command_text(argv: &[String]) -> Option<&str> { + let mut args = argv.iter().skip(1); + while let Some(arg) = args.next() { + let flag = arg.trim_matches('"').to_lowercase(); + match flag.as_str() { + "-command" | "-c" | "/command" | "/c" => return args.next().map(String::as_str), + "-file" | "-f" | "/file" | "-encodedcommand" | "-enc" | "/encodedcommand" | "/enc" => { + return None + } + "-configurationname" | "-executionpolicy" | "-outputformat" | "-psconsolefile" + | "-version" | "-windowstyle" | "-workingdirectory" => { + let _ = args.next(); + } + _ if flag.starts_with('-') || flag.starts_with('/') => {} + _ => return None, + } + } + None +} + fn command_text_agent_name(command: &str) -> Option { + let (agent, _) = command_text_agent(command)?; + Some(agent) +} + +fn command_text_resume_options(command: &str, agent: Agent) -> Option> { + let (detected_agent, mut rest) = command_text_agent(command)?; + if detected_agent != agent_label(agent) { + return None; + } + + let mut args = Vec::new(); + while let Some((token, next)) = command_text_token(rest) { + let token = token.trim(); + if matches!(token, "&" | "&&" | "|" | "||" | ";") { + break; + } + args.push(token.to_string()); + rest = next; + } + Some(manifest::filter_resume_options(agent, &args)) +} + +fn command_text_agent(command: &str) -> Option<(String, &str)> { let mut rest = command; while let Some((token, next)) = command_text_token(rest) { let token = token.trim(); @@ -497,7 +595,7 @@ fn command_text_agent_name(command: &str) -> Option { rest = next; continue; } - return agent_name_from_path_token(token); + return agent_name_from_path_token(token).map(|agent| (agent, next)); } None } @@ -892,7 +990,7 @@ mod tests { "herdr:mastracode", "mastracode" )); - assert!(!Agent::SCREEN_MANIFEST_AGENTS.contains(&Agent::Mastracode)); + assert!(!Agent::MANIFEST_AGENTS.contains(&Agent::Mastracode)); } #[test] @@ -904,7 +1002,7 @@ mod tests { ] { assert!(!full_lifecycle_hook_authority(source, label)); assert!(session_identity_only_integration(source, label)); - assert!(Agent::SCREEN_MANIFEST_AGENTS.contains(&agent)); + assert!(Agent::MANIFEST_AGENTS.contains(&agent)); } } @@ -968,6 +1066,7 @@ mod tests { } } + #[cfg(windows)] #[test] fn identify_agent_in_job_detects_windows_cursor_install() { let job = crate::platform::ForegroundJob { @@ -988,6 +1087,35 @@ mod tests { ); } + #[cfg(windows)] + #[test] + fn windows_cursor_install_preserves_resume_options_after_the_script() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 123, + "node.exe", + &[ + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\node.exe", + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index.js", + "--model", + "composer-1", + "--yolo", + ], + )], + }; + + assert_eq!( + resume_options_in_job(&job, Agent::Cursor), + Some(vec![ + "--model".to_string(), + "composer-1".to_string(), + "--yolo".to_string(), + ]) + ); + } + + #[cfg(windows)] #[test] fn identify_agent_in_job_ignores_invalid_windows_cursor_install_paths() { for script in [ @@ -1024,6 +1152,32 @@ mod tests { assert_eq!(identify_agent_in_job(&lookalike), None); } + #[test] + fn resume_options_in_job_filters_arguments_after_wrapped_agent_token() { + let job = crate::platform::ForegroundJob { + process_group_id: 1, + processes: vec![foreground_process( + 1, + "node", + &[ + "node", + "/path/to/bin/claude", + "--permission-mode", + "bypassPermissions", + "fix the bug", + ], + )], + }; + + assert_eq!( + resume_options_in_job(&job, Agent::Claude), + Some(vec![ + "--permission-mode".to_string(), + "bypassPermissions".to_string(), + ]) + ); + } + #[test] fn identify_agent_in_job_prefers_recognized_process_group_leader() { let job = crate::platform::ForegroundJob { @@ -1226,6 +1380,51 @@ mod tests { ); } + #[test] + fn resume_options_in_job_reads_options_from_windows_cmd_command_text() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "cmd.exe", + &[ + "cmd.exe", + "/D", + "/S", + "/C", + "C:\\Users\\herdr\\AppData\\Roaming\\npm\\codex.cmd --model \"gpt 5\" && echo done", + ], + )], + }; + + assert_eq!( + resume_options_in_job(&job, Agent::Codex), + Some(vec!["--model".to_string(), "gpt 5".to_string()]) + ); + } + + #[test] + fn resume_options_in_job_reads_options_from_powershell_command_text() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "powershell.exe", + &[ + "powershell.exe", + "-NoProfile", + "-Command", + "& 'C:\\Tools\\claude.ps1' --name 'worker one' ; echo done", + ], + )], + }; + + assert_eq!( + resume_options_in_job(&job, Agent::Claude), + Some(vec!["--name".to_string(), "worker one".to_string()]) + ); + } + #[test] fn identify_agent_in_job_detects_powershell_file_wrapped_claude() { let job = crate::platform::ForegroundJob { diff --git a/src/events.rs b/src/events.rs index cffcf03d8c..474ff1efbe 100644 --- a/src/events.rs +++ b/src/events.rs @@ -62,6 +62,12 @@ pub enum AppEvent { agent: Agent, observed_at: Instant, }, + /// Replayable options observed on a detected agent invocation. + AgentResumeOptionsDetected { + pane_id: PaneId, + agent: Agent, + options: Vec, + }, /// Fallback detector state changed in a pane. StateChanged { pane_id: PaneId, diff --git a/src/main.rs b/src/main.rs index a6ba1c0309..7722cd740d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,6 +55,7 @@ fn set_host_color_scheme_reports(enabled: bool) -> io::Result<()> { } mod agent_resume; +mod agent_resume_options; mod api; mod app; mod build_info; diff --git a/src/pane.rs b/src/pane.rs index 9f2416ab18..020cc90498 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -186,6 +186,46 @@ fn active_pending_release( } } +async fn publish_agent_resume_options( + last_reported: &mut Option<(Option, Agent, Vec)>, + state_events: &mpsc::Sender, + pane_id: PaneId, + process_group_id: Option, + agent: Option, + options: Option>, +) { + let Some(agent) = agent else { + return; + }; + let options = match options { + Some(options) => options, + None => match last_reported.as_ref() { + Some((reported_group, reported_agent, _)) + if *reported_group != process_group_id || *reported_agent != agent => + { + Vec::new() + } + _ => return, + }, + }; + if last_reported.as_ref().is_some_and(|reported| { + reported.0 == process_group_id && reported.1 == agent && reported.2 == options + }) { + return; + } + *last_reported = Some((process_group_id, agent, options.clone())); + if let Err(err) = state_events + .send(AppEvent::AgentResumeOptionsDetected { + pane_id, + agent, + options, + }) + .await + { + warn!(pane = pane_id.raw(), %err, "failed to deliver agent resume options"); + } +} + async fn publish_state_changed_event( state_events: mpsc::Sender, pane_id: PaneId, @@ -549,6 +589,7 @@ struct ProcessProbeResult { foreground_is_pane_shell: bool, agent: Option, process_name: Option, + resume_options: Option>, } fn agent_hint_for_foreground_job_members( @@ -594,6 +635,7 @@ fn process_probe_result( foreground_is_pane_shell: job.processes.iter().any(|process| process.pid == pid), agent: Some(agent), process_name: Some(process_name), + resume_options: crate::detect::resume_options_in_job(job, agent), } } @@ -654,6 +696,9 @@ fn probe_foreground_process_from_jobs( process_group_id: Some(job.process_group_id), foreground_is_pane_shell: job.processes.iter().any(|process| process.pid == pid), agent: identified.as_ref().map(|(agent, _)| *agent), + resume_options: identified + .as_ref() + .and_then(|(agent, _)| crate::detect::resume_options_in_job(job, *agent)), process_name: identified.map(|(_, process_name)| process_name), }; } @@ -663,6 +708,7 @@ fn probe_foreground_process_from_jobs( foreground_is_pane_shell: false, agent: None, process_name: None, + resume_options: None, } } @@ -713,6 +759,7 @@ fn spawn_basic_detection_task( let mut last_screen_scan_detection_content_seq = None; let mut agent_startup_grace_until = None; let mut pending_idle = PendingIdleConfirmation::default(); + let mut last_reported_resume_options = None; loop { let sleep_duration = if pending_idle.active() { @@ -741,6 +788,7 @@ fn spawn_basic_detection_task( last_screen_scan_detection_content_seq = None; agent_startup_grace_until = None; pending_idle.clear(); + last_reported_resume_options = None; } } @@ -790,6 +838,7 @@ fn spawn_basic_detection_task( let tracked_process_group_id = process_group_for_change_tracking(foreground_pgid, process_group_id); let foreground_is_pane_shell = probe.foreground_is_pane_shell; + let resume_options = probe.resume_options; let mut new_agent = probe.agent; if let Some(suppressed_agent) = suppressed_agent { if new_agent == Some(suppressed_agent) { @@ -798,6 +847,15 @@ fn spawn_basic_detection_task( *pending_release = None; } } + publish_agent_resume_options( + &mut last_reported_resume_options, + &state_events, + pane_id, + process_group_id, + new_agent, + resume_options, + ) + .await; let previous_agent = agent_presence.current_agent(); let foreground_action = foreground_shell_agent_action( previous_agent, @@ -2199,6 +2257,7 @@ impl PaneRuntime { let mut last_screen_scan_detection_content_seq = None; let mut agent_startup_grace_until = None; let mut pending_idle = PendingIdleConfirmation::default(); + let mut last_reported_resume_options = None; tokio::time::sleep(Duration::from_millis(50)).await; @@ -2237,6 +2296,7 @@ impl PaneRuntime { last_screen_scan_detection_content_seq = None; agent_startup_grace_until = None; pending_idle.clear(); + last_reported_resume_options = None; } } @@ -2317,6 +2377,7 @@ impl PaneRuntime { process_group_id, ); let foreground_is_pane_shell = probe.foreground_is_pane_shell; + let resume_options = probe.resume_options; let mut new_agent = probe.agent; if let Some(suppressed_agent) = suppressed_agent { @@ -2329,6 +2390,15 @@ impl PaneRuntime { } } + publish_agent_resume_options( + &mut last_reported_resume_options, + &state_events, + pane_id, + process_group_id, + new_agent, + resume_options, + ) + .await; let previous_agent = agent_presence.current_agent(); let foreground_action = foreground_shell_agent_action( previous_agent, @@ -3112,6 +3182,39 @@ mod tests { assert!(cmd.get_env("WT_SESSION").is_none()); } + #[tokio::test] + async fn unreadable_options_clear_a_previous_agent_invocation() { + let (events, mut event_rx) = mpsc::channel(4); + let pane_id = PaneId::alloc(); + let mut last = None; + + publish_agent_resume_options( + &mut last, + &events, + pane_id, + Some(10), + Some(Agent::Claude), + Some(vec!["--model".into(), "opus".into()]), + ) + .await; + let _ = event_rx.recv().await.unwrap(); + + publish_agent_resume_options( + &mut last, + &events, + pane_id, + Some(11), + Some(Agent::Claude), + None, + ) + .await; + + assert!(matches!( + event_rx.recv().await, + Some(AppEvent::AgentResumeOptionsDetected { options, .. }) if options.is_empty() + )); + } + #[tokio::test] async fn cwd_returns_accepted_report_without_rechecking_filesystem() { let stamp = std::time::SystemTime::now() diff --git a/src/persist/restore.rs b/src/persist/restore.rs index 3c5775c9e1..85b9ea7d6b 100644 --- a/src/persist/restore.rs +++ b/src/persist/restore.rs @@ -497,6 +497,7 @@ fn restore_tab( .and_then(crate::detect::parse_canonical_agent_label); let saved_launch_argv = saved_pane.and_then(|p| p.launch_argv.clone()); let saved_agent_session = saved_pane.and_then(|p| p.agent_session.as_ref()); + let saved_agent_resume_options = saved_pane.and_then(|p| p.agent_resume_options.as_ref()); let saved_history = old_id.and_then(|old_id| history.and_then(|history| history.panes.get(old_id))); let startup = { @@ -504,7 +505,12 @@ fn restore_tab( enabled: runtime_context.resume_agents_on_restore, resumed_sessions: resumed_agent_sessions, }; - pane_restore_startup(saved_agent_session, saved_history, &mut agent_restore) + pane_restore_startup( + saved_agent_session, + saved_agent_resume_options, + saved_history, + &mut agent_restore, + ) }; let restored_agent_session = restored_terminal_agent_session(saved_agent_session, startup.duplicate_agent_session); @@ -543,6 +549,13 @@ fn restore_tab( if let Some(session) = restored_agent_session { terminal.set_persisted_agent_session(session); } + if let Some(options) = + resume_options_for_session(saved_agent_resume_options, saved_agent_session) + { + if let Some(agent) = crate::detect::parse_canonical_agent_label(&options.agent) { + terminal.set_agent_resume_options(agent, options.options.clone()); + } + } match (saved_agent_name, saved_managed_agent) { (Some(agent_name), Some(agent)) => { terminal.restore_managed_agent(agent_name, agent) @@ -738,6 +751,7 @@ fn restore_tab( fn pane_restore_startup<'a>( session: Option<&PaneAgentSessionSnapshot>, + resume_options: Option<&super::snapshot::PaneAgentResumeOptionsSnapshot>, history: Option<&'a PaneHistorySnapshot>, agent_restore: &mut AgentRestoreState<'_>, ) -> PaneRestoreStartup<'a> { @@ -745,8 +759,9 @@ fn pane_restore_startup<'a>( // resumable agent session and resume is enabled, do not replay saved pane // presentation history into that terminal, even when this pane is a // duplicate suppressed by session de-duplication. - let restore_plan = - session.and_then(|session| restore_plan_for_snapshot(session, agent_restore.enabled)); + let restore_plan = session.and_then(|session| { + restore_plan_for_snapshot(session, resume_options, agent_restore.enabled) + }); let has_native_agent_restore = restore_plan.is_some(); // Reserve before spawning so later panes in the same restore pass cannot // launch the same native agent session. The caller rolls this reservation @@ -781,15 +796,34 @@ fn pane_restore_startup<'a>( } } +fn resume_options_for_session<'a>( + options: Option<&'a super::snapshot::PaneAgentResumeOptionsSnapshot>, + session: Option<&PaneAgentSessionSnapshot>, +) -> Option<&'a super::snapshot::PaneAgentResumeOptionsSnapshot> { + options.filter(|options| session.is_none_or(|session| session.agent == options.agent)) +} + fn restore_plan_for_snapshot( session: &PaneAgentSessionSnapshot, + resume_options: Option<&super::snapshot::PaneAgentResumeOptionsSnapshot>, resume_agents_on_restore: bool, ) -> Option { if !resume_agents_on_restore { return None; } let persisted = persisted_agent_session_from_snapshot(session)?; - crate::agent_resume::plan(&session.source, &session.agent, &persisted.session_ref) + let options = resume_options_for_session(resume_options, Some(session)) + .and_then(|options| { + crate::detect::parse_canonical_agent_label(&options.agent).map(|agent| (agent, options)) + }) + .map(|(agent, options)| { + crate::detect::manifest::filter_resume_options(agent, &options.options) + }) + .unwrap_or_default(); + let mut plan = + crate::agent_resume::plan(&session.source, &session.agent, &persisted.session_ref)?; + plan.argv.extend(options); + Some(plan) } fn persisted_agent_session_from_snapshot( @@ -819,7 +853,7 @@ fn take_restore_plan_for_snapshot( resume_agents_on_restore: bool, resumed_agent_sessions: &mut HashSet, ) -> Option { - restore_plan_for_snapshot(session, resume_agents_on_restore) + restore_plan_for_snapshot(session, None, resume_agents_on_restore) .filter(|plan| resumed_agent_sessions.insert(plan.dedupe_key.clone())) } @@ -1019,9 +1053,11 @@ mod tests { value: pi_session_path.clone(), }; - assert!(restore_plan_for_snapshot(&session, false).is_none()); + assert!(restore_plan_for_snapshot(&session, None, false).is_none()); assert_eq!( - restore_plan_for_snapshot(&session, true).unwrap().argv, + restore_plan_for_snapshot(&session, None, true) + .unwrap() + .argv, vec!["pi", "--session", pi_session_path.as_str()] ); @@ -1031,7 +1067,99 @@ mod tests { kind: crate::agent_resume::AgentSessionRefKind::Path, value: test_session_path("claude-session"), }; - assert!(restore_plan_for_snapshot(&unsupported_path, true).is_none()); + assert!(restore_plan_for_snapshot(&unsupported_path, None, true).is_none()); + } + + #[test] + fn restore_plan_revalidates_cached_options_with_the_active_manifest() { + let session = super::super::snapshot::PaneAgentSessionSnapshot { + source: "herdr:claude".into(), + agent: "claude".into(), + kind: crate::agent_resume::AgentSessionRefKind::Id, + value: "session-id".into(), + }; + let options = super::super::snapshot::PaneAgentResumeOptionsSnapshot { + agent: "claude".into(), + options: vec![ + "--dangerously-skip-permissions".into(), + "--model".into(), + "opus".into(), + "--print".into(), + "captured prompt".into(), + ], + }; + + assert_eq!( + restore_plan_for_snapshot(&session, Some(&options), true) + .unwrap() + .argv, + vec![ + "claude", + "--resume", + "session-id", + "--dangerously-skip-permissions", + "--model", + "opus", + ] + ); + + let mismatched = super::super::snapshot::PaneAgentResumeOptionsSnapshot { + agent: "codex".into(), + options: vec!["--full-auto".into()], + }; + assert_eq!( + restore_plan_for_snapshot(&session, Some(&mismatched), true) + .unwrap() + .argv, + vec!["claude", "--resume", "session-id"] + ); + } + + #[test] + fn detected_option_value_survives_persistence_and_restore_planning() { + let job = crate::platform::ForegroundJob { + process_group_id: 42, + processes: vec![crate::platform::ForegroundProcess { + pid: 42, + name: "claude".into(), + argv0: Some("claude".into()), + argv: Some(vec![ + "claude".into(), + "--name".into(), + "worker seven's".into(), + ]), + cmdline: None, + }], + }; + let detected = crate::detect::resume_options_in_job(&job, crate::detect::Agent::Claude) + .expect("claude argv should be readable"); + assert_eq!(detected, ["--name", "worker seven's"]); + + let persisted = super::super::snapshot::PaneAgentResumeOptionsSnapshot { + agent: "claude".into(), + options: detected, + }; + let encoded = serde_json::to_string(&persisted).unwrap(); + let restored = serde_json::from_str(&encoded).unwrap(); + let session = super::super::snapshot::PaneAgentSessionSnapshot { + source: "herdr:claude".into(), + agent: "claude".into(), + kind: crate::agent_resume::AgentSessionRefKind::Id, + value: "session-id".into(), + }; + + assert_eq!( + restore_plan_for_snapshot(&session, Some(&restored), true) + .unwrap() + .argv, + [ + "claude", + "--resume", + "session-id", + "--name", + "worker seven's", + ] + ); } #[test] @@ -1075,7 +1203,8 @@ mod tests { resumed_sessions: &mut resumed, }; - let startup = pane_restore_startup(Some(&session), Some(&history), &mut agent_restore); + let startup = + pane_restore_startup(Some(&session), None, Some(&history), &mut agent_restore); assert!(startup.restore_plan.is_some()); assert!(startup.initial_history_ansi.is_none()); @@ -1100,8 +1229,9 @@ mod tests { resumed_sessions: &mut resumed, }; - let first = pane_restore_startup(Some(&session), Some(&history), &mut agent_restore); - let duplicate = pane_restore_startup(Some(&session), Some(&history), &mut agent_restore); + let first = pane_restore_startup(Some(&session), None, Some(&history), &mut agent_restore); + let duplicate = + pane_restore_startup(Some(&session), None, Some(&history), &mut agent_restore); assert!(first.restore_plan.is_some()); assert!(first.initial_history_ansi.is_none()); @@ -1128,7 +1258,8 @@ mod tests { resumed_sessions: &mut resumed, }; - let startup = pane_restore_startup(Some(&session), Some(&history), &mut agent_restore); + let startup = + pane_restore_startup(Some(&session), None, Some(&history), &mut agent_restore); assert!(startup.restore_plan.is_none()); assert_eq!(startup.initial_history_ansi, Some("RESTORED_HISTORY\r\n")); @@ -1197,6 +1328,7 @@ mod tests { kind: crate::agent_resume::AgentSessionRefKind::Id, value: "opencode-session".into(), }), + agent_resume_options: None, launch_argv: None, }, )]), @@ -1278,6 +1410,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ), @@ -1289,6 +1422,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ), @@ -1342,6 +1476,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ) @@ -1357,6 +1492,7 @@ mod tests { kind: crate::agent_resume::AgentSessionRefKind::Id, value: "codex-session".into(), }), + agent_resume_options: None, launch_argv: None, }; let snapshot = SessionSnapshot { @@ -1508,6 +1644,7 @@ mod tests { kind: crate::agent_resume::AgentSessionRefKind::Id, value: "codex-session".into(), }), + agent_resume_options: None, launch_argv: None, }, )]), @@ -1669,6 +1806,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ); diff --git a/src/persist/snapshot.rs b/src/persist/snapshot.rs index 39f790ddda..c2c0991dd5 100644 --- a/src/persist/snapshot.rs +++ b/src/persist/snapshot.rs @@ -106,9 +106,17 @@ pub struct PaneSnapshot { #[serde(default, skip_serializing_if = "Option::is_none")] pub agent_session: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_resume_options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub launch_argv: Option>, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaneAgentResumeOptionsSnapshot { + pub agent: String, + pub options: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PaneAgentSessionSnapshot { pub source: String, @@ -338,6 +346,16 @@ fn capture_tab( }) .unwrap_or_default(); let launch_argv = terminal.and_then(|terminal| terminal.launch_argv.clone()); + let agent_resume_options = terminal.and_then(|terminal| { + terminal + .agent_resume_options + .as_ref() + .filter(|resume| !resume.options.is_empty()) + .map(|resume| PaneAgentResumeOptionsSnapshot { + agent: crate::detect::agent_label(resume.agent).to_string(), + options: resume.options.clone(), + }) + }); let agent_session = terminal.and_then(|terminal| { if let Some(authority) = terminal.hook_authority.as_ref() { if let Some(session_ref) = authority.session_ref.as_ref() { @@ -367,6 +385,7 @@ fn capture_tab( agent_name, managed_agent_kind, agent_session, + agent_resume_options, launch_argv, }, ); @@ -647,6 +666,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ); @@ -658,6 +678,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ); @@ -1099,6 +1120,31 @@ mod tests { assert!(second_history.ansi.contains("second-pane-history")); } + #[test] + fn capture_contract_tracks_filtered_agent_resume_options() { + let mut state = state_with_workspaces(&["one"]); + let root = state.workspaces[0].tabs[0].root_pane; + let terminal_id = state.workspaces[0].tabs[0].panes[&root] + .attached_terminal_id + .clone(); + state + .terminals + .get_mut(&terminal_id) + .unwrap() + .set_agent_resume_options( + crate::detect::Agent::Claude, + vec!["--model".into(), "opus".into()], + ); + + let snapshot = capture_from_state(&state); + let options = snapshot.workspaces[0].tabs[0].panes[&root.raw()] + .agent_resume_options + .as_ref() + .unwrap(); + assert_eq!(options.agent, "claude"); + assert_eq!(options.options, ["--model", "opus"]); + } + #[test] fn capture_contract_tracks_hook_authority_agent_session() { let mut state = state_with_workspaces(&["one"]); @@ -1206,6 +1252,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ); @@ -1219,6 +1266,7 @@ mod tests { agent_name: None, managed_agent_kind: None, agent_session: None, + agent_resume_options: None, launch_argv: None, }, ); diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 5c659eb9f4..fcfa2b97e7 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -106,6 +106,12 @@ struct AgentNameOwner { session_ref: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentResumeOptions { + pub agent: Agent, + pub options: Vec, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct RecentAgentProcessExit { agent: Agent, @@ -128,6 +134,7 @@ pub struct TerminalState { pub agent_metadata: HashMap, pub metadata_tokens: crate::metadata_tokens::MetadataTokens, pub persisted_agent_session: Option, + pub agent_resume_options: Option, pub terminal_title: Option, pub manual_label: Option, pub agent_name: Option, @@ -162,6 +169,7 @@ impl TerminalState { agent_metadata: HashMap::new(), metadata_tokens: crate::metadata_tokens::MetadataTokens::default(), persisted_agent_session: None, + agent_resume_options: None, terminal_title: None, manual_label: None, agent_name: None, @@ -184,6 +192,15 @@ impl TerminalState { } } + pub fn set_agent_resume_options(&mut self, agent: Agent, options: Vec) -> bool { + let observed = AgentResumeOptions { agent, options }; + if self.agent_resume_options.as_ref() == Some(&observed) { + return false; + } + self.agent_resume_options = Some(observed); + true + } + pub fn set_detected_agent_process_at( &mut self, agent: Agent, @@ -1587,6 +1604,13 @@ impl TerminalState { self.hook_authority = None; } self.reconcile_agent_name_owner(&agent_label, Some(&session_ref)); + if self + .agent_resume_options + .as_ref() + .is_some_and(|options| crate::detect::agent_label(options.agent) != agent_label) + { + self.agent_resume_options = None; + } self.persisted_agent_session = Some(crate::agent_resume::PersistedAgentSession { source, agent: agent_label, @@ -2053,6 +2077,7 @@ impl TerminalState { self.fallback_observed_at = None; self.hook_authority = None; self.persisted_agent_session = None; + self.agent_resume_options = None; self.agent_metadata.clear(); self.metadata_report_agents.clear(); self.suppressed_full_lifecycle_hook_reports.clear(); @@ -5857,6 +5882,16 @@ mod tests { assert!(terminal.hook_authority.is_some()); } + #[test] + fn agent_resume_options_cache_deduplicates_the_persisted_payload() { + let mut terminal = test_terminal(); + assert!(terminal + .set_agent_resume_options(Agent::Claude, vec!["--model".into(), "opus".into()],)); + assert!(!terminal + .set_agent_resume_options(Agent::Claude, vec!["--model".into(), "opus".into()],)); + assert!(terminal.set_agent_resume_options(Agent::Codex, Vec::new())); + } + #[test] fn same_sequence_from_different_sources_is_independent() { let mut terminal = test_terminal(); diff --git a/website/agent-detection/claude.toml b/website/agent-detection/claude.toml index 696a197890..0712dcdd08 100644 --- a/website/agent-detection/claude.toml +++ b/website/agent-detection/claude.toml @@ -1,9 +1,30 @@ id = "claude" version = "2026.08.21.1" -min_engine_version = 2 +min_engine_version = 4 updated_at = "2026-08-21T00:00:00Z" aliases = ["claude-code"] +[resume_options] +flags = [ + "--dangerously-skip-permissions", + "--allow-dangerously-skip-permissions", + "--bare", + "--safe-mode", + "--chrome", + "--no-chrome", + "--disable-slash-commands", + "--ide", + "--verbose", +] +options = [ + "--agent", + "--autocompact", + "--effort", + "--model", + "--name", + "--permission-mode", +] + [[rules]] id = "osc_title_working" state = "working" diff --git a/website/agent-detection/codex.toml b/website/agent-detection/codex.toml index 9169e10848..ebc3833d16 100644 --- a/website/agent-detection/codex.toml +++ b/website/agent-detection/codex.toml @@ -1,7 +1,27 @@ id = "codex" -version = "2026.08.09.1" -min_engine_version = 3 -updated_at = "2026-08-09T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" + +[resume_options] +flags = [ + "--approve-for-me", + "--dangerously-bypass-approvals-and-sandbox", + "--dangerously-bypass-hook-trust", + "--oss", + "--search", + "--strict-config", + "--no-alt-screen", +] +options = [ + "-m", + "--model", + "-s", + "--sandbox", + "-a", + "--ask-for-approval", + "--local-provider", +] [[rules]] id = "osc_title_blocked" diff --git a/website/agent-detection/cursor.toml b/website/agent-detection/cursor.toml index ee03e6db9d..94f9d552fb 100644 --- a/website/agent-detection/cursor.toml +++ b/website/agent-detection/cursor.toml @@ -1,9 +1,13 @@ id = "cursor" -version = "2026.08.03.1" -min_engine_version = 1 -updated_at = "2026-08-03T01:08:04Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["cursor-agent"] +[resume_options] +flags = ["-f", "--force", "--yolo", "--auto-review", "--trust"] +options = ["--model", "--mode", "--sandbox"] + [[rules]] id = "write_file_approval" state = "blocked" diff --git a/website/agent-detection/droid.toml b/website/agent-detection/droid.toml index c41d71b43b..0d4f008fca 100644 --- a/website/agent-detection/droid.toml +++ b/website/agent-detection/droid.toml @@ -1,7 +1,10 @@ id = "droid" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" + +[resume_options] +options = ["--auto"] [[rules]] id = "execute_selection_blocker" diff --git a/website/agent-detection/github-copilot.toml b/website/agent-detection/github-copilot.toml index 4233c563eb..016f9baa85 100644 --- a/website/agent-detection/github-copilot.toml +++ b/website/agent-detection/github-copilot.toml @@ -1,9 +1,36 @@ id = "copilot" -version = "2026.07.07.1" -min_engine_version = 1 -updated_at = "2026-07-07T14:15:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["github-copilot", "ghcs"] +[resume_options] +flags = [ + "--allow-all", + "--allow-all-tools", + "--allow-all-urls", + "--autopilot", + "--plan", + "--disallow-temp-dir", + "--enable-memory", + "--enable-reasoning-summaries", + "--experimental", + "--no-experimental", + "--no-ask-user", + "--no-custom-instructions", + "--yolo", +] +options = [ + "--agent", + "--effort", + "--reasoning-effort", + "--max-ai-credits", + "--max-autopilot-continues", + "--mode", + "--model", + "--stream", +] + [[rules]] id = "selection_blocker" state = "blocked" diff --git a/website/agent-detection/hermes.toml b/website/agent-detection/hermes.toml index 1754218497..bd4bf6f10d 100644 --- a/website/agent-detection/hermes.toml +++ b/website/agent-detection/hermes.toml @@ -1,9 +1,23 @@ id = "hermes" -version = "2026.07.24.1" -min_engine_version = 2 -updated_at = "2026-07-24T19:12:54Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["hermes-agent"] +[resume_options] +flags = [ + "--yolo", + "--accept-hooks", + "--ignore-user-config", + "--ignore-rules", + "--safe-mode", + "--no-restore-cwd", + "--pass-session-id", + "--tui", + "--cli", +] +options = ["-m", "--model", "--provider", "-t", "--toolsets", "--skills", "-s"] + [[rules]] id = "osc_title_blocked" state = "blocked" diff --git a/website/agent-detection/index.toml b/website/agent-detection/index.toml index f44a83065a..36d3045c91 100644 --- a/website/agent-detection/index.toml +++ b/website/agent-detection/index.toml @@ -60,6 +60,14 @@ path = "kiro.toml" id = "maki" path = "maki.toml" +[[agents]] +id = "muse" +path = "muse.toml" + +[[agents]] +id = "omp" +path = "omp.toml" + [[agents]] id = "opencode" path = "opencode.toml" diff --git a/website/agent-detection/kimi.toml b/website/agent-detection/kimi.toml index b4d0100fba..11555d1725 100644 --- a/website/agent-detection/kimi.toml +++ b/website/agent-detection/kimi.toml @@ -1,9 +1,13 @@ id = "kimi" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["kimi-code", "kimi code"] +[resume_options] +flags = ["-y", "--yolo", "--auto", "--plan"] +options = ["-m", "--model", "--agent"] + [[rules]] id = "current_approval_panel" state = "blocked" diff --git a/website/agent-detection/muse.toml b/website/agent-detection/muse.toml new file mode 100644 index 0000000000..50818422c3 --- /dev/null +++ b/website/agent-detection/muse.toml @@ -0,0 +1,113 @@ +id = "muse" +version = "2026.08.26.1" +min_engine_version = 2 +updated_at = "2026-08-26T00:00:00Z" +aliases = ["muse-code", "muse-cli"] + +# Evidence: live bottom-buffer captures from Muse Code 0.2.1 in Herdr, using a local +# deterministic Responses provider so each UI state could be held and inspected. +# +# Idle has a `⟩` prompt and `model · effort · cwd` footer. Active turns show +# `◆ Working (... · esc to interrupt)` or another activity label with the same interrupt hint. +# +# Structured questions show two co-occurring footer controls: +# Enter to select · ↑/↓ to move · Tab for an optional note · Esc to interrupt +# Multi-select uses `Enter to toggle` instead. The paired controls distinguish a real picker +# from ordinary transcript text that happens to mention one action. +# +# First launch in an untrusted directory shows `Do you trust this workspace?` together with +# `Trust and continue`. This is a real blocker. User-opened `/theme` and `/skills` menus are +# not blockers; their paired footer controls identify overlays whose prior state must be kept. +# +# Muse 0.2.1 command approval shows `Allow this stage once` together with +# `Always allow in this workspace`. Muse 0.1 used `Allow once` with +# `Allow for this session`. Network approval shows `Yes, proceed` together with +# `Yes, don't ask again this session`. Each approval rule requires a pair because Muse can +# emit any one of these phrases as ordinary assistant text after a completed turn. + +[[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(8)" +visible_blocker = true +any = [ + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, +] + +[[rules]] +id = "menu_overlay" +state = "unknown" +priority = 940 +region = "bottom_non_empty_lines(8)" +skip_state_update = true +any = [ + { contains = ["enter confirm", "esc go back"] }, + { contains = ["enter save", "esc go back"] }, + { contains = ["space toggle", "esc close", "type filter"] }, +] + +[[rules]] +id = "working_esc_interrupt" +state = "working" +priority = 900 +region = "bottom_non_empty_lines(8)" +visible_working = true +contains = ["esc to interrupt"] +not = [ + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, +] + +[[rules]] +id = "blocked_approval" +state = "blocked" +priority = 850 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +any = [ + { contains = ["Allow this stage once", "Always allow in this workspace"] }, + { contains = ["Allow once", "Allow for this session"] }, + { contains = ["Yes, proceed", "Yes, don't ask again this session"] }, +] + +[[rules]] +id = "idle_prompt" +state = "idle" +priority = 700 +region = "bottom_non_empty_lines(5)" +visible_idle = true +any = [ + { line_regex = ['^\s*⟩\s*$'] }, + { line_regex = ['^\s*⟩\s+\S'] }, +] +not = [ + { contains = ["esc to interrupt"] }, + { contains = ["Enter to select", "Tab for an optional note"] }, + { contains = ["Enter to toggle", "Esc to interrupt"] }, + { contains = ["enter confirm", "esc go back"] }, + { contains = ["enter save", "esc go back"] }, + { contains = ["space toggle", "esc close", "type filter"] }, +] + +[[rules]] +id = "idle_status_fallback" +state = "idle" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_idle = true +line_regex = ['^\s*\S+ · (none|minimal|low|medium|high|xhigh|ultra) · '] +not = [{ contains = ["esc to interrupt"] }] diff --git a/website/agent-detection/omp.toml b/website/agent-detection/omp.toml new file mode 100644 index 0000000000..1a76f6c72b --- /dev/null +++ b/website/agent-detection/omp.toml @@ -0,0 +1,33 @@ +id = "omp" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" +aliases = ["oh-my-pi"] + +[resume_options] +flags = [ + "--prewalk", + "--no-prewalk", + "--plan-yolo", + "--allow-home", + "--no-tools", + "--no-lsp", + "--no-pty", + "--hide-thinking", + "--advisor", + "--no-extensions", + "--no-skills", + "--no-rules", + "--no-title", + "--auto-approve", +] +options = [ + "--model", + "--smol", + "--slow", + "--plan", + "--provider", + "--thinking", + "--max-time", + "--approval-mode", +] diff --git a/website/agent-detection/opencode.toml b/website/agent-detection/opencode.toml index 5245238371..2303508083 100644 --- a/website/agent-detection/opencode.toml +++ b/website/agent-detection/opencode.toml @@ -1,9 +1,13 @@ id = "opencode" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["open-code", "herdr:opencode"] +[resume_options] +flags = ["--pure", "--auto", "--mini", "--no-replay"] +options = ["-m", "--model", "--agent", "--replay-limit"] + [[rules]] id = "permission_required" state = "blocked" diff --git a/website/agent-detection/pi.toml b/website/agent-detection/pi.toml index a58d30e903..19cfbfcfe3 100644 --- a/website/agent-detection/pi.toml +++ b/website/agent-detection/pi.toml @@ -1,9 +1,34 @@ id = "pi" -version = "2026.06.10.1" -min_engine_version = 1 -updated_at = "2026-06-10T00:00:00Z" +version = "2026.08.12.1" +min_engine_version = 4 +updated_at = "2026-08-12T14:05:03Z" aliases = ["herdr:pi"] +[resume_options] +flags = [ + "--no-tools", + "-nt", + "--no-builtin-tools", + "-nbt", + "--no-extensions", + "-ne", + "--no-skills", + "-ns", + "--no-context-files", + "-nc", + "--approve", + "-a", + "--no-approve", + "-na", + "--offline", +] +options = [ + "--provider", + "--model", + "--thinking", + "--tui-mode", +] + [[rules]] id = "working_literal" state = "working"