From d1990cca47409df1a4a99069e3ea4bd83f496cb7 Mon Sep 17 00:00:00 2001 From: CodeWhale Bot Date: Wed, 2 Sep 2026 11:43:05 -0700 Subject: [PATCH 1/3] tui: delete AppMode::Auto and its CommandMode mirror Audit slice: dead/pretender mode abstractions (item 1). AppMode::Auto had no user spelling: parse() maps "auto" to Agent and the variant was never in the Tab cycle (crates/config/src/app_mode.rs). Only tests constructed it directly. Delete the variant and drop the defensive Auto arms from every exhaustive match in codewhale-tui, plus the init comment that explained the ghost (crates/tui/src/tui/app/init.rs). command-contract's CommandMode::Auto existed only as the adapter mirror of the dead variant (crates/tui/src/commands/contract.rs); remove the mirror and its adapter arms. CommandMode has no consumers outside the tui adapter, so the boundary shape stays otherwise untouched. Tests updated (not weakened): the Auto assertions in app_mode helper and base_policy_for_mode tests, the cycle_mode_reverse Auto leg, the protocol_parity mode-label round trip, the command-contract boundary mapping test, and the engine mode-invariant matrix "auto-compat" case (behavior identical to the remaining "agent" case). Gates: cargo fmt --check; nextest codewhale-tui 11909 passed; codewhale-config 639 passed; codewhale-command-contract 29 passed; dead-code budget PASS 425; vocabulary gate exit 0. --- crates/command-contract/src/types.rs | 1 - crates/config/src/app_mode.rs | 14 +++----------- crates/tui/src/commands/contract.rs | 3 --- crates/tui/src/commands/groups/core/core.rs | 3 +-- crates/tui/src/config_ui.rs | 2 +- crates/tui/src/core/authority.rs | 7 +++---- crates/tui/src/core/engine/tests.rs | 12 +----------- crates/tui/src/core/protocol_parity.rs | 15 +++------------ crates/tui/src/runtime_policy.rs | 2 +- crates/tui/src/tui/app/init.rs | 6 ++---- crates/tui/src/tui/app/tests.rs | 17 ----------------- crates/tui/src/tui/app/types.rs | 4 ++-- crates/tui/src/tui/hotbar/actions.rs | 6 ------ crates/tui/src/tui/underwater.rs | 4 ++-- crates/tui/src/tui/widgets/mod.rs | 2 +- 15 files changed, 20 insertions(+), 78 deletions(-) diff --git a/crates/command-contract/src/types.rs b/crates/command-contract/src/types.rs index 9f54b6aa52..dc85ceec88 100644 --- a/crates/command-contract/src/types.rs +++ b/crates/command-contract/src/types.rs @@ -27,7 +27,6 @@ pub enum CommandReasoningEffort { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommandMode { Agent, - Auto, Yolo, Plan, Operate, diff --git a/crates/config/src/app_mode.rs b/crates/config/src/app_mode.rs index 4fbf33fa7e..f249b164e4 100644 --- a/crates/config/src/app_mode.rs +++ b/crates/config/src/app_mode.rs @@ -7,7 +7,6 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppMode { Agent, - Auto, /// Legacy compatibility alias; resolves to [`Self::Agent`] + bypass approvals. Yolo, Plan, @@ -17,8 +16,6 @@ pub enum AppMode { impl AppMode { /// Productive keyboard cycle: Plan -> Act -> Operate -> Plan. /// - /// `Auto` remains an internal variant while the real implementation is - /// redesigned; do not expose it through user-facing mode selection (#3733). /// `Yolo` is kept for parse/back-compat only and is not in the Tab cycle. /// Operate joins the visible cycle as the always-on pod operation: /// a lead plans slices, then workers execute against an optional burn rate. @@ -51,7 +48,6 @@ impl AppMode { pub fn as_setting(self) -> &'static str { match self { Self::Agent => "agent", - Self::Auto => "agent", // Write current permission vocabulary, not the legacy YOLO label. Self::Yolo => "agent", Self::Plan => "plan", @@ -63,7 +59,6 @@ impl AppMode { pub fn label(self) -> &'static str { match self { AppMode::Agent => "ACT", - AppMode::Auto => "ACT", AppMode::Yolo => "ACT", AppMode::Plan => "PLAN", AppMode::Operate => "OPERATE", @@ -74,7 +69,6 @@ impl AppMode { pub fn display_name(self) -> &'static str { match self { AppMode::Agent => "Act", - AppMode::Auto => "Act", AppMode::Yolo => "Act", AppMode::Plan => "Plan", AppMode::Operate => "Operate", @@ -84,7 +78,7 @@ impl AppMode { #[must_use] pub fn number(self) -> char { match self { - AppMode::Agent | AppMode::Auto | AppMode::Yolo => '1', + AppMode::Agent | AppMode::Yolo => '1', AppMode::Plan => '2', AppMode::Operate => '3', } @@ -92,7 +86,7 @@ impl AppMode { #[must_use] pub fn uses_agent_baseline(self) -> bool { - matches!(self, Self::Agent | Self::Auto | Self::Operate) + matches!(self, Self::Agent | Self::Operate) } /// Operate gets a higher parallel launch floor so background fan-out is @@ -108,9 +102,7 @@ impl AppMode { /// Description shown in help or onboarding text. pub fn description(self) -> &'static str { match self { - AppMode::Agent | AppMode::Auto => { - "Act mode - direct work in the current session with tools" - } + AppMode::Agent => "Act mode - direct work in the current session with tools", AppMode::Yolo => "Act mode with Full Access (legacy compatibility setting)", AppMode::Plan => "Plan mode - research and design before implementing", AppMode::Operate => { diff --git a/crates/tui/src/commands/contract.rs b/crates/tui/src/commands/contract.rs index 054c6c840d..ceabfaf922 100644 --- a/crates/tui/src/commands/contract.rs +++ b/crates/tui/src/commands/contract.rs @@ -81,7 +81,6 @@ pub(crate) const PENDING_GROUPS: &[&str] = pub(crate) fn to_command_mode(mode: AppMode) -> CommandMode { match mode { AppMode::Agent => CommandMode::Agent, - AppMode::Auto => CommandMode::Auto, AppMode::Yolo => CommandMode::Yolo, AppMode::Plan => CommandMode::Plan, AppMode::Operate => CommandMode::Operate, @@ -91,7 +90,6 @@ pub(crate) fn to_command_mode(mode: AppMode) -> CommandMode { fn from_command_mode(mode: CommandMode) -> AppMode { match mode { CommandMode::Agent => AppMode::Agent, - CommandMode::Auto => AppMode::Auto, CommandMode::Yolo => AppMode::Yolo, CommandMode::Plan => AppMode::Plan, CommandMode::Operate => AppMode::Operate, @@ -1477,7 +1475,6 @@ mod tests { fn boundary_mappings_cover_every_variant() { for mode in [ AppMode::Agent, - AppMode::Auto, AppMode::Yolo, AppMode::Plan, AppMode::Operate, diff --git a/crates/tui/src/commands/groups/core/core.rs b/crates/tui/src/commands/groups/core/core.rs index e4a0a3bff6..c52c650c63 100644 --- a/crates/tui/src/commands/groups/core/core.rs +++ b/crates/tui/src/commands/groups/core/core.rs @@ -684,7 +684,7 @@ pub fn home_dashboard(app: &mut App) -> CommandResult { let _ = writeln!(stats, "\n{}", tr(locale, MessageId::HomeModeTips)); let _ = writeln!(stats, "--------------------------------------------"); match app.mode { - AppMode::Agent | AppMode::Auto => { + AppMode::Agent => { let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeTip)); let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeReviewTip)); let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeYoloTip)); @@ -1729,7 +1729,6 @@ mod tests { fn test_home_dashboard_mode_tips_for_each_mode() { let modes = [ AppMode::Agent, - AppMode::Auto, AppMode::Yolo, AppMode::Plan, AppMode::Operate, diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index b95c045ed0..b5a4d3b869 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -1401,7 +1401,7 @@ impl From<&str> for DefaultModeValue { other => match AppMode::from_setting(other) { AppMode::Plan => Self::Plan, AppMode::Operate => Self::Operate, - AppMode::Agent | AppMode::Yolo | AppMode::Auto => Self::Agent, + AppMode::Agent | AppMode::Yolo => Self::Agent, }, } } diff --git a/crates/tui/src/core/authority.rs b/crates/tui/src/core/authority.rs index 2a02eb7851..7ace8d09d9 100644 --- a/crates/tui/src/core/authority.rs +++ b/crates/tui/src/core/authority.rs @@ -43,7 +43,6 @@ pub(crate) struct EffectiveModePolicy { /// This is the single source of truth for the mode/permission table: /// - `Plan` -> read-only: no shell, no trust, `Suggest` approvals. /// - `Agent` -> the user's durable baseline (`prefs`). -/// - `Auto` -> compatibility alias for Agent; not a separate behavior. /// - `Operate` -> Agent baseline plus orchestration capabilities in the runtime. /// - `Yolo` -> legacy compat; full authority: shell + trust + `Bypass` approvals. #[must_use] @@ -55,7 +54,7 @@ pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> E trust_mode: false, approval_mode: ApprovalMode::Suggest, }, - AppMode::Agent | AppMode::Auto | AppMode::Operate => EffectiveModePolicy { + AppMode::Agent | AppMode::Operate => EffectiveModePolicy { mode, allow_shell: prefs.agent_allow_shell, trust_mode: prefs.agent_trust_mode, @@ -418,7 +417,7 @@ pub(crate) fn shell_policy_for_mode(mode: AppMode, allow_shell: bool) -> ShellPo } match mode { AppMode::Plan => ShellPolicy::None, - AppMode::Agent | AppMode::Auto | AppMode::Operate | AppMode::Yolo => ShellPolicy::Full, + AppMode::Agent | AppMode::Operate | AppMode::Yolo => ShellPolicy::Full, } } @@ -503,7 +502,7 @@ pub(crate) fn write_carve_out_posture( auto_approve: bool, ) -> bool { !auto_approve - && matches!(mode, AppMode::Agent | AppMode::Auto | AppMode::Operate) + && matches!(mode, AppMode::Agent | AppMode::Operate) && approval_mode == ApprovalMode::Suggest } diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 914af1923a..2e907386fb 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -11151,7 +11151,7 @@ async fn run_shell_command_op_allows_readonly_shell_in_auto_mode() { engine .handle_run_shell_command( "pwd".to_string(), - AppMode::Auto, + AppMode::Agent, true, false, false, @@ -13158,16 +13158,6 @@ fn mode_invariant_matrix_covers_context_catalog_subagents_and_prompt_metadata() approval_mode: ApprovalMode::Bypass, plan_hint: false, }, - ModeCase { - name: "auto-compat", - mode: AppMode::Auto, - shell_policy: ShellPolicy::Full, - sandbox: ExpectedSandbox::WorkspaceWrite, - trust_mode: false, - auto_approve: false, - approval_mode: ApprovalMode::Suggest, - plan_hint: false, - }, ModeCase { name: "operate", mode: AppMode::Operate, diff --git a/crates/tui/src/core/protocol_parity.rs b/crates/tui/src/core/protocol_parity.rs index 3604146187..71279397c0 100644 --- a/crates/tui/src/core/protocol_parity.rs +++ b/crates/tui/src/core/protocol_parity.rs @@ -64,13 +64,12 @@ fn count(value: usize) -> u64 { u64::try_from(value).unwrap_or(u64::MAX) } -/// Lossless mode label. Unlike `AppMode::as_setting`, `auto` and `yolo` keep -/// their identity so a client can round-trip through `AppMode::parse`. +/// Lossless mode label. Unlike `AppMode::as_setting`, `yolo` keeps its +/// identity so a client can round-trip through `AppMode::parse`. #[must_use] pub fn app_mode_str(mode: AppMode) -> &'static str { match mode { AppMode::Agent => "agent", - AppMode::Auto => "auto", AppMode::Yolo => "yolo", AppMode::Plan => "plan", AppMode::Operate => "operate", @@ -1393,19 +1392,11 @@ mod tests { fn mode_labels_round_trip_through_app_mode_parse() { for mode in [ AppMode::Agent, - AppMode::Auto, AppMode::Yolo, AppMode::Plan, AppMode::Operate, ] { - let parsed = AppMode::parse(app_mode_str(mode)); - // `auto` parses back to the visible Agent mode by design. - let expected = if mode == AppMode::Auto { - AppMode::Agent - } else { - mode - }; - assert_eq!(parsed, Some(expected), "{mode:?}"); + assert_eq!(AppMode::parse(app_mode_str(mode)), Some(mode), "{mode:?}"); } for mode in [ ApprovalMode::Auto, diff --git a/crates/tui/src/runtime_policy.rs b/crates/tui/src/runtime_policy.rs index 7f06c47358..28b5310234 100644 --- a/crates/tui/src/runtime_policy.rs +++ b/crates/tui/src/runtime_policy.rs @@ -104,7 +104,7 @@ pub(crate) fn parse_runtime_mode(value: &str) -> Option { #[must_use] fn visible_mode(mode: AppMode) -> AppMode { match mode { - AppMode::Auto | AppMode::Yolo => AppMode::Agent, + AppMode::Yolo => AppMode::Agent, other => other, } } diff --git a/crates/tui/src/tui/app/init.rs b/crates/tui/src/tui/app/init.rs index 4a79246d16..30a44f6c67 100644 --- a/crates/tui/src/tui/app/init.rs +++ b/crates/tui/src/tui/app/init.rs @@ -512,10 +512,8 @@ impl App { }; // Durable Agent-era permission baseline (#3386). Plan/YOLO derive from - // and restore to this. Legacy Auto inputs parse to Agent; if an older - // caller still constructs `AppMode::Auto` directly, it projects through - // the Agent baseline instead of enabling a fourth runtime posture. When - // the user starts in YOLO the live shell flag is force-enabled below, so + // and restore to this. When the user starts in YOLO the live shell + // flag is force-enabled below, so // the baseline shell value is taken from the interactive default (the // pre-mode Agent surface) rather than the YOLO-forced live mirror; // otherwise it mirrors the resolved `allow_shell` option, which already diff --git a/crates/tui/src/tui/app/tests.rs b/crates/tui/src/tui/app/tests.rs index fc8fa16eb0..8e6625a6a7 100644 --- a/crates/tui/src/tui/app/tests.rs +++ b/crates/tui/src/tui/app/tests.rs @@ -3385,15 +3385,11 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::from_setting("5"), AppMode::Operate); assert_eq!(AppMode::Agent.as_setting(), "agent"); - assert_eq!(AppMode::Auto.as_setting(), "agent"); assert_eq!(AppMode::Yolo.as_setting(), "agent"); assert_eq!(AppMode::Plan.display_name(), "Plan"); - assert_eq!(AppMode::Auto.display_name(), "Act"); - assert_eq!(AppMode::Auto.label(), "ACT"); assert_eq!(AppMode::Yolo.label(), "ACT"); assert_eq!(AppMode::Yolo.display_name(), "Act"); assert_eq!(AppMode::Agent.number(), '1'); - assert_eq!(AppMode::Auto.number(), '1'); assert_eq!(AppMode::Yolo.number(), '1'); assert_eq!(AppMode::Operate.number(), '3'); assert_eq!( @@ -3404,12 +3400,10 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::Plan.next(), AppMode::Agent); assert_eq!(AppMode::Agent.next(), AppMode::Operate); assert_eq!(AppMode::Operate.next(), AppMode::Plan); - assert_eq!(AppMode::Auto.next(), AppMode::Agent); assert_eq!(AppMode::Yolo.next(), AppMode::Agent); assert_eq!(AppMode::Plan.previous(), AppMode::Operate); assert_eq!(AppMode::Agent.previous(), AppMode::Plan); assert_eq!(AppMode::Operate.previous(), AppMode::Agent); - assert_eq!(AppMode::Auto.previous(), AppMode::Agent); assert_eq!(AppMode::Yolo.previous(), AppMode::Agent); } @@ -3456,10 +3450,6 @@ fn test_cycle_mode_reverse_transitions() { app.mode = AppMode::Agent; app.cycle_mode_reverse(); assert_eq!(app.mode, AppMode::Plan); - - app.mode = AppMode::Auto; - app.cycle_mode_reverse(); - assert_eq!(app.mode, AppMode::Agent); } #[test] @@ -3660,13 +3650,6 @@ fn base_policy_for_mode_projects_the_mode_permission_table() { assert!(agent.trust_mode); assert_eq!(agent.approval_mode, ApprovalMode::Never); - // Auto: compatibility alias for the durable Agent baseline. - let auto = base_policy_for_mode(AppMode::Auto, &prefs); - assert_eq!(auto.mode, AppMode::Auto); - assert!(auto.allow_shell); - assert!(auto.trust_mode); - assert_eq!(auto.approval_mode, ApprovalMode::Never); - // Operate uses the Agent baseline. let operate = base_policy_for_mode(AppMode::Operate, &prefs); assert_eq!(operate.mode, AppMode::Operate); diff --git a/crates/tui/src/tui/app/types.rs b/crates/tui/src/tui/app/types.rs index 248d50cd7a..e24bdabbba 100644 --- a/crates/tui/src/tui/app/types.rs +++ b/crates/tui/src/tui/app/types.rs @@ -63,7 +63,7 @@ impl AppModeUi for AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgent, + AppMode::Agent | AppMode::Yolo => MessageId::AppModeAgent, AppMode::Plan => MessageId::AppModePlan, AppMode::Operate => MessageId::AppModeOperate, }, @@ -75,7 +75,7 @@ impl AppModeUi for AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgentHint, + AppMode::Agent | AppMode::Yolo => MessageId::AppModeAgentHint, AppMode::Plan => MessageId::AppModePlanHint, AppMode::Operate => MessageId::AppModeOperateHint, }, diff --git a/crates/tui/src/tui/hotbar/actions.rs b/crates/tui/src/tui/hotbar/actions.rs index 9eb3f3e4e6..0a623716b7 100644 --- a/crates/tui/src/tui/hotbar/actions.rs +++ b/crates/tui/src/tui/hotbar/actions.rs @@ -914,9 +914,6 @@ impl AppHotbarAction { AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentName, AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloName, AppHotbarKind::Mode(AppMode::Operate) => MessageId::HotbarActionModeOperateName, - AppHotbarKind::Mode(AppMode::Auto) => { - return None; - } AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleName, AppHotbarKind::SidebarToggle => MessageId::HotbarActionSidebarToggleName, AppHotbarKind::FileTreeToggle => MessageId::HotbarActionFileTreeToggleName, @@ -933,9 +930,6 @@ impl AppHotbarAction { AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentDescription, AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloDescription, AppHotbarKind::Mode(AppMode::Operate) => MessageId::HotbarActionModeOperateDescription, - AppHotbarKind::Mode(AppMode::Auto) => { - return None; - } AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleDescription, AppHotbarKind::SidebarToggle => MessageId::HotbarActionSidebarToggleDescription, AppHotbarKind::FileTreeToggle => MessageId::HotbarActionFileTreeToggleDescription, diff --git a/crates/tui/src/tui/underwater.rs b/crates/tui/src/tui/underwater.rs index 0d94e241db..7049355cf1 100644 --- a/crates/tui/src/tui/underwater.rs +++ b/crates/tui/src/tui/underwater.rs @@ -511,7 +511,7 @@ fn header_mode_ink(mode: AppMode) -> ChromeInk { // on a selected mode. It wears the act badge because `mode_label` // resolves it to act; the posture it implies is the permission // chip's Cognition ink, not this one. - AppMode::Agent | AppMode::Auto | AppMode::Yolo => ChromeInk::PolicyAct, + AppMode::Agent | AppMode::Yolo => ChromeInk::PolicyAct, } } @@ -731,7 +731,7 @@ pub(crate) fn phase_marker_with_activity( fn mode_label(locale: Locale, mode: AppMode) -> Cow<'static, str> { match mode { - AppMode::Agent | AppMode::Auto | AppMode::Yolo => tr(locale, MessageId::ChipModeAct), + AppMode::Agent | AppMode::Yolo => tr(locale, MessageId::ChipModeAct), AppMode::Plan => tr(locale, MessageId::ChipModePlan), AppMode::Operate => tr(locale, MessageId::ChipModeOperate), } diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 58145be15d..e5994a3781 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -1342,7 +1342,7 @@ impl<'a> ComposerWidget<'a> { fn mode_color(&self) -> Color { match self.app.mode { - AppMode::Agent | AppMode::Auto | AppMode::Yolo => self.app.ui_theme.mode_agent, + AppMode::Agent | AppMode::Yolo => self.app.ui_theme.mode_agent, AppMode::Plan => self.app.ui_theme.mode_plan, AppMode::Operate => self.app.ui_theme.mode_operate, } From 1e8227a556987978b31817c1f4fc7282f3485545 Mon Sep 17 00:00:00 2001 From: CodeWhale Bot Date: Wed, 2 Sep 2026 12:18:12 -0700 Subject: [PATCH 2/3] =?UTF-8?q?tui:=20finish=20YOLO=20=E2=80=94=20parse=20?= =?UTF-8?q?alias=20only,=20no=20enum=20variant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit slice: dead/pretender mode abstractions (item 2). Delete the AppMode::Yolo variant. parse() keeps accepting the legacy spellings ("yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions") and now returns AppMode::Agent directly: YOLO was a permission change (Full Access + trust + shell), never a mode. The posture split already lives at the edges and is untouched there: settings load rewrites default_mode=yolo into Act + full-access posture (crates/tui/src/settings.rs), the runtime wire reader re-derives Bypass from the raw legacy spelling (crates/tui/src/runtime_policy.rs), and --yolo keeps working through the launch flag plus the new set_mode_yolo_compat elevation. Mode-shaped entry points keep their behavior through the posture: - set_mode loses the Yolo fold; the transient full-access elevation moves to App::set_mode_yolo_compat / App::select_yolo_compat with the same lock refusal, baseline capture, and once-per-install toast. - Alt+Y routes through apply_yolo_compat_update; /mode yolo and /zidong route through the compat path before parse folds the alias. - authority/engine drop the mode==Yolo disjuncts; auto_approve and ApprovalMode::Bypass carry the posture, so sandbox, shell, tool approval, and narrowing behavior are unchanged. CommandMode::Yolo dies with it (no external consumers of the command-contract enum). Tests updated (not weakened): yolo spellings now assert parse -> Agent; set_mode_yolo_compat/select_yolo_compat cover the elevation, baseline restore, locked-policy refusal, and startup-default persistence the variant tests pinned; engine posture tests pass Agent + explicit full-access flags instead of the retired Yolo mode. Gates: cargo fmt --check; nextest codewhale-tui 11908 passed; codewhale-config 639 passed; codewhale-command-contract 29 passed; dead-code budget PASS 425; vocabulary gate exit 0. --- crates/command-contract/src/types.rs | 1 - crates/config/src/app_mode.rs | 17 +-- crates/tui/src/commands/contract.rs | 18 +-- .../tui/src/commands/groups/config/config.rs | 44 +++++- .../tui/src/commands/groups/config/status.rs | 2 +- crates/tui/src/commands/groups/core/core.rs | 12 +- crates/tui/src/config_ui.rs | 2 +- crates/tui/src/core/authority.rs | 75 +++------- crates/tui/src/core/engine.rs | 13 +- crates/tui/src/core/engine/preview.rs | 4 +- crates/tui/src/core/engine/tests.rs | 133 ++++++++---------- .../tui/src/core/engine/tool_catalog/tests.rs | 4 +- crates/tui/src/core/protocol_parity.rs | 11 +- crates/tui/src/exec_agent.rs | 8 +- crates/tui/src/lib.rs | 6 +- crates/tui/src/prompts.rs | 9 +- crates/tui/src/runtime_policy.rs | 23 +-- crates/tui/src/runtime_threads/tests.rs | 7 +- crates/tui/src/tui/app.rs | 131 ++++++++++------- crates/tui/src/tui/app/init.rs | 7 +- crates/tui/src/tui/app/tests.rs | 45 +++--- crates/tui/src/tui/app/types.rs | 4 +- crates/tui/src/tui/hotbar/actions.rs | 2 - crates/tui/src/tui/startup_defaults.rs | 5 - crates/tui/src/tui/ui/apply.rs | 18 +++ crates/tui/src/tui/ui/approval_routing.rs | 6 +- crates/tui/src/tui/ui/event_loop.rs | 4 +- crates/tui/src/tui/ui/tests.rs | 44 +----- crates/tui/src/tui/underwater.rs | 8 +- crates/tui/src/tui/widgets/mod.rs | 2 +- 30 files changed, 309 insertions(+), 356 deletions(-) diff --git a/crates/command-contract/src/types.rs b/crates/command-contract/src/types.rs index dc85ceec88..d730dd43fa 100644 --- a/crates/command-contract/src/types.rs +++ b/crates/command-contract/src/types.rs @@ -27,7 +27,6 @@ pub enum CommandReasoningEffort { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommandMode { Agent, - Yolo, Plan, Operate, } diff --git a/crates/config/src/app_mode.rs b/crates/config/src/app_mode.rs index f249b164e4..7eac994b66 100644 --- a/crates/config/src/app_mode.rs +++ b/crates/config/src/app_mode.rs @@ -7,8 +7,6 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppMode { Agent, - /// Legacy compatibility alias; resolves to [`Self::Agent`] + bypass approvals. - Yolo, Plan, Operate, } @@ -16,7 +14,6 @@ pub enum AppMode { impl AppMode { /// Productive keyboard cycle: Plan -> Act -> Operate -> Plan. /// - /// `Yolo` is kept for parse/back-compat only and is not in the Tab cycle. /// Operate joins the visible cycle as the always-on pod operation: /// a lead plans slices, then workers execute against an optional burn rate. pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate]; @@ -27,9 +24,12 @@ impl AppMode { "agent" | "act" | "work" | "auto" | "1" => Some(Self::Agent), "plan" | "2" => Some(Self::Plan), "operate" | "operation" | "ops" | "3" => Some(Self::Operate), - // Invisible one-way permission shorthand only — never a visible mode. + // Invisible one-way permission shorthand only — never a visible + // mode. These spellings resolve to Act; the bypass posture they + // imply is carried by the permission surface (settings load, + // CLI/runtime wire), not by a mode. "yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" => { - Some(Self::Yolo) + Some(Self::Agent) } _ => None, } @@ -48,8 +48,6 @@ impl AppMode { pub fn as_setting(self) -> &'static str { match self { Self::Agent => "agent", - // Write current permission vocabulary, not the legacy YOLO label. - Self::Yolo => "agent", Self::Plan => "plan", Self::Operate => "operate", } @@ -59,7 +57,6 @@ impl AppMode { pub fn label(self) -> &'static str { match self { AppMode::Agent => "ACT", - AppMode::Yolo => "ACT", AppMode::Plan => "PLAN", AppMode::Operate => "OPERATE", } @@ -69,7 +66,6 @@ impl AppMode { pub fn display_name(self) -> &'static str { match self { AppMode::Agent => "Act", - AppMode::Yolo => "Act", AppMode::Plan => "Plan", AppMode::Operate => "Operate", } @@ -78,7 +74,7 @@ impl AppMode { #[must_use] pub fn number(self) -> char { match self { - AppMode::Agent | AppMode::Yolo => '1', + AppMode::Agent => '1', AppMode::Plan => '2', AppMode::Operate => '3', } @@ -103,7 +99,6 @@ impl AppMode { pub fn description(self) -> &'static str { match self { AppMode::Agent => "Act mode - direct work in the current session with tools", - AppMode::Yolo => "Act mode with Full Access (legacy compatibility setting)", AppMode::Plan => "Plan mode - research and design before implementing", AppMode::Operate => { "Operate mode - always-on pod operation: lead plans, optional $/time burn rate, workers follow the plan" diff --git a/crates/tui/src/commands/contract.rs b/crates/tui/src/commands/contract.rs index ceabfaf922..4b7a216342 100644 --- a/crates/tui/src/commands/contract.rs +++ b/crates/tui/src/commands/contract.rs @@ -81,7 +81,6 @@ pub(crate) const PENDING_GROUPS: &[&str] = pub(crate) fn to_command_mode(mode: AppMode) -> CommandMode { match mode { AppMode::Agent => CommandMode::Agent, - AppMode::Yolo => CommandMode::Yolo, AppMode::Plan => CommandMode::Plan, AppMode::Operate => CommandMode::Operate, } @@ -90,7 +89,6 @@ pub(crate) fn to_command_mode(mode: AppMode) -> CommandMode { fn from_command_mode(mode: CommandMode) -> AppMode { match mode { CommandMode::Agent => AppMode::Agent, - CommandMode::Yolo => AppMode::Yolo, CommandMode::Plan => AppMode::Plan, CommandMode::Operate => AppMode::Operate, } @@ -1473,12 +1471,7 @@ mod tests { #[test] fn boundary_mappings_cover_every_variant() { - for mode in [ - AppMode::Agent, - AppMode::Yolo, - AppMode::Plan, - AppMode::Operate, - ] { + for mode in [AppMode::Agent, AppMode::Plan, AppMode::Operate] { let command = to_command_mode(mode); assert_eq!(from_command_mode(command), mode); } @@ -1610,17 +1603,16 @@ mod tests { let mut bundle = app.command_contexts(); let mut parts = bundle.parts(); let policy = parts.mode_policy.as_mut().expect("mode facet"); + policy.set_mode(CommandMode::Operate); policy.set_shell_access(true); - policy.set_mode(CommandMode::Yolo); assert!(policy.allow_shell()); - assert_eq!(policy.approval_mode(), CommandApprovalMode::Bypass); + assert_eq!(policy.mode(), CommandMode::Operate); } assert_eq!( app.mode, - AppMode::Agent, - "YOLO is an Agent compatibility mode" + AppMode::Operate, + "adapter delegates to App authority" ); - assert!(app.yolo); assert!(app.allow_shell); } diff --git a/crates/tui/src/commands/groups/config/config.rs b/crates/tui/src/commands/groups/config/config.rs index aabf1caad6..a5b600f53b 100644 --- a/crates/tui/src/commands/groups/config/config.rs +++ b/crates/tui/src/commands/groups/config/config.rs @@ -2915,6 +2915,26 @@ pub fn mode(app: &mut App, arg: Option<&str>) -> CommandResult { let Some(arg) = arg.filter(|value| !value.trim().is_empty()) else { return CommandResult::action(AppAction::OpenModePicker); }; + // The legacy YOLO spellings are a one-way permission shorthand, not a + // mode: route them to the full-access compat path before parse folds + // them to Act. + if matches!( + arg.trim().to_ascii_lowercase().as_str(), + "yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" + ) { + let (message, changed) = switch_yolo_compat_with_status(app); + if changed { + CommandResult::with_message_and_action(message, AppAction::ModeChanged(app.mode)) + } else { + CommandResult::message(message) + } + } else { + mode_selection(app, arg) + } +} + +/// `/mode ` for the real modes (Plan/Act/Operate). +fn mode_selection(app: &mut App, arg: &str) -> CommandResult { match AppMode::parse(arg) { Some(mode) => { let (message, changed) = switch_mode_with_status(app, mode); @@ -2951,6 +2971,24 @@ fn switch_mode_with_status(app: &mut App, mode: AppMode) -> (String, bool) { } } +/// Status for the legacy YOLO alias: user-facing copy says Act, because the +/// alias is invisible Act + Full Access. +fn switch_yolo_compat_with_status(app: &mut App) -> (String, bool) { + match app.select_yolo_compat() { + SettingSelection::Changed => ( + format!("Switched to {} mode.", AppMode::Agent.display_name()), + true, + ), + SettingSelection::PersistedSame => { + (app.mode_startup_default_receipt(AppMode::Agent), false) + } + SettingSelection::Refused => ( + app.setting_locked_message(MessageId::SettingSubjectMode), + false, + ), + } +} + /// `/theme [name]` — with no argument, open the interactive picker (arrow /// keys, live preview, Enter to persist, Esc to revert). With an argument, /// route through `set_config_value("theme", ...)` so the apply + save flow is @@ -3668,7 +3706,7 @@ mod tests { let result = mode(&mut app, Some("yolo")); // YOLO is invisible Act+Bypass shorthand — user-facing copy says Act. assert!(result.message.unwrap().contains("Switched to Act mode")); - assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Yolo))); + assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Agent))); assert!(app.allow_shell); assert!(app.trust_mode); assert!(app.yolo); @@ -3703,9 +3741,9 @@ mod tests { assert!(result.is_error); assert_eq!(app.mode, AppMode::Operate); let result = mode(&mut app, Some("4")); - assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Yolo))); - // "4" still parses as the deprecated YOLO alias, which lands in Agent + // "4" still routes to the deprecated YOLO alias, which lands in Agent // mode with bypass approvals (M6 compat shim). + assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Agent))); assert_eq!(app.mode, AppMode::Agent); assert!(app.yolo); } diff --git a/crates/tui/src/commands/groups/config/status.rs b/crates/tui/src/commands/groups/config/status.rs index b869deca21..5a2ba549c6 100644 --- a/crates/tui/src/commands/groups/config/status.rs +++ b/crates/tui/src/commands/groups/config/status.rs @@ -759,7 +759,7 @@ mod tests { } app.configured_sandbox_mode = None; - app.mode = AppMode::Yolo; + app.mode = AppMode::Agent; let yolo = format_status(&app); assert!(yolo.contains("sandbox disabled, network unrestricted")); } diff --git a/crates/tui/src/commands/groups/core/core.rs b/crates/tui/src/commands/groups/core/core.rs index c52c650c63..3a55454e46 100644 --- a/crates/tui/src/commands/groups/core/core.rs +++ b/crates/tui/src/commands/groups/core/core.rs @@ -689,11 +689,6 @@ pub fn home_dashboard(app: &mut App) -> CommandResult { let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeReviewTip)); let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeYoloTip)); } - AppMode::Yolo => { - // Compatibility residual: YOLO is invisible Act + Full Access. - let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeYoloModeTip)); - let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeYoloModeCaution)); - } AppMode::Operate => { let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeOperateModeTip)); let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeOperateModeFleetTip)); @@ -1727,12 +1722,7 @@ mod tests { #[test] fn test_home_dashboard_mode_tips_for_each_mode() { - let modes = [ - AppMode::Agent, - AppMode::Yolo, - AppMode::Plan, - AppMode::Operate, - ]; + let modes = [AppMode::Agent, AppMode::Plan, AppMode::Operate]; for mode in modes { let mut app = create_test_app(); app.mode = mode; diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index b5a4d3b869..44886363a0 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -1401,7 +1401,7 @@ impl From<&str> for DefaultModeValue { other => match AppMode::from_setting(other) { AppMode::Plan => Self::Plan, AppMode::Operate => Self::Operate, - AppMode::Agent | AppMode::Yolo => Self::Agent, + AppMode::Agent => Self::Agent, }, } } diff --git a/crates/tui/src/core/authority.rs b/crates/tui/src/core/authority.rs index 7ace8d09d9..e03f055f7f 100644 --- a/crates/tui/src/core/authority.rs +++ b/crates/tui/src/core/authority.rs @@ -44,7 +44,9 @@ pub(crate) struct EffectiveModePolicy { /// - `Plan` -> read-only: no shell, no trust, `Suggest` approvals. /// - `Agent` -> the user's durable baseline (`prefs`). /// - `Operate` -> Agent baseline plus orchestration capabilities in the runtime. -/// - `Yolo` -> legacy compat; full authority: shell + trust + `Bypass` approvals. +/// +/// The legacy YOLO spelling resolves to Agent plus a `Bypass` approval +/// posture before it reaches this table; modes no longer carry permission. #[must_use] pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> EffectiveModePolicy { match mode { @@ -60,12 +62,6 @@ pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> E trust_mode: prefs.agent_trust_mode, approval_mode: prefs.agent_approval_mode, }, - AppMode::Yolo => EffectiveModePolicy { - mode, - allow_shell: true, - trust_mode: true, - approval_mode: ApprovalMode::Bypass, - }, } } @@ -187,17 +183,13 @@ impl TurnAuthority { /// Authority for the per-tool approval gate, folded from the legacy /// session `auto_approve` bit so [`resolve_tool_permission`] observes the /// same effective posture the old boolean helpers encoded: a set bit is - /// Full Access (Yolo/Bypass-shaped), a cleared bit is an ordinary Ask + /// the Full Access posture (Bypass), a cleared bit is an ordinary Ask /// turn. The engine's `Never` denial deliberately stays at the UI layer, /// so this constructor never produces a `Never` posture. #[must_use] pub(crate) fn for_tool_approval_decision(auto_approve: bool) -> Self { Self::from_effective_fields( - if auto_approve { - AppMode::Yolo - } else { - AppMode::Agent - }, + AppMode::Agent, true, false, auto_approve, @@ -241,7 +233,7 @@ pub(crate) fn effective_input_policy( auto_approve: bool, approval_mode: ApprovalMode, ) -> TurnAuthority { - let mut mode = requested_mode; + let mode = requested_mode; let mut trust_mode = trust_mode; let mut auto_approve = auto_approve; let mut approval_mode = approval_mode; @@ -250,13 +242,8 @@ pub(crate) fn effective_input_policy( if !provenance_can_inherit_standing_auto_authority(provenance) { let from_mode = mode; let from_approval = approval_mode; - let had_auto_authority = matches!(mode, AppMode::Yolo) - || trust_mode - || auto_approve - || matches!(approval_mode, ApprovalMode::Bypass); - if matches!(mode, AppMode::Yolo) { - mode = AppMode::Agent; - } + let had_auto_authority = + trust_mode || auto_approve || matches!(approval_mode, ApprovalMode::Bypass); trust_mode = false; auto_approve = false; if matches!(approval_mode, ApprovalMode::Auto | ApprovalMode::Bypass) { @@ -344,7 +331,7 @@ pub(crate) fn sandbox_policy_for_turn( ) -> SandboxPolicy { let default = if mode == AppMode::Plan { SandboxPolicy::ReadOnly - } else if mode == AppMode::Yolo || approval_mode == ApprovalMode::Bypass { + } else if approval_mode == ApprovalMode::Bypass { SandboxPolicy::DangerFullAccess } else { workspace_write_policy(workspace, network_access) @@ -417,7 +404,7 @@ pub(crate) fn shell_policy_for_mode(mode: AppMode, allow_shell: bool) -> ShellPo } match mode { AppMode::Plan => ShellPolicy::None, - AppMode::Agent | AppMode::Operate | AppMode::Yolo => ShellPolicy::Full, + AppMode::Agent | AppMode::Operate => ShellPolicy::Full, } } @@ -442,9 +429,9 @@ pub(crate) enum ToolPermission { /// - `Auto` tools always run — even under `Never`, which stays read-only /// rather than dead. /// - `Never` denies any tool that would otherwise prompt, but only when the -/// authority is not full-access shaped: a Yolo/Bypass authority carrying a -/// stale `Never` enum still auto-approves, matching the legacy UI order in -/// which the full-access shortcut ran before the `Never` check. +/// authority is not full-access shaped: a Bypass-shaped authority carrying +/// a stale `Never` enum still auto-approves, matching the legacy UI order +/// in which the full-access shortcut ran before the `Never` check. /// - `Suggest` and `Required` are both bypassable by auto-approve authority /// unless the tool is on the typed non-bypassable hold list /// (`is_non_bypassable`), which always prompts. A generic `Required` tool @@ -458,7 +445,6 @@ pub(crate) fn resolve_tool_permission( if authority.approval_mode == ApprovalMode::Never && requirement != ApprovalRequirement::Auto && !authority.auto_approve - && authority.mode != AppMode::Yolo { return ToolPermission::Deny; } @@ -470,16 +456,13 @@ pub(crate) fn resolve_tool_permission( // shell included — so a hold that cannot open its own // approval modal auto-approves instead of stranding the call. // #3866 blocked here through v0.9.6; reversed 2026-08-10. - return if authority.auto_approve || authority.mode == AppMode::Yolo { + return if authority.auto_approve { ToolPermission::Allow } else { ToolPermission::Prompt }; } - if authority.auto_approve - || authority.approval_mode == ApprovalMode::Bypass - || authority.mode == AppMode::Yolo - { + if authority.auto_approve || authority.approval_mode == ApprovalMode::Bypass { ToolPermission::Allow } else { ToolPermission::Prompt @@ -726,11 +709,6 @@ mod tests { ApprovalMode::Bypass, true )); - assert!(!write_carve_out_posture( - AppMode::Yolo, - ApprovalMode::Bypass, - true - )); // Never still denies; Auto-Review still fails unresolved holds closed; // Plan is read-only by mode. assert!(!write_carve_out_posture( @@ -910,11 +888,11 @@ mod tests { } } - // Yolo/Bypass is deliberately unsandboxed and keeps its semantics: - // DangerFullAccess reports network regardless of this key, because it - // applies no sandbox at all. - let yolo = authority(AppMode::Yolo, true, ApprovalMode::Bypass); - let policy = yolo.sandbox_policy(workspace, None, SandboxNetworkAccess::Restricted); + // The Bypass posture is deliberately unsandboxed and keeps its + // semantics: DangerFullAccess reports network regardless of this key, + // because it applies no sandbox at all. + let bypass = authority(AppMode::Agent, true, ApprovalMode::Bypass); + let policy = bypass.sandbox_policy(workspace, None, SandboxNetworkAccess::Restricted); assert_eq!(policy, SandboxPolicy::DangerFullAccess); assert!(policy.has_network_access()); @@ -985,7 +963,6 @@ mod tests { (AppMode::Agent, false, ApprovalMode::Auto), (AppMode::Agent, false, ApprovalMode::Never), (AppMode::Agent, true, ApprovalMode::Bypass), - (AppMode::Yolo, true, ApprovalMode::Bypass), (AppMode::Plan, false, ApprovalMode::Suggest), ] { let auth = authority(mode, auto_approve, approval_mode); @@ -1018,7 +995,6 @@ mod tests { fn full_access_allows_bypassable_but_prompts_for_non_bypassable() { for auth in [ authority(AppMode::Agent, true, ApprovalMode::Bypass), - authority(AppMode::Yolo, true, ApprovalMode::Bypass), TurnAuthority::for_tool_approval_decision(true), ] { for requirement in [ApprovalRequirement::Suggest, ApprovalRequirement::Required] { @@ -1064,19 +1040,14 @@ mod tests { "Never remains read-only rather than dead" ); - // Legacy host shape: full-access bit/Yolo mode with a stale Never enum - // still auto-approves — the UI's full-access shortcut ran before its - // Never check. + // Legacy host shape: a full-access bit with a stale Never enum still + // auto-approves — the UI's full-access shortcut ran before its Never + // check. let stale = authority(AppMode::Agent, true, ApprovalMode::Never); assert_eq!( resolve_tool_permission(&stale, ApprovalRequirement::Suggest, false), ToolPermission::Allow ); - let yolo_never = authority(AppMode::Yolo, false, ApprovalMode::Never); - assert_eq!( - resolve_tool_permission(&yolo_never, ApprovalRequirement::Suggest, false), - ToolPermission::Allow - ); } #[test] diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 6749650475..14c978adee 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -4521,17 +4521,10 @@ impl Engine { // which is not necessarily the installed one under auto routing. let capability = route.capability_profile(); let always_load = self.config.tools_always_load.clone(); - let bypass = input_policy.auto_approve - || input_policy.approval_mode == crate::tui::approval::ApprovalMode::Bypass; - let catalog_mode = if bypass { - AppMode::Yolo - } else { - input_policy.mode - }; let mut catalog = build_model_tool_catalog_with_surface( tool_registry.to_api_tools_with_cache(true), mcp_tools, - catalog_mode, + input_policy.mode, &always_load, capability.tool_surface_budget, ); @@ -4747,7 +4740,7 @@ impl Engine { &content, allow_shell, trust_mode, - mode == AppMode::Yolo || auto_approve, + auto_approve, approval_mode, ); let prompt_context = NextTurnPromptContext::for_planned_turn( @@ -5847,7 +5840,7 @@ impl Engine { mode, self.session.allow_shell, self.session.trust_mode, - mode == AppMode::Yolo || auto_approve, + auto_approve, self.session.approval_mode, ); let route = TurnRouteContext { diff --git a/crates/tui/src/core/engine/preview.rs b/crates/tui/src/core/engine/preview.rs index 425fce5bf4..2cfb998dd7 100644 --- a/crates/tui/src/core/engine/preview.rs +++ b/crates/tui/src/core/engine/preview.rs @@ -289,7 +289,7 @@ impl Engine { &hypothetical_content, inputs.allow_shell, inputs.trust_mode, - inputs.mode == AppMode::Yolo || inputs.auto_approve, + inputs.auto_approve, inputs.approval_mode, ); let prompt_context = NextTurnPromptContext { @@ -647,7 +647,7 @@ impl Engine { "", inputs.allow_shell, inputs.trust_mode, - inputs.mode == AppMode::Yolo || inputs.auto_approve, + inputs.auto_approve, inputs.approval_mode, ); SessionFacts { diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 2e907386fb..659565e3c3 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -10631,12 +10631,12 @@ fn deferred_apply_patch_first_use_hydrates_schema_without_execution() { } #[test] -fn model_tool_catalog_defers_non_core_native_tools_in_yolo_mode() { +fn model_tool_catalog_defers_non_core_native_tools_in_act_mode() { let always_load = HashSet::new(); let catalog = build_model_tool_catalog( vec![api_tool("read"), api_tool("project_map")], vec![api_tool("mcp_server_write")], - AppMode::Yolo, + AppMode::Agent, &always_load, ); @@ -10713,10 +10713,10 @@ fn auto_review_hides_question_tool_while_other_postures_keep_it() { } #[test] -fn legacy_yolo_auto_shape_keeps_question_tool_as_effective_full_access() { +fn legacy_full_access_bit_keeps_question_tool_as_effective_full_access() { let authority = crate::core::authority::effective_input_policy( UserInputProvenance::ExternalUser, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -10756,7 +10756,7 @@ fn model_tool_catalog_sorts_each_partition_for_prefix_cache_stability() { api_tool("exec_shell"), ], vec![api_tool("mcp_zoo_b"), api_tool("mcp_aardvark_a")], - AppMode::Yolo, + AppMode::Agent, &always_load, ); @@ -11086,7 +11086,7 @@ async fn run_shell_command_op_skips_approval_when_auto_approved() { engine .handle_run_shell_command( "echo bang-yolo".to_string(), - AppMode::Yolo, + AppMode::Agent, true, true, true, @@ -11197,10 +11197,10 @@ async fn run_shell_command_op_allows_readonly_shell_in_auto_mode() { #[tokio::test] async fn yolo_mode_does_not_prompt_for_typed_ask_rule() { // #3386: a command matching a typed ask-rule (permissions.toml) must not - // surface an approval modal in YOLO mode, even though Yolo resolves to - // ApprovalMode::Auto which the execpolicy maps to OnFailure (honors + // surface an approval modal in the Full Access posture, even though the + // stale ApprovalMode::Auto maps to OnFailure in the execpolicy (honors // ask-rules). The auto_review safety floor and typed deny rules still - // apply; only the ask-rule Prompt is suppressed in YOLO. + // apply; only the ask-rule Prompt is suppressed under Full Access. let (mut engine, handle) = Engine::new( EngineConfig { exec_policy_engine: ask_rule_engine("echo"), @@ -11212,7 +11212,7 @@ async fn yolo_mode_does_not_prompt_for_typed_ask_rule() { engine .handle_run_shell_command( "echo yolo-ask-rule".to_string(), - AppMode::Yolo, + AppMode::Agent, true, true, true, @@ -12454,7 +12454,7 @@ async fn yolo_mode_does_not_prompt_for_background_shell() { handle .send(Op::SendMessage { content: "please run a background shell".to_string(), - mode: AppMode::Yolo, + mode: AppMode::Agent, route: resolved_route_for_test(&api_config, crate::config::DEFAULT_TEXT_MODEL), compaction: Box::new(CompactionConfig::default()), goal_objective: None, @@ -12590,7 +12590,7 @@ async fn yolo_mode_executes_publish_like_shell_without_prompt() { handle .send(Op::SendMessage { content: "please publish this crate".to_string(), - mode: AppMode::Yolo, + mode: AppMode::Agent, route: resolved_route_for_test(&api_config, crate::config::DEFAULT_TEXT_MODEL), compaction: Box::new(CompactionConfig::default()), goal_objective: None, @@ -12730,7 +12730,7 @@ async fn yolo_mode_does_not_prompt_for_mcp_action() { handle .send(Op::SendMessage { content: "please open the PR".to_string(), - mode: AppMode::Yolo, + mode: AppMode::Agent, route: resolved_route_for_test(&api_config, crate::config::DEFAULT_TEXT_MODEL), compaction: Box::new(CompactionConfig::default()), goal_objective: None, @@ -13037,12 +13037,7 @@ fn plan_mode_toggle_preserves_catalog_byte_stability() { fn parent_turn_registry_includes_goal_tools_for_all_modes() { let (engine, _handle) = Engine::new(EngineConfig::default(), &Config::default()); - for mode in [ - AppMode::Plan, - AppMode::Agent, - AppMode::Operate, - AppMode::Yolo, - ] { + for mode in [AppMode::Plan, AppMode::Agent, AppMode::Operate] { let registry = engine .build_turn_tool_registry_builder( mode, @@ -13168,18 +13163,6 @@ fn mode_invariant_matrix_covers_context_catalog_subagents_and_prompt_metadata() approval_mode: ApprovalMode::Suggest, plan_hint: false, }, - ModeCase { - // YOLO remains an elevated-permission alias, but prompt/setting - // surfaces now speak Act (invisible one-way permission shorthand). - name: "yolo", - mode: AppMode::Yolo, - shell_policy: ShellPolicy::Full, - sandbox: ExpectedSandbox::DangerFullAccess, - trust_mode: true, - auto_approve: true, - approval_mode: ApprovalMode::Bypass, - plan_hint: false, - }, ]; for case in cases { @@ -13384,7 +13367,7 @@ fn mode_invariant_matrix_covers_provenance_authority_narrowing() { ProvenanceCase { name: "external user", provenance: UserInputProvenance::ExternalUser, - expected_mode: AppMode::Yolo, + expected_mode: AppMode::Agent, expected_trust: true, expected_auto: true, expected_approval: ApprovalMode::Bypass, @@ -13393,7 +13376,7 @@ fn mode_invariant_matrix_covers_provenance_authority_narrowing() { ProvenanceCase { name: "runtime continuation", provenance: UserInputProvenance::Runtime, - expected_mode: AppMode::Yolo, + expected_mode: AppMode::Agent, expected_trust: true, expected_auto: true, expected_approval: ApprovalMode::Bypass, @@ -13402,7 +13385,7 @@ fn mode_invariant_matrix_covers_provenance_authority_narrowing() { ProvenanceCase { name: "sub-agent handoff", provenance: UserInputProvenance::SubAgentHandoff, - expected_mode: AppMode::Yolo, + expected_mode: AppMode::Agent, expected_trust: true, expected_auto: true, expected_approval: ApprovalMode::Bypass, @@ -13440,7 +13423,7 @@ fn mode_invariant_matrix_covers_provenance_authority_narrowing() { for case in cases { let policy = effective_input_policy( case.provenance, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -13475,7 +13458,6 @@ fn agent_mode_can_build_auto_approved_tool_context() { .auto_approve ); assert!(engine.build_tool_context(AppMode::Agent, true).auto_approve); - assert!(engine.build_tool_context(AppMode::Yolo, false).auto_approve); } #[test] @@ -13530,10 +13512,6 @@ fn build_tool_context_uses_typed_shell_policy_per_mode() { .shell_policy, crate::worker_profile::ShellPolicy::Full ); - assert_eq!( - engine.build_tool_context(AppMode::Yolo, false).shell_policy, - crate::worker_profile::ShellPolicy::Full - ); config.allow_shell = false; let (engine, _handle) = Engine::new(config, &Config::default()); @@ -13553,7 +13531,7 @@ fn turn_tool_context_uses_planned_authority_and_route_not_installed_session() { engine.session.model = "installed-old-model".to_string(); let authority = crate::core::authority::TurnAuthority::from_effective_fields( - AppMode::Yolo, + AppMode::Agent, true, true, true, @@ -13627,21 +13605,24 @@ fn agent_mode_elevates_writes_without_granting_network() { "Agent mode must still elevate workspace writes; got {agent_policy:?}", ); - let yolo_ctx = engine.build_tool_context(AppMode::Yolo, false); - let yolo_policy = yolo_ctx + let full_access_ctx = engine.build_tool_context(AppMode::Agent, true); + let full_access_policy = full_access_ctx .elevated_sandbox_policy .as_ref() - .expect("Yolo mode should elevate the sandbox policy"); - assert!(yolo_policy.has_network_access()); - // v0.8.11: YOLO drops to DangerFullAccess (no sandbox) so the user - // is not bounced through approval round-trips for legitimate + .expect("Full Access should elevate the sandbox policy"); + assert!(full_access_policy.has_network_access()); + // v0.8.11: Full Access drops to DangerFullAccess (no sandbox) so the + // user is not bounced through approval round-trips for legitimate // outside-workspace writes (package installs, sub-agent - // workspaces, ~/.cache mutations, etc.). YOLO is opt-in and + // workspaces, ~/.cache mutations, etc.). Full Access is opt-in and // already enables trust mode + auto-approve; the sandbox was the // last guardrail and contradicts the contract. assert!( - matches!(yolo_policy, crate::sandbox::SandboxPolicy::DangerFullAccess), - "Yolo mode must use DangerFullAccess (no sandbox); got {yolo_policy:?}", + matches!( + full_access_policy, + crate::sandbox::SandboxPolicy::DangerFullAccess + ), + "Full Access must use DangerFullAccess (no sandbox); got {full_access_policy:?}", ); // Plan mode (#1077): the sandbox must actually deny workspace writes. @@ -13735,11 +13716,11 @@ fn sandbox_policy_for_turn_returns_correct_default_policy_per_mode() { other => panic!("Agent mode should be WorkspaceWrite; got {other:?}"), } - // YOLO: DangerFullAccess. + // Bypass posture: DangerFullAccess. assert!(matches!( sandbox_policy_for_turn( - AppMode::Yolo, - ApprovalMode::Suggest, + AppMode::Agent, + ApprovalMode::Bypass, None, &workspace, SandboxNetworkAccess::Restricted, @@ -13851,7 +13832,7 @@ async fn change_mode_refreshes_session_prompt_and_updates_session() { let run = tokio::spawn(engine.run()); handle .send(Op::ChangeMode { - mode: AppMode::Yolo, + mode: AppMode::Agent, allow_shell: true, trust_mode: true, auto_approve: true, @@ -14496,7 +14477,7 @@ async fn change_mode_op_updates_current_mode_and_emits_status() { let run = tokio::spawn(engine.run()); handle .send(Op::ChangeMode { - mode: AppMode::Yolo, + mode: AppMode::Agent, allow_shell: true, trust_mode: true, auto_approve: true, @@ -14570,16 +14551,16 @@ fn runtime_mode_policy_updates_engine_session_mirrors() { crate::tui::approval::ApprovalMode::Never ); - let yolo_authority = crate::core::authority::TurnAuthority::from_effective_fields( - AppMode::Yolo, + let full_access_authority = crate::core::authority::TurnAuthority::from_effective_fields( + AppMode::Agent, true, true, true, crate::tui::approval::ApprovalMode::Bypass, ); - engine.apply_runtime_mode_policy(&yolo_authority); + engine.apply_runtime_mode_policy(&full_access_authority); - assert_eq!(engine.current_mode, AppMode::Yolo); + assert_eq!(engine.current_mode, AppMode::Agent); assert!(engine.session.allow_shell); assert!(engine.session.trust_mode); assert!(engine.config.trust_mode); @@ -16904,7 +16885,7 @@ fn provenance_gate_preserves_standing_yolo_for_runtime_and_subagent_continuation for provenance in all_provenances { let policy = effective_input_policy( provenance, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -16913,7 +16894,7 @@ fn provenance_gate_preserves_standing_yolo_for_runtime_and_subagent_continuation ); if inheriting_provenances.contains(&provenance) { - assert_eq!(policy.mode, AppMode::Yolo, "{provenance:?}"); + assert_eq!(policy.mode, AppMode::Agent, "{provenance:?}"); assert!(policy.allow_shell, "{provenance:?}"); assert!(policy.trust_mode, "{provenance:?}"); assert!(policy.auto_approve, "{provenance:?}"); @@ -17011,7 +16992,7 @@ fn self_generated_fake_approvals_cannot_authorize_work() { for content in ["改吧", "嗯"] { let policy = effective_input_policy( provenance, - AppMode::Yolo, + AppMode::Agent, content, true, true, @@ -17049,7 +17030,7 @@ fn external_prompt_wording_never_changes_effective_mode_or_authority() { "你在帮我看看 外卖部分还哪里没有使用多语言", ), ( - AppMode::Yolo, + AppMode::Agent, crate::tui::approval::ApprovalMode::Bypass, true, true, @@ -17089,14 +17070,14 @@ fn external_prompt_wording_never_changes_effective_mode_or_authority() { fn external_user_wording_does_not_downgrade_standing_authority() { let review_wording = effective_input_policy( UserInputProvenance::ExternalUser, - AppMode::Yolo, + AppMode::Agent, "你在帮我看看 外卖部分还哪里没有使用多语言 我看看要不要加", true, true, true, crate::tui::approval::ApprovalMode::Bypass, ); - assert_eq!(review_wording.mode, AppMode::Yolo); + assert_eq!(review_wording.mode, AppMode::Agent); assert!(review_wording.allow_shell); assert!(review_wording.trust_mode); assert!(review_wording.auto_approve); @@ -17111,14 +17092,14 @@ fn external_user_wording_does_not_downgrade_standing_authority() { let later_user_instruction = effective_input_policy( UserInputProvenance::ExternalUser, - AppMode::Yolo, + AppMode::Agent, "需要修复下", true, true, true, crate::tui::approval::ApprovalMode::Bypass, ); - assert_eq!(later_user_instruction.mode, AppMode::Yolo); + assert_eq!(later_user_instruction.mode, AppMode::Agent); assert!(later_user_instruction.allow_shell); assert!(later_user_instruction.trust_mode); assert!(later_user_instruction.auto_approve); @@ -17285,8 +17266,8 @@ fn current_mode_field_assignment_takes_effect_synchronously() { let (mut engine, _handle) = Engine::new(config, &Config::default()); assert_eq!(engine.current_mode, AppMode::Agent); - engine.current_mode = AppMode::Yolo; - assert_eq!(engine.current_mode, AppMode::Yolo); + engine.current_mode = AppMode::Operate; + assert_eq!(engine.current_mode, AppMode::Operate); } #[test] @@ -20888,7 +20869,7 @@ fn every_effective_mode_change_carries_a_structured_narrowing_event() { for provenance in narrowing_provenances { let policy = effective_input_policy( provenance, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -20913,10 +20894,8 @@ fn every_effective_mode_change_carries_a_structured_narrowing_event() { "{provenance:?}" ); assert_eq!(event.reason().as_str(), "non_authoritative_provenance"); - // The transition names both ends, so a reader can see what was lost. - // Mode deliberately reads as the permission vocabulary (`AppMode:: - // as_setting` writes "agent" for the legacy Yolo label), so the - // posture is what carries the change here. + // The transition names both ends, so a reader can see what was + // lost; the posture is what carries the change here. let transition = event.transition(); assert_eq!( transition, "agent (Full Access) -> agent (Ask)", @@ -20927,7 +20906,7 @@ fn every_effective_mode_change_carries_a_structured_narrowing_event() { // An authoritative turn narrows nothing and therefore reports nothing. let unchanged = effective_input_policy( UserInputProvenance::ExternalUser, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -20945,7 +20924,7 @@ fn every_effective_mode_change_carries_a_structured_narrowing_event() { fn ui_status_and_model_metadata_render_the_same_narrowing_sentence() { let policy = effective_input_policy( UserInputProvenance::AssistantGenerated, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, @@ -21014,7 +20993,7 @@ fn turn_metadata_carries_the_narrowing_only_on_a_narrowed_turn() { let policy = effective_input_policy( UserInputProvenance::AssistantGenerated, - AppMode::Yolo, + AppMode::Agent, "continue", true, true, diff --git a/crates/tui/src/core/engine/tool_catalog/tests.rs b/crates/tui/src/core/engine/tool_catalog/tests.rs index 52431705b4..af8e101190 100644 --- a/crates/tui/src/core/engine/tool_catalog/tests.rs +++ b/crates/tui/src/core/engine/tool_catalog/tests.rs @@ -86,7 +86,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_full_access() { .into_iter() .map(str::to_string) .collect::>(); - for mode in [AppMode::Plan, AppMode::Agent, AppMode::Yolo] { + for mode in [AppMode::Plan, AppMode::Agent] { let mut catalog = [ "read", "write", @@ -114,7 +114,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_full_access() { #[test] fn mcp_tools_are_searchable_not_eager_in_every_mode() { - for mode in [AppMode::Plan, AppMode::Agent, AppMode::Yolo] { + for mode in [AppMode::Plan, AppMode::Agent] { let mut catalog = vec![tool("read_mcp_resource"), tool("mcp_acme_lookup")]; apply_mcp_tool_deferral(&mut catalog, mode, &HashSet::new()); assert!( diff --git a/crates/tui/src/core/protocol_parity.rs b/crates/tui/src/core/protocol_parity.rs index 71279397c0..2d7adae5c0 100644 --- a/crates/tui/src/core/protocol_parity.rs +++ b/crates/tui/src/core/protocol_parity.rs @@ -64,13 +64,11 @@ fn count(value: usize) -> u64 { u64::try_from(value).unwrap_or(u64::MAX) } -/// Lossless mode label. Unlike `AppMode::as_setting`, `yolo` keeps its -/// identity so a client can round-trip through `AppMode::parse`. +/// Lossless mode label; round-trips through `AppMode::parse`. #[must_use] pub fn app_mode_str(mode: AppMode) -> &'static str { match mode { AppMode::Agent => "agent", - AppMode::Yolo => "yolo", AppMode::Plan => "plan", AppMode::Operate => "operate", } @@ -1390,12 +1388,7 @@ mod tests { #[test] fn mode_labels_round_trip_through_app_mode_parse() { - for mode in [ - AppMode::Agent, - AppMode::Yolo, - AppMode::Plan, - AppMode::Operate, - ] { + for mode in [AppMode::Agent, AppMode::Plan, AppMode::Operate] { assert_eq!(AppMode::parse(app_mode_str(mode)), Some(mode), "{mode:?}"); } for mode in [ diff --git a/crates/tui/src/exec_agent.rs b/crates/tui/src/exec_agent.rs index 4f2dacfda1..15c77e3da7 100644 --- a/crates/tui/src/exec_agent.rs +++ b/crates/tui/src/exec_agent.rs @@ -342,11 +342,9 @@ pub(crate) async fn run_exec_agent( }; let engine_handle = spawn_engine(engine_config, &execution_config); - let mode = if auto_approve { - AppMode::Yolo - } else { - AppMode::Agent - }; + // The Full Access posture travels in the op's auto_approve/approval_mode + // fields; modes no longer carry permission. + let mode = AppMode::Agent; let resuming_session = resume_session.is_some(); let mut loaded_session_id = None; diff --git a/crates/tui/src/lib.rs b/crates/tui/src/lib.rs index 0057cd59b5..18692ad069 100644 --- a/crates/tui/src/lib.rs +++ b/crates/tui/src/lib.rs @@ -11920,11 +11920,7 @@ async fn build_direct_workflow_tool( } let yolo = config.yolo.unwrap_or(false); - let mode = if yolo { - AppMode::Yolo - } else { - AppMode::Operate - }; + let mode = AppMode::Operate; let allow_shell = yolo || config.allow_shell(); let shell_policy = shell_policy_for_mode(mode, allow_shell); let trusted = crate::workspace_trust::WorkspaceTrust::load_for(workspace); diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 2dd6cc719f..e396473f30 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -1691,9 +1691,10 @@ mod tests { } #[test] - fn yolo_mode_uses_the_shared_completion_contract() { - // `codewhale exec --auto` runs AppMode::Yolo; the verify-then-stop - // contract must survive composition into the prompt that mode ships. + fn full_access_posture_uses_the_shared_completion_contract() { + // `codewhale exec --auto` runs Act with the Full Access posture; the + // verify-then-stop contract must survive composition into the prompt + // that posture ships. let tmp = tempdir().expect("tempdir"); let text = system_prompt_flat_text( &system_prompt_for_mode_with_context_skills_session_and_approval( @@ -1712,7 +1713,7 @@ mod tests { verbosity: None, skills_scan_codewhale_only: false, plugin_registry: None, - mode: crate::tui::app::AppMode::Yolo, + mode: crate::tui::app::AppMode::Agent, }, ), ); diff --git a/crates/tui/src/runtime_policy.rs b/crates/tui/src/runtime_policy.rs index 28b5310234..e209cd494f 100644 --- a/crates/tui/src/runtime_policy.rs +++ b/crates/tui/src/runtime_policy.rs @@ -30,14 +30,14 @@ impl RuntimePolicyProjection { .and_then(ApprovalMode::from_config_value) .filter(|permission| *permission != ApprovalMode::Never) .unwrap_or_else(|| { - if parsed_mode == AppMode::Yolo || auto_approve { + if legacy_yolo_alias(mode) || auto_approve { ApprovalMode::Bypass } else { ApprovalMode::Suggest } }); Self { - mode: visible_mode(parsed_mode), + mode: parsed_mode, permission, } } @@ -58,16 +58,14 @@ impl RuntimePolicyProjection { "unsupported permission posture {value:?}; expected ask, auto-review, or full-access" ) })?, - None if parsed_mode == AppMode::Yolo || auto_approve.unwrap_or(false) => { - ApprovalMode::Bypass - } + None if legacy_yolo_alias(mode) || auto_approve.unwrap_or(false) => ApprovalMode::Bypass, None => ApprovalMode::Suggest, }; if permission == ApprovalMode::Never { bail!("permission posture 'never' is not part of the Runtime product contract"); } Ok(Self { - mode: visible_mode(parsed_mode), + mode: parsed_mode, permission, }) } @@ -101,12 +99,15 @@ pub(crate) fn parse_runtime_mode(value: &str) -> Option { } } +/// Legacy mode spellings that carried the Full Access posture. `AppMode:: +/// parse` folds them to Agent; the posture is re-derived from the raw wire +/// value so old persisted shapes keep their permission meaning. #[must_use] -fn visible_mode(mode: AppMode) -> AppMode { - match mode { - AppMode::Yolo => AppMode::Agent, - other => other, - } +fn legacy_yolo_alias(mode: &str) -> bool { + matches!( + mode.trim().to_ascii_lowercase().as_str(), + "yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" + ) } #[cfg(test)] diff --git a/crates/tui/src/runtime_threads/tests.rs b/crates/tui/src/runtime_threads/tests.rs index 52f2981d53..1a91dc9173 100644 --- a/crates/tui/src/runtime_threads/tests.rs +++ b/crates/tui/src/runtime_threads/tests.rs @@ -11063,8 +11063,9 @@ fn parse_mode_opt_resolves_explicit_tokens_and_aliases() { assert_eq!(parse_mode_opt("auto"), Some(AppMode::Agent)); assert_eq!(parse_mode_opt("operate"), Some(AppMode::Operate)); assert_eq!(parse_mode_opt("3"), Some(AppMode::Operate)); - assert_eq!(parse_mode_opt("yolo"), Some(AppMode::Yolo)); - assert_eq!(parse_mode_opt("4"), Some(AppMode::Yolo)); + // Legacy YOLO spellings resolve to Act; the posture travels separately. + assert_eq!(parse_mode_opt("yolo"), Some(AppMode::Agent)); + assert_eq!(parse_mode_opt("4"), Some(AppMode::Agent)); assert_eq!(parse_mode_opt(" PLAN "), Some(AppMode::Plan)); } @@ -11088,7 +11089,7 @@ fn parse_mode_wrapper_defaults_and_resolves_numeric_aliases() { assert_eq!(parse_mode("1"), AppMode::Agent); assert_eq!(parse_mode("2"), AppMode::Plan); assert_eq!(parse_mode("3"), AppMode::Operate); - assert_eq!(parse_mode("4"), AppMode::Yolo); + assert_eq!(parse_mode("4"), AppMode::Agent); } fn rebind_event(event: &str, agent_id: &str, seq: u64) -> RuntimeEventRecord { diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index d02a6fcd09..e6e82e71cb 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -2779,17 +2779,8 @@ impl App { } pub fn set_mode(&mut self, mode: AppMode) -> bool { - let requested_mode = mode; - let mode = match mode { - AppMode::Yolo => AppMode::Agent, - other => other, - }; - // YOLO is a permission change (Full Access + trust + shell), not a - // mode change. A locked approval policy owns that surface — every - // other posture route already honors the lock. - let yolo_compat = requested_mode == AppMode::Yolo && !self.approval_policy_locked(); let previous_mode = self.mode; - if previous_mode == mode && !yolo_compat && !self.yolo { + if previous_mode == mode && !self.yolo { return false; } @@ -2812,38 +2803,95 @@ impl App { }; } - if yolo_compat { - // Transient full-access mirrors for legacy YOLO entry points; do not - // persist trust/shell elevation into the durable Agent baseline. - if self.shell_access_editable { - self.allow_shell = true; - } - self.trust_mode = true; - self.approval_mode = ApprovalMode::Bypass; - self.yolo = true; - self.notify_yolo_compat_once(); + let policy = base_policy_for_mode(mode, &self.mode_prefs); + self.allow_shell = policy.allow_shell; + self.trust_mode = policy.trust_mode; + self.approval_mode = policy.approval_mode; + self.yolo = matches!(policy.approval_mode, ApprovalMode::Bypass); + + self.finish_mode_change(previous_mode); + true + } + + /// Legacy YOLO entry points (`--yolo` launch, Alt+Y, the `/mode` yolo + /// alias, `/zidong`). YOLO is a permission change (Full Access + trust + shell), + /// not a mode change: the installed mode stays Act and the elevated + /// authority lives in transient full-access mirrors, never in the durable + /// Agent baseline (#3386/#3279). + pub fn set_mode_yolo_compat(&mut self) -> bool { + // YOLO is a permission change. A locked approval policy must not be + // sidestepped by --yolo, default_mode=yolo, /zidong, or Alt+Y. + if self.approval_policy_locked() { + return false; + } + let previous_mode = self.mode; + // Same baseline-refresh rule as a mode hop: the elevation must not + // bleed into the restored Agent surface. + if previous_mode.uses_agent_baseline() && !self.yolo { + self.mode_prefs = ModeSessionPrefs { + agent_allow_shell: self.allow_shell, + agent_trust_mode: self.trust_mode, + agent_approval_mode: self.approval_mode, + }; + } + // The legacy alias always lands in Act, never in Plan or Operate. + self.mode = AppMode::Agent; + // Transient full-access mirrors; do not persist trust/shell elevation + // into the durable Agent baseline. + if self.shell_access_editable { + self.allow_shell = true; + } + self.trust_mode = true; + self.approval_mode = ApprovalMode::Bypass; + self.yolo = true; + self.notify_yolo_compat_once(); + self.finish_mode_change(previous_mode); + true + } + + /// Apply the legacy YOLO selection from a user-facing entry point + /// (Alt+Y, the `/mode` yolo alias, `/zidong`). A locked approval policy owns the + /// permission surface and refuses here; otherwise this behaves like + /// [`Self::select_mode`] and persists the mode actually installed (Act). + pub fn select_yolo_compat(&mut self) -> SettingSelection { + if self.reject_setting_change_while_busy(MessageId::SettingSubjectMode) { + return SettingSelection::Refused; + } + if self.approval_policy_locked() { + self.push_status_toast( + "Permissions are controlled by config or managed requirements".to_string(), + StatusToastLevel::Warning, + Some(6_000), + ); + self.needs_redraw = true; + return SettingSelection::Refused; + } + let changed = self.set_mode_yolo_compat(); + self.startup_defaults + .spawn(crate::tui::startup_defaults::StartupDefaults::mode( + self.mode, + )); + if changed { + SettingSelection::Changed } else { - let policy = base_policy_for_mode(mode, &self.mode_prefs); - self.allow_shell = policy.allow_shell; - self.trust_mode = policy.trust_mode; - self.approval_mode = policy.approval_mode; - self.yolo = matches!(policy.approval_mode, ApprovalMode::Bypass); + SettingSelection::PersistedSame } + } - // Execute mode change hooks. Built from `base_hook_context` so this - // event carries the same session id, workspace, model, and token total - // as every other event — it used to omit `DEEPSEEK_SESSION_ID` - // entirely, which made mode transitions uncorrelatable with the - // session they belonged to. + /// Shared tail of every mode transition: ModeChange hooks plus redraw. + /// Built from `base_hook_context` so this event carries the same session + /// id, workspace, model, and token total as every other event — it used + /// to omit `DEEPSEEK_SESSION_ID` entirely, which made mode transitions + /// uncorrelatable with the session they belonged to. + fn finish_mode_change(&mut self, previous_mode: AppMode) { let context = self .base_hook_context() - .with_mode(mode.label()) + .with_mode(self.mode.label()) .with_previous_mode(previous_mode.label()); if let Err(error) = self.submit_hooks(HookEvent::ModeChange, context) { self.surface_observer_hook_submission_failure(error); } self.needs_redraw = true; - true } /// Apply a *user-facing* mode selection: change the live session mode and @@ -2854,9 +2902,10 @@ impl App { /// application use it because they are re-installing a mode the user /// already chose elsewhere, and re-persisting there would let a restored /// session silently rewrite the startup default. Every interactive - /// selector (Tab/Shift+Tab cycling, the Alt+A/P/Y shortcuts, the hotbar + /// selector (Tab/Shift+Tab cycling, the Alt+A/P shortcuts, the hotbar /// mode actions) goes through here instead, so "I switched to Operate" - /// survives a restart (reported by Hunter against v0.9.1). + /// survives a restart (reported by Hunter against v0.9.1). The legacy + /// YOLO entry points go through [`Self::select_yolo_compat`] instead. /// /// The write is queued, not performed here: it is ordered behind every /// earlier selection by [`StartupDefaultsWriter`], and a failure surfaces @@ -2864,10 +2913,7 @@ impl App { /// dropped. /// /// What is persisted is `self.mode` — the mode `set_mode` actually - /// installed — not the requested enum. The legacy `Yolo` entry point installs - /// Act, so persisting the request would write a startup mode the user never - /// lands in. `AppMode::as_setting` collapses that alias too, but reading the - /// installed value keeps the two from having to agree. + /// installed — not the requested enum. /// /// The outcome is typed, not a bool, because three things can happen and /// only one of them means "nothing was saved": @@ -2891,15 +2937,6 @@ impl App { if self.reject_setting_change_while_busy(MessageId::SettingSubjectMode) { return SettingSelection::Refused; } - if matches!(mode, AppMode::Yolo) && self.approval_policy_locked() { - self.push_status_toast( - "Permissions are controlled by config or managed requirements".to_string(), - StatusToastLevel::Warning, - Some(6_000), - ); - self.needs_redraw = true; - return SettingSelection::Refused; - } let changed = self.set_mode(mode); // Persist an explicit selection even when it matches the live mode. // A restored session can be Operate while the startup default remains diff --git a/crates/tui/src/tui/app/init.rs b/crates/tui/src/tui/app/init.rs index 30a44f6c67..cc4dfaf431 100644 --- a/crates/tui/src/tui/app/init.rs +++ b/crates/tui/src/tui/app/init.rs @@ -504,7 +504,10 @@ impl App { // Resolve the saved mode separately from the permission posture. let preferred_mode = AppMode::from_setting(&settings.default_mode); - let yolo_requested = yolo || (preferred_mode == AppMode::Yolo && !start_in_agent_mode); + // Legacy `default_mode = "yolo"` was split into Act plus the + // full-access posture at the settings edge, so only the CLI flag + // requests the compat elevation here. + let yolo_requested = yolo; let initial_mode = if yolo_requested || start_in_agent_mode { AppMode::Agent } else { @@ -587,7 +590,7 @@ impl App { .unwrap_or_default(); let configured_trust_mode = configured_approval_mode == ApprovalMode::Bypass; let mode_prefs = ModeSessionPrefs { - agent_allow_shell: if yolo_compat || matches!(initial_mode, AppMode::Yolo) { + agent_allow_shell: if yolo_compat { config.interactive_allow_shell() } else { allow_shell diff --git a/crates/tui/src/tui/app/tests.rs b/crates/tui/src/tui/app/tests.rs index 8e6625a6a7..e19cc89f34 100644 --- a/crates/tui/src/tui/app/tests.rs +++ b/crates/tui/src/tui/app/tests.rs @@ -3376,8 +3376,12 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::parse("auto"), Some(AppMode::Agent)); assert_eq!(AppMode::parse("3"), Some(AppMode::Operate)); assert_eq!(AppMode::parse("operate"), Some(AppMode::Operate)); - assert_eq!(AppMode::parse("YOLO"), Some(AppMode::Yolo)); - assert_eq!(AppMode::parse("4"), Some(AppMode::Yolo)); + // Legacy YOLO spellings resolve to Act; the bypass posture they imply + // travels on the permission surface, not on a mode. + assert_eq!(AppMode::parse("YOLO"), Some(AppMode::Agent)); + assert_eq!(AppMode::parse("4"), Some(AppMode::Agent)); + assert_eq!(AppMode::parse("bypass"), Some(AppMode::Agent)); + assert_eq!(AppMode::parse("bypass-permissions"), Some(AppMode::Agent)); assert_eq!(AppMode::parse("multitask"), None); assert_eq!(AppMode::parse("5"), None); assert_eq!(AppMode::parse("fast"), None); @@ -3385,12 +3389,8 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::from_setting("5"), AppMode::Operate); assert_eq!(AppMode::Agent.as_setting(), "agent"); - assert_eq!(AppMode::Yolo.as_setting(), "agent"); assert_eq!(AppMode::Plan.display_name(), "Plan"); - assert_eq!(AppMode::Yolo.label(), "ACT"); - assert_eq!(AppMode::Yolo.display_name(), "Act"); assert_eq!(AppMode::Agent.number(), '1'); - assert_eq!(AppMode::Yolo.number(), '1'); assert_eq!(AppMode::Operate.number(), '3'); assert_eq!( AppMode::CYCLE, @@ -3400,11 +3400,9 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::Plan.next(), AppMode::Agent); assert_eq!(AppMode::Agent.next(), AppMode::Operate); assert_eq!(AppMode::Operate.next(), AppMode::Plan); - assert_eq!(AppMode::Yolo.next(), AppMode::Agent); assert_eq!(AppMode::Plan.previous(), AppMode::Operate); assert_eq!(AppMode::Agent.previous(), AppMode::Plan); assert_eq!(AppMode::Operate.previous(), AppMode::Agent); - assert_eq!(AppMode::Yolo.previous(), AppMode::Agent); } #[test] @@ -3476,7 +3474,7 @@ fn test_mode_switch_toasts_do_not_disrupt_non_mode_toasts() { app.set_mode(AppMode::Agent); app.sync_status_message_to_toasts(); - app.set_mode(AppMode::Yolo); + app.set_mode_yolo_compat(); app.sync_status_message_to_toasts(); assert_eq!(app.status_toasts.len(), 1); @@ -3537,9 +3535,8 @@ fn test_set_mode_updates_state() { let mut app = App::new(test_options(false), &Config::default()); app.yolo_compat_notified = true; app.set_mode(AppMode::Plan); - assert_eq!(app.mode, AppMode::Plan); - // The deprecated YOLO alias remaps to Agent (M6 back-compat shim). - app.set_mode(AppMode::Yolo); + // The deprecated YOLO alias lands in Act (M6 back-compat shim). + app.set_mode_yolo_compat(); assert_eq!(app.mode, AppMode::Agent); assert!(app.yolo); // YOLO compat shim should enable trust, shell, and bypass approvals. @@ -3568,7 +3565,7 @@ fn set_mode_yolo_restores_previous_policies_on_exit() { app.approval_mode = ApprovalMode::Never; app.yolo_compat_notified = true; - app.set_mode(AppMode::Yolo); + app.set_mode_yolo_compat(); assert!(app.allow_shell); assert!(app.trust_mode); assert_eq!(app.approval_mode, ApprovalMode::Bypass); @@ -3613,7 +3610,7 @@ fn set_mode_plan_to_yolo_keeps_yolo_permissions_and_restores_agent_baseline() { app.set_mode(AppMode::Plan); app.approval_mode = ApprovalMode::Suggest; - app.set_mode(AppMode::Yolo); + app.set_mode_yolo_compat(); assert_eq!(app.mode, AppMode::Agent); assert!(app.allow_shell); assert!(app.trust_mode); @@ -3657,13 +3654,8 @@ fn base_policy_for_mode_projects_the_mode_permission_table() { assert_eq!(operate.trust_mode, agent.trust_mode); assert_eq!(operate.approval_mode, ApprovalMode::Never); - // YOLO: full authority is represented by Bypass, not a separate - // auto-approve field (#3736). - let yolo = base_policy_for_mode(AppMode::Yolo, &prefs); - assert_eq!(yolo.mode, AppMode::Yolo); - assert!(yolo.allow_shell); - assert!(yolo.trust_mode); - assert_eq!(yolo.approval_mode, ApprovalMode::Bypass); + // Full Access is represented by the Bypass posture, not a mode row or a + // separate auto-approve field (#3736). // A minimal Agent baseline projects through Agent unchanged. let minimal = ModeSessionPrefs { @@ -3999,7 +3991,7 @@ fn yolo_entry_points_honor_a_locked_approval_policy() { assert!(!app.trust_mode); assert!(!app.yolo); - assert_eq!(app.select_mode(AppMode::Yolo), SettingSelection::Refused); + assert_eq!(app.select_yolo_compat(), SettingSelection::Refused); assert_eq!(app.approval_mode, ApprovalMode::Suggest); assert!(!app.allow_shell); assert!(!app.yolo); @@ -4009,7 +4001,7 @@ fn yolo_entry_points_honor_a_locked_approval_policy() { .any(|toast| toast.text.contains("controlled")) ); - assert!(!app.set_mode(AppMode::Yolo)); + assert!(!app.set_mode_yolo_compat()); assert_eq!(app.approval_mode, ApprovalMode::Suggest); assert!(!app.allow_shell); assert!(!app.yolo); @@ -4029,7 +4021,8 @@ fn set_mode_agent_to_yolo_to_agent_restores_baseline_without_yolo_leak() { app.approval_mode = ApprovalMode::Suggest; app.yolo_compat_notified = true; - app.set_mode(AppMode::Yolo); + app.set_mode_yolo_compat(); + assert_eq!(app.mode, AppMode::Agent); assert!(app.allow_shell); assert!(app.trust_mode); assert_eq!(app.approval_mode, ApprovalMode::Bypass); @@ -4069,7 +4062,7 @@ fn set_mode_plan_to_yolo_to_agent_does_not_bleed_yolo_into_agent() { assert!(!app.trust_mode); assert_eq!(app.approval_mode, ApprovalMode::Suggest); - app.set_mode(AppMode::Yolo); + app.set_mode_yolo_compat(); assert!(app.allow_shell); assert!(app.trust_mode); assert_eq!(app.approval_mode, ApprovalMode::Bypass); @@ -6309,7 +6302,7 @@ fn explicit_mode_selection_and_hotbar_share_the_persistence_owner() { // The legacy YOLO entry point installs Act, so that is what must persist — // "yolo" is a permission alias, never a startup mode. - assert_eq!(app.select_mode(AppMode::Yolo), SettingSelection::Changed); + assert_eq!(app.select_yolo_compat(), SettingSelection::Changed); assert_eq!(Settings::load().expect("reload").default_mode, "agent"); } diff --git a/crates/tui/src/tui/app/types.rs b/crates/tui/src/tui/app/types.rs index e24bdabbba..21fa69b882 100644 --- a/crates/tui/src/tui/app/types.rs +++ b/crates/tui/src/tui/app/types.rs @@ -63,7 +63,7 @@ impl AppModeUi for AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Yolo => MessageId::AppModeAgent, + AppMode::Agent => MessageId::AppModeAgent, AppMode::Plan => MessageId::AppModePlan, AppMode::Operate => MessageId::AppModeOperate, }, @@ -75,7 +75,7 @@ impl AppModeUi for AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Yolo => MessageId::AppModeAgentHint, + AppMode::Agent => MessageId::AppModeAgentHint, AppMode::Plan => MessageId::AppModePlanHint, AppMode::Operate => MessageId::AppModeOperateHint, }, diff --git a/crates/tui/src/tui/hotbar/actions.rs b/crates/tui/src/tui/hotbar/actions.rs index 0a623716b7..3d377f7a78 100644 --- a/crates/tui/src/tui/hotbar/actions.rs +++ b/crates/tui/src/tui/hotbar/actions.rs @@ -912,7 +912,6 @@ impl AppHotbarAction { AppHotbarKind::SessionCompact => MessageId::HotbarActionSessionCompactName, AppHotbarKind::Mode(AppMode::Plan) => MessageId::HotbarActionModePlanName, AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentName, - AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloName, AppHotbarKind::Mode(AppMode::Operate) => MessageId::HotbarActionModeOperateName, AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleName, AppHotbarKind::SidebarToggle => MessageId::HotbarActionSidebarToggleName, @@ -928,7 +927,6 @@ impl AppHotbarAction { AppHotbarKind::SessionCompact => MessageId::HotbarActionSessionCompactDescription, AppHotbarKind::Mode(AppMode::Plan) => MessageId::HotbarActionModePlanDescription, AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentDescription, - AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloDescription, AppHotbarKind::Mode(AppMode::Operate) => MessageId::HotbarActionModeOperateDescription, AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleDescription, AppHotbarKind::SidebarToggle => MessageId::HotbarActionSidebarToggleDescription, diff --git a/crates/tui/src/tui/startup_defaults.rs b/crates/tui/src/tui/startup_defaults.rs index 401cc9133d..a7010c4459 100644 --- a/crates/tui/src/tui/startup_defaults.rs +++ b/crates/tui/src/tui/startup_defaults.rs @@ -822,11 +822,6 @@ mod tests { assert_eq!(scrubbed, "cannot open "); } - #[test] - fn legacy_yolo_selection_persists_the_mode_it_actually_installs() { - assert_eq!(StartupDefaults::mode(AppMode::Yolo).mode, Some("agent")); - } - #[test] fn empty_update_is_a_no_op() { assert!(StartupDefaults::default().is_empty()); diff --git a/crates/tui/src/tui/ui/apply.rs b/crates/tui/src/tui/ui/apply.rs index 343153b78f..24e47df3e9 100644 --- a/crates/tui/src/tui/ui/apply.rs +++ b/crates/tui/src/tui/ui/apply.rs @@ -616,6 +616,24 @@ pub(crate) async fn apply_mode_update( } } +/// Apply the legacy YOLO shortcut (Alt+Y): a permission change, not a mode +/// change. Same persist/report/sync contract as [`apply_mode_update`]; the +/// startup default written is the mode actually installed (Act). +pub(crate) async fn apply_yolo_compat_update( + app: &mut App, + engine_handle: &EngineHandle, + _config: &Config, +) -> bool { + let outcome = app.select_yolo_compat(); + app.report_mode_selection(AppMode::Agent, outcome); + if outcome.changed_live_state() { + sync_mode_update(app, engine_handle).await; + true + } else { + false + } +} + /// Entering Operate attaches to the recorded operation (a fresh one only /// when none exists or the last was cancelled), shows the localized lead /// plan, and keeps always-on mode durable by reinstalling the hourly lead diff --git a/crates/tui/src/tui/ui/approval_routing.rs b/crates/tui/src/tui/ui/approval_routing.rs index 989d09cb65..e26b3c1fda 100644 --- a/crates/tui/src/tui/ui/approval_routing.rs +++ b/crates/tui/src/tui/ui/approval_routing.rs @@ -3,7 +3,7 @@ use crate::audit::log_sensitive_event; use crate::core::engine::EngineHandle; use crate::localization::MessageId; -use crate::tui::app::{App, AppMode, StatusToastLevel}; +use crate::tui::app::{App, StatusToastLevel}; use crate::tui::approval::ApprovalMode; use crate::tui::history::HistoryCell; @@ -80,13 +80,13 @@ pub(super) async fn auto_deny_session_approval( } pub(super) fn app_auto_approve_enabled(app: &App) -> bool { - app.mode == AppMode::Yolo || app.approval_mode == ApprovalMode::Bypass + app.approval_mode == ApprovalMode::Bypass } /// Build the UI-side TurnAuthority for approval disposition (#4412). /// /// Shell/trust bits do not affect disposition; mode + approval_mode + the -/// full-access shape (Yolo/Bypass) are what the shared resolver consults. +/// full-access shape (Bypass) are what the shared resolver consults. fn app_turn_authority_for_approvals(app: &App) -> crate::core::authority::TurnAuthority { crate::core::authority::TurnAuthority::from_effective_fields( app.mode, diff --git a/crates/tui/src/tui/ui/event_loop.rs b/crates/tui/src/tui/ui/event_loop.rs index e843f765f3..7fb3da9328 100644 --- a/crates/tui/src/tui/ui/event_loop.rs +++ b/crates/tui/src/tui/ui/event_loop.rs @@ -6156,7 +6156,7 @@ pub(crate) async fn run_event_loop( continue; } KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::ALT) => { - apply_mode_update(app, &engine_handle, config, AppMode::Yolo).await; + apply_yolo_compat_update(app, &engine_handle, config).await; continue; } KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::ALT) => { @@ -6168,7 +6168,7 @@ pub(crate) async fn run_event_loop( continue; } KeyCode::Char('Y') if key.modifiers.contains(KeyModifiers::ALT) => { - apply_mode_update(app, &engine_handle, config, AppMode::Yolo).await; + apply_yolo_compat_update(app, &engine_handle, config).await; continue; } KeyCode::Char('P') if key.modifiers.contains(KeyModifiers::ALT) => { diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 23da8e8cd3..3a295c5243 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -5886,28 +5886,6 @@ fn full_access_auto_approves_requests_while_auto_review_holds_without_a_modal() ); app.approval_mode = ApprovalMode::Suggest; - app.mode = AppMode::Yolo; - assert_eq!( - resolve_ui_approval_disposition( - &app, - "exec_shell", - "shell:exec_shell:cargo test", - "key", - false, - ), - ApprovalRequestDisposition::AutoApprove - ); - assert_eq!( - resolve_ui_approval_disposition( - &app, - "exec_shell", - "shell:exec_shell:cargo test", - "key", - true, - ), - ApprovalRequestDisposition::AutoDenyFullAccessPolicyHold - ); - app.mode = AppMode::Agent; app.approval_session_approved .insert("shell:exec_shell:cargo test".to_string()); @@ -5935,7 +5913,7 @@ fn full_access_auto_approves_requests_while_auto_review_holds_without_a_modal() } #[test] -fn app_auto_approval_helper_covers_yolo_and_bypass_only() { +fn app_auto_approval_helper_covers_bypass_only() { let mut app = create_test_app(); app.mode = AppMode::Agent; app.approval_mode = ApprovalMode::Suggest; @@ -5946,10 +5924,6 @@ fn app_auto_approval_helper_covers_yolo_and_bypass_only() { app.approval_mode = ApprovalMode::Bypass; assert!(app_auto_approve_enabled(&app)); - - app.approval_mode = ApprovalMode::Suggest; - app.mode = AppMode::Yolo; - assert!(app_auto_approve_enabled(&app)); } #[test] @@ -5969,10 +5943,9 @@ fn auto_review_suppresses_stale_question_prompts_while_other_postures_allow_them ); } - // Compatibility shape: legacy Yolo hosts can carry a stale Auto enum, - // but their effective posture is Full Access, where questions are valid. - app.mode = AppMode::Yolo; - app.approval_mode = ApprovalMode::Auto; + // Full Access keeps questions valid, same as Auto-Review suppresses + // them only for its own stale-prompt cleanup. + app.approval_mode = ApprovalMode::Bypass; assert!(!should_suppress_user_input_prompt(&app)); } @@ -7768,13 +7741,7 @@ async fn mode_change_update_notifies_engine() { let mut engine = crate::core::engine::mock_engine_handle(); assert!( - apply_mode_update( - &mut app, - &engine.handle, - &crate::config::Config::default(), - crate::tui::app::AppMode::Yolo - ) - .await + apply_yolo_compat_update(&mut app, &engine.handle, &crate::config::Config::default()).await ); match engine.rx_op.recv().await.expect("change mode op") { @@ -14117,7 +14084,6 @@ fn test_esc_priority_order_matches_cancel_stack() { let mut app = create_test_app(); app.is_loading = true; app.input = "draft".to_string(); - app.mode = AppMode::Yolo; assert_eq!(next_escape_action(&app, false), EscapeAction::CancelRequest); app.input.clear(); diff --git a/crates/tui/src/tui/underwater.rs b/crates/tui/src/tui/underwater.rs index 7049355cf1..86537b23ba 100644 --- a/crates/tui/src/tui/underwater.rs +++ b/crates/tui/src/tui/underwater.rs @@ -507,11 +507,7 @@ fn header_mode_ink(mode: AppMode) -> ChromeInk { match mode { AppMode::Plan => ChromeInk::PolicyPlan, AppMode::Operate => ChromeInk::PolicyOperate, - // YOLO stays Policy, not Failure — the header must not spend red - // on a selected mode. It wears the act badge because `mode_label` - // resolves it to act; the posture it implies is the permission - // chip's Cognition ink, not this one. - AppMode::Agent | AppMode::Yolo => ChromeInk::PolicyAct, + AppMode::Agent => ChromeInk::PolicyAct, } } @@ -731,7 +727,7 @@ pub(crate) fn phase_marker_with_activity( fn mode_label(locale: Locale, mode: AppMode) -> Cow<'static, str> { match mode { - AppMode::Agent | AppMode::Yolo => tr(locale, MessageId::ChipModeAct), + AppMode::Agent => tr(locale, MessageId::ChipModeAct), AppMode::Plan => tr(locale, MessageId::ChipModePlan), AppMode::Operate => tr(locale, MessageId::ChipModeOperate), } diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index e5994a3781..da5faaf95b 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -1342,7 +1342,7 @@ impl<'a> ComposerWidget<'a> { fn mode_color(&self) -> Color { match self.app.mode { - AppMode::Agent | AppMode::Yolo => self.app.ui_theme.mode_agent, + AppMode::Agent => self.app.ui_theme.mode_agent, AppMode::Plan => self.app.ui_theme.mode_plan, AppMode::Operate => self.app.ui_theme.mode_operate, } From 1ce049e1f9508023a34348b858c145a3c798050f Mon Sep 17 00:00:00 2001 From: CodeWhale Bot Date: Wed, 2 Sep 2026 12:30:08 -0700 Subject: [PATCH 3/3] config: delete VerifierVerdictPolicy and the harness_profiles schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit slice: dead/pretender mode abstractions (item 3). VerifierVerdictPolicy was a single-value enum ("hunt" only) — one value is not a setting. Delete the enum and the [verifier].verdict_policy field. ConfigToml has no deny_unknown_fields, so an old config that still carries the key keeps loading and the key is read-only dropped (never re-serialized), matching the retired launch_screen pattern. harness_profiles (ConfigToml field, resolve_harness_profile helper, and the whole crates/config/src/harness.rs type family) was accepted and serialized but had no runtime consumer — harness.rs said so itself ("wired later"). Per the no-framework-without-a-caller rule, remove the writable schema entry and delete the dead types with it. Old configs with [[harness_profiles]] tables keep parsing (unknown keys ignored). The cli bundle exporter drops its harness routing (the key can no longer occur); the portable bundle's profiles table remains for importing older bundles. Docs and config.example.toml drop the matching schema sections. Tests updated (not weakened): the Hunt assertions become a legacy-config loads-cleanly test; the twelve harness-only tests are removed with the feature they encoded. Gates: cargo fmt --check; nextest codewhale-config 626 passed; codewhale-tui 11908 passed; codewhale-command-contract 29 passed; codewhale-cli 332 passed; dead-code budget PASS 425; vocabulary gate exit 0. --- config.example.toml | 22 --- crates/cli/src/config_bundles.rs | 25 +-- crates/config/src/harness.rs | 247 ------------------------- crates/config/src/lib.rs | 52 +----- crates/config/src/tests.rs | 303 +------------------------------ crates/tui/src/config/tests.rs | 17 +- docs/CONFIGURATION.md | 33 ---- 7 files changed, 14 insertions(+), 685 deletions(-) delete mode 100644 crates/config/src/harness.rs diff --git a/config.example.toml b/config.example.toml index f4646413c1..02a1ed5df6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -994,11 +994,9 @@ max_subagents = 10 # optional (default 64, clamped to 1-128) # ───────────────────────────────────────────────────────────────────────────────── # Enables automatic claim-of-done verifier preview once the runtime trigger is # active. Manual `run_verifiers` remains available even when this is false. -# The shipped policy maps pass/partial/fail to hunted/wounded/escaped. # # [verifier] # enabled = false -# verdict_policy = "hunt" # ───────────────────────────────────────────────────────────────────────────────── # Advisor / Watcher (#3982) @@ -1189,26 +1187,6 @@ exponential_base = 2.0 # Bash = 2048 # shell output synthesised aggressively # Web = 8192 # web results can be large; give them more room -# ───────────────────────────────────────────────────────────────────────────────── -# Harness Profiles (preview schema; runtime consumption follows later) -# ───────────────────────────────────────────────────────────────────────────────── -# Harness profiles let future Codewhale runtime slices select model-specific -# prompt, context, tool, and subagent posture. v0.9 parses, validates, and can -# resolve profiles for tests/status plumbing, but normal Agent and Workflow -# runs do not silently promote or mutate behavior from these profiles yet. -# -# [[harness_profiles]] -# provider_route = "deepseek" -# model_pattern = "deepseek-v4.*" -# -# [harness_profiles.posture] -# kind = "cache-heavy" # standard | cache-heavy | lean | custom -# max_subagents = 10 # 0 means runtime default -# prefer_codebase_search = false -# compaction_strategy = "prefix-cache" # default | prefix-cache | aggressive -# tool_surface = "full" # full | read-only | auto -# safety_posture = "standard" # standard | strict | permissive - # ───────────────────────────────────────────────────────────────────────────────── # Profile Example (for multiple environments) # ───────────────────────────────────────────────────────────────────────────────── diff --git a/crates/cli/src/config_bundles.rs b/crates/cli/src/config_bundles.rs index f44750fb43..544d3b6d09 100644 --- a/crates/cli/src/config_bundles.rs +++ b/crates/cli/src/config_bundles.rs @@ -845,7 +845,7 @@ pub fn export_bundle( metadata: BundleMetadata, ) -> Result { let mut preferences = BundleTable::default(); - let mut profiles = BundleTable::default(); + let profiles = BundleTable::default(); let mut global = BundleTable::default(); let mut project = BundleTable::default(); @@ -855,9 +855,6 @@ pub fn export_bundle( ExportSection::Preferences => { preferences.entries.insert(key, value); } - ExportSection::Profiles => { - profiles.entries.insert(key, value); - } ExportSection::Global => { global.entries.insert(key, value); } @@ -918,16 +915,12 @@ const MACHINE_SPECIFIC_KEYS: [&str; 14] = [ enum ExportSection { Preferences, - Profiles, Global, Project, Drop, } fn export_section_for(key: &str, scope: BundleScope) -> ExportSection { - if key.starts_with("harness") || key.contains("harness_profiles") { - return ExportSection::Profiles; - } if key.starts_with("skills") || key.starts_with("tools") || key.starts_with("snapshots") { return ExportSection::Preferences; } @@ -2231,28 +2224,12 @@ max_age_days = 11 [portable_table] enabled = true count = 4 - -[[harness_profiles]] -provider_route = "deepseek" -model_pattern = "deepseek-v4-*" - -[harness_profiles.posture] -kind = "custom" -max_subagents = 3 -prefer_codebase_search = true -compaction_strategy = "prefix-cache" -tool_surface = "read-only" -safety_posture = "strict" "#, ) .expect("typed config parses"); let bundle = export_bundle(&config, BundleScope::Global, BundleMetadata::default()) .expect("typed export"); - assert!(matches!( - bundle.profiles.entries.get("harness_profiles"), - Some(toml::Value::Array(_)) - )); assert!(matches!( bundle.preferences.entries.get("skills"), Some(toml::Value::Table(_)) diff --git a/crates/config/src/harness.rs b/crates/config/src/harness.rs deleted file mode 100644 index f99fa33a11..0000000000 --- a/crates/config/src/harness.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Harness posture + profile config types (#3311). -//! -//! A *harness posture* is the agent-shaping policy (sub-agent cap, tool -//! surface, compaction/cache strategy, safety stance); a *harness profile* -//! binds a posture to a provider route + model pattern. Extracted verbatim -//! from lib.rs to separate this agent-posture domain from the rest of the -//! config schema; re-exported at the crate root so existing paths are -//! unchanged. Behavior is identical. - -use std::sync::OnceLock; - -use serde::{Deserialize, Serialize}; - -use crate::ProviderKind; - -/// Kinds of built-in harness postures. -/// -/// A posture names the runtime strategy CodeWhale should use for a -/// provider/model route: how much context to preload, how aggressively to lean -/// on sub-agents, and how to balance prompt-cache stability against quick -/// exploration. Runtime selection is wired in later v0.9 slices; this config -/// model intentionally keeps the policy data explicit first. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "kebab-case")] -pub enum HarnessPostureKind { - /// Full-featured default: rich constitution, broad tool catalog, and normal - /// sub-agent posture. - #[default] - Standard, - /// Cache-heavy: deeper prompt layering and prefix-cache-oriented context. - CacheHeavy, - /// Lean: smaller starting context, faster compaction, and stronger - /// exploration/delegation bias. - Lean, - /// User-defined posture assembled from explicit knobs below. - Custom, -} - -/// How this posture should approach compaction and prompt-cache stability. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "kebab-case")] -pub enum HarnessCompactionStrategy { - #[default] - Default, - PrefixCache, - Aggressive, -} - -/// Which tool catalog shape this posture prefers. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "kebab-case")] -pub enum HarnessToolSurface { - #[default] - Full, - ReadOnly, - Auto, -} - -/// Safety posture applied when the runtime consumes a harness profile. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "kebab-case")] -pub enum HarnessSafetyPosture { - #[default] - Standard, - Strict, - Permissive, -} - -/// A concrete harness posture with policy knobs. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct HarnessPosture { - /// Named posture kind. - #[serde(default)] - pub kind: HarnessPostureKind, - /// Maximum number of concurrent sub-agents (0 = runtime default). - #[serde(default)] - pub max_subagents: usize, - /// Prefer search-based/on-demand context over always-on documentation. - #[serde(default)] - pub prefer_codebase_search: bool, - /// Compaction and prompt-cache strategy. - #[serde(default)] - pub compaction_strategy: HarnessCompactionStrategy, - /// Preferred tool catalog shape. - #[serde(default)] - pub tool_surface: HarnessToolSurface, - /// Safety posture for runtime consumers. - #[serde(default)] - pub safety_posture: HarnessSafetyPosture, -} - -impl Default for HarnessPosture { - fn default() -> Self { - Self { - kind: HarnessPostureKind::Standard, - max_subagents: 0, - prefer_codebase_search: false, - compaction_strategy: HarnessCompactionStrategy::default(), - tool_surface: HarnessToolSurface::default(), - safety_posture: HarnessSafetyPosture::default(), - } - } -} - -impl HarnessPosture { - /// A cache-heavy posture tuned for DeepSeek V4 / MiMo-style models. - #[must_use] - pub fn cache_heavy() -> Self { - Self { - kind: HarnessPostureKind::CacheHeavy, - max_subagents: 10, - prefer_codebase_search: false, - compaction_strategy: HarnessCompactionStrategy::PrefixCache, - tool_surface: HarnessToolSurface::Full, - safety_posture: HarnessSafetyPosture::Standard, - } - } - - /// A lean posture for smaller-context or weaker tool-use models. - #[must_use] - pub fn lean() -> Self { - Self { - kind: HarnessPostureKind::Lean, - max_subagents: 20, - prefer_codebase_search: true, - compaction_strategy: HarnessCompactionStrategy::Aggressive, - tool_surface: HarnessToolSurface::Full, - safety_posture: HarnessSafetyPosture::Standard, - } - } -} - -/// A harness profile binds a posture to a provider route and model pattern. -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct HarnessProfile { - /// Provider route this profile applies to, e.g. "deepseek" or - /// "xiaomi-mimo". - pub provider_route: String, - /// Regex or glob pattern for model names, e.g. "deepseek-v4.*". - pub model_pattern: String, - /// The posture to apply. - #[serde(default)] - pub posture: HarnessPosture, -} - -impl HarnessProfile { - /// Return true when this profile applies to the provider/model route. - /// - /// This is a pure config helper: matching a profile must not mutate runtime - /// provider selection, prompts, auth, tools, context, or persisted config. - #[must_use] - pub fn matches_route(&self, provider_route: &str, model: &str) -> bool { - provider_routes_equal(&self.provider_route, provider_route) - && wildcard_pattern_matches(&self.model_pattern, model) - } -} - -/// Built-in profile seeds for common provider/model families. -/// -/// User-configured profiles are always checked first; these seeds only provide -/// a stable resolver result when config has no narrower match. -#[must_use] -pub fn built_in_harness_profiles() -> &'static [HarnessProfile] { - static PROFILES: OnceLock> = OnceLock::new(); - PROFILES.get_or_init(|| { - vec![ - HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4*".to_string(), - posture: HarnessPosture::cache_heavy(), - }, - HarnessProfile { - provider_route: "xiaomi-mimo".to_string(), - model_pattern: "mimo-v2.5*".to_string(), - posture: HarnessPosture::cache_heavy(), - }, - HarnessProfile { - provider_route: "arcee".to_string(), - model_pattern: "trinity-large-thinking".to_string(), - posture: HarnessPosture::cache_heavy(), - }, - HarnessProfile { - provider_route: "huggingface".to_string(), - model_pattern: "*".to_string(), - posture: HarnessPosture::lean(), - }, - HarnessProfile { - provider_route: "sglang".to_string(), - model_pattern: "*".to_string(), - posture: HarnessPosture::lean(), - }, - HarnessProfile { - provider_route: "vllm".to_string(), - model_pattern: "*".to_string(), - posture: HarnessPosture::lean(), - }, - HarnessProfile { - provider_route: "ollama".to_string(), - model_pattern: "*".to_string(), - posture: HarnessPosture::lean(), - }, - ] - }) -} - -fn provider_routes_equal(expected: &str, actual: &str) -> bool { - match (ProviderKind::parse(expected), ProviderKind::parse(actual)) { - (Some(expected), Some(actual)) => expected == actual, - _ => expected.trim().eq_ignore_ascii_case(actual.trim()), - } -} - -fn wildcard_pattern_matches(pattern: &str, value: &str) -> bool { - wildcard_chars_match( - &pattern.chars().collect::>(), - &value.chars().collect::>(), - ) -} - -fn wildcard_chars_match(pattern: &[char], value: &[char]) -> bool { - let (mut pattern_idx, mut value_idx) = (0, 0); - let mut star_idx: Option = None; - let mut star_value_idx = 0; - - while value_idx < value.len() { - if pattern_idx < pattern.len() - && (pattern[pattern_idx] == '?' || pattern[pattern_idx] == value[value_idx]) - { - pattern_idx += 1; - value_idx += 1; - } else if pattern_idx < pattern.len() && pattern[pattern_idx] == '*' { - star_idx = Some(pattern_idx); - pattern_idx += 1; - star_value_idx = value_idx; - } else if let Some(star) = star_idx { - pattern_idx = star + 1; - star_value_idx += 1; - value_idx = star_value_idx; - } else { - return false; - } - } - - pattern[pattern_idx..].iter().all(|ch| *ch == '*') -} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 703f81107b..60db5ec012 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -6,7 +6,6 @@ mod config_document; pub mod descriptors; pub mod device_code; pub mod external_credentials; -mod harness; pub mod model_reference; pub mod models_dev; pub mod persistence; @@ -24,10 +23,6 @@ pub use config_document::{ create_config_document, mutate_config_document, replace_config_document_if_unchanged, set_config_document_value, unset_config_document_value, }; -pub use harness::{ - HarnessCompactionStrategy, HarnessPosture, HarnessPostureKind, HarnessProfile, - HarnessSafetyPosture, HarnessToolSurface, built_in_harness_profiles, -}; pub use model_reference::{Modality, ModelReferenceCard, ModelReferenceDatabase}; pub(crate) use provider_defaults::*; pub use provider_kind::ProviderKind; @@ -866,10 +861,6 @@ pub struct ConfigToml { /// applies the defaults documented in [`LspConfigToml`]. #[serde(default)] pub lsp: Option, - /// Per-model harness profiles (#2693). Runtime wiring lands in follow-up - /// v0.9 slices; this is the durable config data model. - #[serde(default)] - pub harness_profiles: Vec, /// Optional 1-8 hotbar slot bindings (#2064). When absent, the TUI falls /// back to the built-in default slots. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1223,23 +1214,6 @@ fn insert_provider_config_values( } impl ConfigToml { - /// Resolve the first configured harness profile for a provider/model route. - /// - /// This helper is deliberately dormant for v0.9: callers may display or - /// test the resolved profile, but runtime provider/model routing and prompt - /// shaping remain unchanged until a later, explicit integration slice. - #[must_use] - pub fn resolve_harness_profile( - &self, - provider_route: &str, - model: &str, - ) -> Option<&HarnessProfile> { - self.harness_profiles - .iter() - .chain(built_in_harness_profiles().iter()) - .find(|profile| profile.matches_route(provider_route, model)) - } - /// Resolve durable hotbar config into normalized 1-8 slot bindings. /// /// `known_action_ids` is supplied by the TUI action registry in later @@ -2486,38 +2460,14 @@ pub fn built_in_role_presets() -> BTreeMap { .into() } -/// Verdict policy for the verifier-preview surface (#2093). -/// -/// Only the hunt vocabulary is shipped today. Keeping this typed lets future -/// policy additions reject misspellings instead of silently accepting unknown -/// strings. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "snake_case")] -pub enum VerifierVerdictPolicy { - #[default] - Hunt, -} - /// On-disk schema for `[verifier]`. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct VerifierConfigToml { /// Enable automatic verifier preview when the runtime wires a /// claim-of-done trigger. Manual `run_verifiers` remains available /// regardless. #[serde(default)] pub enabled: bool, - /// How verifier verdicts map into the goal/hunt system. - #[serde(default)] - pub verdict_policy: VerifierVerdictPolicy, -} - -impl Default for VerifierConfigToml { - fn default() -> Self { - Self { - enabled: false, - verdict_policy: VerifierVerdictPolicy::Hunt, - } - } } /// On-disk schema for `[advisor]` (#3982). diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index 266de73b1b..83cf0fcc93 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -29,34 +29,24 @@ fn network_policy_toml_deserializes_proxy_hosts() { } #[test] -fn verifier_config_defaults_to_hunt_verdict_policy() { +fn retired_verifier_verdict_policy_is_accepted_and_dropped_on_load() { + // `verdict_policy` was a single-value setting retired with the + // verifier-preview schema cleanup. Old configs that still carry it must + // keep loading; the key is read-only dropped (never written back). let config: ConfigToml = toml::from_str( r#" [verifier] enabled = true + verdict_policy = "hunt" + + [verifier.unknown_extra] + key = "value" "#, ) - .expect("verifier config toml"); + .expect("a legacy verifier table must still parse"); let verifier = config.verifier.expect("verifier table"); assert!(verifier.enabled); - assert_eq!(verifier.verdict_policy, VerifierVerdictPolicy::Hunt); -} - -#[test] -fn verifier_config_rejects_unknown_verdict_policy() { - let err = toml::from_str::( - r#" - [verifier] - verdict_policy = "strict" - "#, - ) - .expect_err("only the shipped hunt policy should parse"); - - assert!( - err.message().contains("unknown variant"), - "unexpected error: {err}" - ); } #[test] @@ -8106,281 +8096,6 @@ fn fallback_providers_do_not_change_runtime_resolution() { assert_eq!(resolved.provider, ProviderKind::NvidiaNim); } -#[test] -fn harness_posture_default_is_standard() { - let posture = HarnessPosture::default(); - - assert_eq!( - posture, - HarnessPosture { - kind: HarnessPostureKind::Standard, - max_subagents: 0, - prefer_codebase_search: false, - compaction_strategy: HarnessCompactionStrategy::Default, - tool_surface: HarnessToolSurface::Full, - safety_posture: HarnessSafetyPosture::Standard, - } - ); -} - -#[test] -fn harness_posture_factories_are_typed() { - assert_eq!( - HarnessPosture::cache_heavy(), - HarnessPosture { - kind: HarnessPostureKind::CacheHeavy, - max_subagents: 10, - prefer_codebase_search: false, - compaction_strategy: HarnessCompactionStrategy::PrefixCache, - tool_surface: HarnessToolSurface::Full, - safety_posture: HarnessSafetyPosture::Standard, - } - ); - assert_eq!( - HarnessPosture::lean(), - HarnessPosture { - kind: HarnessPostureKind::Lean, - max_subagents: 20, - prefer_codebase_search: true, - compaction_strategy: HarnessCompactionStrategy::Aggressive, - tool_surface: HarnessToolSurface::Full, - safety_posture: HarnessSafetyPosture::Standard, - } - ); -} - -#[test] -fn harness_profile_serde_round_trips_as_a_whole_struct() { - let profile = HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4.*".to_string(), - posture: HarnessPosture::cache_heavy(), - }; - - let json = serde_json::to_string(&profile).expect("serialize profile"); - let round_tripped: HarnessProfile = serde_json::from_str(&json).expect("deserialize profile"); - - assert_eq!(round_tripped, profile); -} - -#[test] -fn config_toml_accepts_harness_profiles() { - let config: ConfigToml = toml::from_str( - r#" -provider = "deepseek" -model = "deepseek-v4-pro" - -[[harness_profiles]] -provider_route = "deepseek" -model_pattern = "deepseek-v4.*" - -[harness_profiles.posture] -kind = "cache-heavy" -max_subagents = 10 -compaction_strategy = "prefix-cache" -tool_surface = "read-only" -safety_posture = "strict" -"#, - ) - .expect("parse harness profiles"); - - assert_eq!( - config.harness_profiles, - vec![HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4.*".to_string(), - posture: HarnessPosture { - kind: HarnessPostureKind::CacheHeavy, - max_subagents: 10, - prefer_codebase_search: false, - compaction_strategy: HarnessCompactionStrategy::PrefixCache, - tool_surface: HarnessToolSurface::ReadOnly, - safety_posture: HarnessSafetyPosture::Strict, - }, - }] - ); -} - -#[test] -fn harness_profile_matches_provider_alias_and_model_wildcard() { - let profile = HarnessProfile { - provider_route: "xiaomi-mimo".to_string(), - model_pattern: "mimo-v2.?-pro".to_string(), - posture: HarnessPosture::cache_heavy(), - }; - - assert!(profile.matches_route("mimo", "mimo-v2.5-pro")); - assert!(!profile.matches_route("mimo", "mimo-v2.50-pro")); - assert!(!profile.matches_route("deepseek", "mimo-v2.5-pro")); -} - -#[test] -fn resolve_harness_profile_returns_first_matching_profile() { - let config = ConfigToml { - harness_profiles: vec![ - HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4-flash".to_string(), - posture: HarnessPosture::lean(), - }, - HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4-*".to_string(), - posture: HarnessPosture::cache_heavy(), - }, - ], - ..ConfigToml::default() - }; - - let flash = config - .resolve_harness_profile("deepseek-cn", "deepseek-v4-flash") - .expect("exact profile should match first"); - assert_eq!(flash.posture.kind, HarnessPostureKind::Lean); - - let pro = config - .resolve_harness_profile("deepseek", "deepseek-v4-pro") - .expect("wildcard profile should match pro model"); - assert_eq!(pro.posture.kind, HarnessPostureKind::CacheHeavy); -} - -#[test] -fn resolve_harness_profile_uses_built_in_seed_when_config_has_no_match() { - let config = ConfigToml::default(); - - let xiaomi = config - .resolve_harness_profile("xiaomi", "mimo-v2.5-pro") - .expect("direct Xiaomi MiMo seed should resolve"); - assert_eq!(xiaomi.provider_route, "xiaomi-mimo"); - assert_eq!(xiaomi.posture.kind, HarnessPostureKind::CacheHeavy); - - let arcee = config - .resolve_harness_profile("arcee", "trinity-large-thinking") - .expect("direct Arcee seed should resolve"); - assert_eq!(arcee.posture.kind, HarnessPostureKind::CacheHeavy); - - let local = config - .resolve_harness_profile("vllm", "Qwen/Qwen3.6-Coder") - .expect("local seed should resolve"); - assert_eq!(local.posture.kind, HarnessPostureKind::Lean); - assert!(local.posture.prefer_codebase_search); -} - -#[test] -fn configured_harness_profile_overrides_built_in_seed() { - let config = ConfigToml { - harness_profiles: vec![HarnessProfile { - provider_route: "xiaomi-mimo".to_string(), - model_pattern: "mimo-v2.5-pro".to_string(), - posture: HarnessPosture { - kind: HarnessPostureKind::Custom, - max_subagents: 3, - prefer_codebase_search: true, - compaction_strategy: HarnessCompactionStrategy::Default, - tool_surface: HarnessToolSurface::Auto, - safety_posture: HarnessSafetyPosture::Strict, - }, - }], - ..ConfigToml::default() - }; - - let profile = config - .resolve_harness_profile("xiaomi-mimo", "mimo-v2.5-pro") - .expect("configured profile should match first"); - - assert_eq!(profile.posture.kind, HarnessPostureKind::Custom); - assert_eq!(profile.posture.max_subagents, 3); - assert_eq!(profile.posture.tool_surface, HarnessToolSurface::Auto); - assert_eq!(profile.posture.safety_posture, HarnessSafetyPosture::Strict); -} - -#[test] -fn resolve_harness_profile_returns_none_when_route_or_model_misses() { - let config = ConfigToml { - harness_profiles: vec![HarnessProfile { - provider_route: "huggingface".to_string(), - model_pattern: "deepseek-ai/*".to_string(), - posture: HarnessPosture::lean(), - }], - ..ConfigToml::default() - }; - - assert!( - config - .resolve_harness_profile("openrouter", "deepseek-ai/DeepSeek-V4-Pro") - .is_none() - ); - assert!( - config - .resolve_harness_profile("deepseek", "Qwen/Qwen3.6-Coder") - .is_none() - ); - assert!( - config - .resolve_harness_profile("openai", "mimo-v2.5-pro") - .is_none() - ); -} - -#[test] -fn resolving_harness_profile_does_not_change_runtime_options() { - let _lock = env_lock(); - let _env = EnvGuard::without_deepseek_runtime_overrides(); - let config = ConfigToml { - provider: ProviderKind::Deepseek, - model: Some("deepseek-v4-pro".to_string()), - harness_profiles: vec![HarnessProfile { - provider_route: "deepseek".to_string(), - model_pattern: "deepseek-v4-*".to_string(), - posture: HarnessPosture::lean(), - }], - ..ConfigToml::default() - }; - - let profile = config - .resolve_harness_profile("deepseek", "deepseek-v4-pro") - .expect("profile should resolve for display/future runtime"); - assert_eq!(profile.posture.kind, HarnessPostureKind::Lean); - - let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default()); - assert_eq!(resolved.provider, ProviderKind::Deepseek); - assert_eq!(resolved.model, "deepseek-v4-pro"); -} - -#[test] -fn harness_posture_kind_rejects_unknown_values() { - let err = toml::from_str::( - r#" -[[harness_profiles]] -provider_route = "deepseek" -model_pattern = "deepseek-v4.*" - -[harness_profiles.posture] -kind = "cahce-heavy" -"#, - ) - .expect_err("misspelled kind should not deserialize as custom"); - - assert!(err.to_string().contains("cahce-heavy")); -} - -#[test] -fn harness_posture_rejects_unknown_policy_keys() { - let err = toml::from_str::( - r#" -[[harness_profiles]] -provider_route = "deepseek" -model_pattern = "deepseek-v4.*" - -[harness_profiles.posture] -kind = "custom" -unknown_policy = "surprise" -"#, - ) - .expect_err("unknown posture keys should not be ignored"); - - assert!(err.to_string().contains("unknown_policy")); -} - #[test] fn test_verbosity_resolution() { let _lock = env_lock(); diff --git a/crates/tui/src/config/tests.rs b/crates/tui/src/config/tests.rs index 9f5bfb7475..edbe0dec29 100644 --- a/crates/tui/src/config/tests.rs +++ b/crates/tui/src/config/tests.rs @@ -965,36 +965,25 @@ fn network_policy_toml_maps_proxy_hosts_to_runtime_policy() { } #[test] -fn verifier_config_parses_hunt_policy_and_merges_overrides() { +fn verifier_config_parses_and_merges_overrides() { let config: Config = toml::from_str( r#" [verifier] enabled = true - verdict_policy = "hunt" "#, ) .expect("parse verifier config"); let verifier = config.verifier.expect("verifier table"); assert!(verifier.enabled); - assert_eq!( - verifier.verdict_policy, - codewhale_config::VerifierVerdictPolicy::Hunt - ); let merged = merge_config( Config { - verifier: Some(codewhale_config::VerifierConfigToml { - enabled: false, - verdict_policy: codewhale_config::VerifierVerdictPolicy::Hunt, - }), + verifier: Some(codewhale_config::VerifierConfigToml { enabled: false }), ..Config::default() }, Config { - verifier: Some(codewhale_config::VerifierConfigToml { - enabled: true, - verdict_policy: codewhale_config::VerifierVerdictPolicy::Hunt, - }), + verifier: Some(codewhale_config::VerifierConfigToml { enabled: true }), ..Config::default() }, ); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9652c7c063..47bbd5f927 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -945,35 +945,6 @@ Select a profile with: If a profile is selected but missing, codewhale exits with an error listing available profiles. -## Harness Profiles - -v0.9 adds a config data model for model-specific harness posture. This is a -preview schema: it can be parsed and tested, but runtime provider/model -selection and prompt/tool behavior are wired in later v0.9 slices. -When no configured profile matches, the resolver falls back to built-in seed -profiles for the model families listed in the cutline doc. Configured profiles -always take precedence over those seeds. - -```toml -[[harness_profiles]] -provider_route = "deepseek" -model_pattern = "deepseek-v4.*" - -[harness_profiles.posture] -kind = "cache-heavy" # standard | cache-heavy | lean | custom -max_subagents = 10 # 0 means runtime default -prefer_codebase_search = false -compaction_strategy = "prefix-cache" # default | prefix-cache | aggressive -tool_surface = "full" # full | read-only | auto -safety_posture = "standard" # standard | strict | permissive -``` - -Unknown posture names or unknown keys inside a harness profile fail config -deserialization instead of silently becoming `custom`. That is intentional: -once runtime wiring consumes these profiles, a typo should be visible. -The v0.9 implementation order and automatic-creator boundary are documented in -[`HARNESS_PROFILE_CUTLINE.md`](rfcs/HARNESS_PROFILE_CUTLINE.md). - ## Environment Variables Most runtime environment variables override config values. API-key variables are @@ -2169,10 +2140,6 @@ reasoning contract, and all four membership ids omit generic sampling fields. - `[verifier].enabled` (bool, default `false`): enables automatic claim-of-done verifier preview once that runtime trigger is active. The manual `run_verifiers` tool is still available when this is false. -- `[verifier].verdict_policy` (string, default `"hunt"`): maps verifier - `pass` / `partial` / `fail` into the goal verdict vocabulary - `hunted` / `wounded` / `escaped`. `"hunt"` is the only shipped policy today; - unknown values are rejected so future policies can be added deliberately. - `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent. Custom paths must be absolute; a relative value falls back to the user-global