| 1 | use std::path::{Path, PathBuf}; |
| 2 | |
| 3 | use anyhow::{Context |
| 4 | use serde::Deserialize |
| 5 | |
| 6 | use crate::tools::ToolDef |
| 7 | |
| 8 | /// Agent operating modes, mirroring Claude Code's mode system: |
| 9 | /// |
| 10 | /// * **auto** — full autonomy: every tool is available and runs without asking. |
| 11 | /// * **accept** (accept-edits) — read/explore freely, but every file mutation |
| 12 | /// (write/edit/delete) pauses for explicit user approval before it runs. |
| 13 | /// * **plan** — read-only research: no file changes and no shell commands. |
| 14 | /// Sub-agents are restricted to read-only `explore` type. |
| 15 | /// * **manual** — text-only: no tools at all. The model can only answer and |
| 16 | /// discuss, never touch the filesystem. |
| 17 | /// |
| 18 | /// Users can add their own modes as JSON files in `~/.coding-agent/modes/` |
| 19 | /// (see [`AnyMode`]). |
| 20 | #[derive(Clone |
| 21 | pub enum Mode |
| 22 | Auto, |
| 23 | Accept, |
| 24 | Plan, |
| 25 | Manual, |
| 26 | } |
| 27 | |
| 28 | impl Mode { |
| 29 | pub const AUTO |
| 30 | pub const ACCEPT |
| 31 | pub const PLAN |
| 32 | pub const MANUAL |
| 33 | |
| 34 | pub fn all() |
| 35 | [Mode::Auto |
| 36 | } |
| 37 | |
| 38 | /// Parse a mode from a user-facing slug. Accepts the exact names plus a few |
| 39 | /// friendly aliases (`default`, `accept-edits`, `text`, …). |
| 40 | pub fn parse( |
| 41 | match s.trim() |
| 42 | ref m if m |
| 43 | ref m if m |
| 44 | ref m if m |
| 45 | ref m if m |
| 46 | Some(Mode |
| 47 | } |
| 48 | _ => None |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | pub fn name( |
| 53 | match self { |
| 54 | Mode::Auto |
| 55 | Mode::Accept |
| 56 | Mode::Plan |
| 57 | Mode::Manual |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | pub fn label( |
| 62 | match self { |
| 63 | Mode::Auto |
| 64 | Mode::Accept |
| 65 | Mode::Plan |
| 66 | Mode::Manual |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | /// Dock indicator symbol, Claude Code style. Auto/accept run "forward" |
| 71 | /// (▶); plan and manual pause everything (⏸). |
| 72 | pub fn symbol( |
| 73 | match self { |
| 74 | Mode::Auto |
| 75 | Mode::Plan |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /// Dock indicator color (hex) so each mode reads differently at a glance. |
| 80 | pub fn color_hex( |
| 81 | match self { |
| 82 | Mode::Auto |
| 83 | Mode::Accept |
| 84 | Mode::Plan |
| 85 | Mode::Manual |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | /// The system prompt used for this mode. Each mode gets its own |
| 90 | /// personality so the model behaves the way the mode promises. |
| 91 | pub fn system_prompt( |
| 92 | match self { |
| 93 | Mode::Auto |
| 94 | Mode::Accept |
| 95 | Mode::Plan |
| 96 | Mode::Manual |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | /// Tools whose execution must be approved by the user first. In |
| 101 | /// accept-edits mode that is every file-mutating tool. |
| 102 | pub fn requires_approval( |
| 103 | matches!(self, |
| 104 | } |
| 105 | |
| 106 | /// Whether this mode allows executing the named tool. This is the |
| 107 | /// execution-time gate (mirrors opencode's `permission.assert` deny rule): |
| 108 | /// even if the model hallucinates a call that was not offered in the tool |
| 109 | /// list, the call is blocked before it runs. |
| 110 | pub fn allows_tool( |
| 111 | match self { |
| 112 | Mode::Auto |
| 113 | Mode::Plan |
| 114 | Mode::Manual |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /// Filter the full tool set down to what this mode allows. |
| 119 | pub fn filter_tools( |
| 120 | match self { |
| 121 | Mode::Auto |
| 122 | Mode::Plan |
| 123 | .into_iter() |
| 124 | .filter(|t| |
| 125 | .map(|t| |
| 126 | if t.name == |
| 127 | // Plan mode's task tool only offers read-only 'explore' |
| 128 | // sub-agents; advertise exactly that. |
| 129 | crate::tools::read_only_task_tool( |
| 130 | } else { |
| 131 | t |
| 132 | } |
| 133 | }) |
| 134 | .collect(), |
| 135 | Mode::Manual |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | /// Sub-agents spawned while this mode is active. Plan mode may still use |
| 140 | /// the `task` tool for read-only research, so any other sub-agent type is |
| 141 | /// coerced to the read-only `explore` variant. |
| 142 | pub fn enforce_subagent( |
| 143 | match self { |
| 144 | Mode::Plan |
| 145 | _ => subagent_type. |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | // ── Custom modes ───────────────────────────────────────────────────────── |
| 151 | // |
| 152 | // Users drop JSON files into ~/.coding-agent/modes/*.json to define their own |
| 153 | // modes. Each file: |
| 154 | // |
| 155 | // ```json |
| 156 | // { |
| 157 | // "name": "review", |
| 158 | // "label": "code review", |
| 159 | // "prompt": "You are a senior reviewer. Explore the code and critique it.", |
| 160 | // "tools": ["read_file", "list_files", "search"], |
| 161 | // "color": "#81a2be", |
| 162 | // "symbol": "🔍" |
| 163 | // } |
| 164 | // ``` |
| 165 | // |
| 166 | // `tools` restricts the agent to that allowlist; `"*"` means every tool and an |
| 167 | // empty list means no tools at all (text-only). `color`/`symbol` tint the dock |
| 168 | // indicator, like the built-in modes. A custom mode never collides with a |
| 169 | // built-in name; invalid files are skipped with a warning. |
| 170 | |
| 171 | /// A user-defined mode loaded from the modes directory. |
| 172 | #[derive(Clone |
| 173 | #[serde(default)] |
| 174 | pub struct CustomMode |
| 175 | /// Slug persisted on the session (e.g. `review`). Must not shadow a |
| 176 | /// built-in mode name. |
| 177 | pub name: String |
| 178 | /// Human label shown in the dock, e.g. "code review". |
| 179 | pub label: String |
| 180 | /// System prompt sent to the model in this mode. |
| 181 | pub prompt: String |
| 182 | /// Allowed tool names (e.g. `read_file`). `"*"` = every tool, `[]` = none. |
| 183 | pub tools: Vec |
| 184 | /// Dock color as `#rrggbb`. |
| 185 | pub color: Option |
| 186 | /// Dock indicator symbol, e.g. `🔍`. |
| 187 | pub symbol: Option |
| 188 | } |
| 189 | |
| 190 | /// A mode a session can run in: one of the four built-ins or a user-defined |
| 191 | /// [`CustomMode`]. Everything the agent needs (system prompt, tool set, |
| 192 | /// approval rules, dock symbol/color) is resolved through this type. |
| 193 | #[derive(Clone |
| 194 | pub enum AnyMode |
| 195 | Builtin(Mode), |
| 196 | Custom(CustomMode), |
| 197 | } |
| 198 | |
| 199 | impl AnyMode { |
| 200 | pub fn slug( |
| 201 | match self { |
| 202 | AnyMode::Builtin( |
| 203 | AnyMode::Custom( |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | pub fn label( |
| 208 | match self { |
| 209 | AnyMode::Builtin( |
| 210 | AnyMode::Custom( |
| 211 | if c.label.trim |
| 212 | c.name.clone() |
| 213 | } else { |
| 214 | c.label.clone() |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | pub fn symbol( |
| 221 | match self { |
| 222 | AnyMode::Builtin( |
| 223 | AnyMode::Custom( |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /// Dock color as `#rrggbb`, when the mode defines one. |
| 228 | pub fn color_hex( |
| 229 | match self { |
| 230 | AnyMode::Builtin( |
| 231 | AnyMode::Custom( |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | /// The system prompt sent to the model in this mode. |
| 236 | pub fn system_prompt( |
| 237 | match self { |
| 238 | AnyMode::Builtin( |
| 239 | AnyMode::Custom( |
| 240 | if c.prompt.trim |
| 241 | SYSTEM_PROMPT_CUSTOM_FALLBACK.to_string() |
| 242 | } else { |
| 243 | c.prompt.clone() |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | /// Tools whose execution must be approved first. Only built-in accept mode |
| 250 | /// pauses for approval; custom modes are autonomous. |
| 251 | pub fn requires_approval( |
| 252 | match self { |
| 253 | AnyMode::Builtin( |
| 254 | AnyMode::Custom( |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | /// Whether this mode allows executing the named tool. The execution-time |
| 259 | /// guard: a call that slips past tool filtering (hallucinated name, cached |
| 260 | /// call) is denied here before it can touch the filesystem or shell. |
| 261 | pub fn allows_tool( |
| 262 | match self { |
| 263 | AnyMode::Builtin( |
| 264 | AnyMode::Custom( |
| 265 | c.tools.iter() |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | /// Filter the full tool set down to what this mode allows. |
| 271 | pub fn filter_tools( |
| 272 | match self { |
| 273 | AnyMode::Builtin( |
| 274 | AnyMode::Custom( |
| 275 | if c.tools.iter |
| 276 | tools |
| 277 | } else { |
| 278 | tools |
| 279 | .into_iter() |
| 280 | .filter(|t| |
| 281 | .collect() |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | /// Sub-agent type coercion for this mode. Only built-in plan mode forces |
| 288 | /// read-only `explore` sub-agents. |
| 289 | pub fn enforce_subagent( |
| 290 | match self { |
| 291 | AnyMode::Builtin( |
| 292 | AnyMode::Custom( |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | /// Directory users drop `.json` mode files into: `~/.coding-agent/modes/`. |
| 298 | pub fn modes_dir() |
| 299 | let home = |
| 300 | PathBuf::from( |
| 301 | } |
| 302 | |
| 303 | /// Load every custom mode. Invalid files are skipped with a warning instead of |
| 304 | /// failing the whole launch, mirroring the theme loader. |
| 305 | pub fn load_custom() |
| 306 | load_from(&modes_dir()) |
| 307 | } |
| 308 | |
| 309 | fn load_from(dir: |
| 310 | let mut out |
| 311 | if !dir.is_dir |
| 312 | return Ok(out |
| 313 | } |
| 314 | let mut entries |
| 315 | .with_context(|| format! |
| 316 | .collect::<std:: |
| 317 | entries.sort_by_key(|e |
| 318 | for entry in entries |
| 319 | let path = |
| 320 | if path.extension() |
| 321 | continue; |
| 322 | } |
| 323 | let raw = |
| 324 | Ok(raw) |
| 325 | Err(e) |
| 326 | eprintln!("warning: cannot read mode {}: |
| 327 | continue; |
| 328 | } |
| 329 | }; |
| 330 | let m: CustomMode |
| 331 | Ok(m) |
| 332 | Err(e) |
| 333 | eprintln!("warning: skipping mode {}: |
| 334 | continue; |
| 335 | } |
| 336 | }; |
| 337 | if m.name.trim |
| 338 | eprintln!("warning: skipping mode {}: missing 'name' |
| 339 | continue; |
| 340 | } |
| 341 | if Mode::parse |
| 342 | eprintln!( |
| 343 | "warning: skipping mode {}: '{}' shadows a built-in mode", |
| 344 | path.display(), |
| 345 | m.name |
| 346 | ); |
| 347 | continue; |
| 348 | } |
| 349 | out.push(m |
| 350 | } |
| 351 | Ok(out) |
| 352 | } |
| 353 | |
| 354 | /// All selectable modes: the four built-ins first, then custom modes in |
| 355 | /// filename order. |
| 356 | pub fn all() |
| 357 | let mut v |
| 358 | v.extend( |
| 359 | load_custom() |
| 360 | .unwrap_or_default() |
| 361 | .into_iter() |
| 362 | .map(AnyMode::Custom |
| 363 | ); |
| 364 | v |
| 365 | } |
| 366 | |
| 367 | /// Slugs of every selectable mode (built-ins + customs), in cycle order. |
| 368 | pub fn names() |
| 369 | all().iter(). |
| 370 | } |
| 371 | |
| 372 | /// Resolve a mode by slug: built-in names (plus their aliases) first, then |
| 373 | /// custom modes (case-insensitive). |
| 374 | pub fn resolve(slug |
| 375 | resolve_from(&modes_dir(), |
| 376 | } |
| 377 | |
| 378 | /// [`resolve`] against an explicit modes directory (testable without touching |
| 379 | /// the user's `~/.coding-agent/modes`). |
| 380 | pub fn resolve_from(dir |
| 381 | let slug = |
| 382 | if let Some |
| 383 | return Some(AnyMode |
| 384 | } |
| 385 | load_from(dir) |
| 386 | .unwrap_or_default() |
| 387 | .into_iter() |
| 388 | .find(|c| |
| 389 | .map(AnyMode::Custom |
| 390 | } |
| 391 | |
| 392 | fn is_read_only(tool: |
| 393 | matches!( |
| 394 | tool, |
| 395 | "read_file" |
| 396 | | "list_files" |
| 397 | | "search" |
| 398 | | "web_search" |
| 399 | | "web_fetch" |
| 400 | | "task" |
| 401 | | "todowrite" |
| 402 | | "todoread" |
| 403 | | "get_goal" |
| 404 | | "create_goal" |
| 405 | | "update_goal" |
| 406 | ) |
| 407 | } |
| 408 | |
| 409 | fn is_read_only_subagent(subagent_type: |
| 410 | matches!(subagent_type, |
| 411 | } |
| 412 | |
| 413 | const SYSTEM_PROMPT_AUTO: &str |
| 414 | |
| 415 | You are in auto mode: you work autonomously end-to-end. Every tool is available and runs without asking. |
| 416 | |
| 417 | You have these tools: read_file, write_file, edit_file, delete_file, list_files, search, run_command, task, web_search, web_fetch, todowrite, todoread. |
| 418 | |
| 419 | Guidelines: |
| 420 | 1. Explore first. Read files and search the codebase before making changes. |
| 421 | 2. Make small, focused changes. Prefer edit_file (exact substring replacement) over rewriting whole files. |
| 422 | 3. After meaningful changes, verify with run_command (e.g. cargo check, npm test). |
| 423 | 4. Report clearly what you changed and how to run or verify it. |
| 424 | 5. Never claim something works unless you verified it. |
| 425 | 6. Tool output may be truncated. Re-read files when you need the full picture. |
| 426 | 7. If a tool returns an error, fix your arguments and try again. |
| 427 | 8. Delegate with the task tool when a task is large or exploratory: use 'explore' for read-only research, 'general' for multi-step work. The sub-agent only sees the prompt you give it — it cannot see this conversation, your todo list, or your plan. So always include your full working context in the task prompt: your current plan and todo list (if you created one, read it back with todoread), the goal, what you have already done, the exact files and line numbers involved, and what the sub-agent must deliver and how to verify it. |
| 428 | 9. Use web_search for current or external information (the current year is 2026), then web_fetch to read the pages behind promising results. |
| 429 | 10. For multi-step tasks, plan with todowrite: create a todo per step, keep exactly one in_progress at a time, mark items completed the moment they finish, and read the list back with todoread when you need to remember what is left. |
| 430 | "#; |
| 431 | |
| 432 | const SYSTEM_PROMPT_ACCEPT: &str |
| 433 | |
| 434 | You are in accept-edits mode: you read and explore freely, but every file change pauses for explicit user approval before it is applied. A write_file, edit_file or delete_file call will show the user a confirmation dialog — if they approve, it runs; if they reject, it is not applied and you must adjust your approach. |
| 435 | |
| 436 | You have these tools: read_file, write_file, edit_file, delete_file, list_files, search, run_command, task, web_search, web_fetch, todowrite, todoread. |
| 437 | |
| 438 | Guidelines: |
| 439 | 1. Explore first. Read files and search the codebase before making changes. |
| 440 | 2. Keep proposed edits small, focused and exact — prefer edit_file (exact substring replacement) over rewriting whole files so approvals are quick. |
| 441 | 3. Only call a mutating tool when you are confident about the change. If you need to iterate, make one change, wait for approval, then continue. |
| 442 | 4. If the user rejects an edit, do not retry the same edit. Adjust your plan or ask what they would prefer. |
| 443 | 5. After meaningful changes are approved, verify with run_command (e.g. cargo check, npm test). |
| 444 | 6. Never claim something works unless you verified it. |
| 445 | 7. Use web_search for current or external information (the current year is 2026), then web_fetch to read the pages behind promising results. |
| 446 | 8. Delegate with the task tool when a task is large or exploratory. The sub-agent only sees the prompt you give it — it cannot see this conversation, your todo list, or your plan. So always include your full working context in the task prompt: your current plan and todo list (if you created one, read it back with todoread), the goal, what you have already done, the exact files and line numbers involved, and what the sub-agent must deliver and how to verify it. |
| 447 | 9. For multi-step tasks, plan with todowrite: create a todo per step, keep exactly one in_progress at a time, and mark items completed the moment they finish. |
| 448 | "#; |
| 449 | |
| 450 | const SYSTEM_PROMPT_PLAN: &str |
| 451 | |
| 452 | You are in plan mode: you are read-only. You cannot modify files and you cannot run shell commands. You can read, search, browse the web, maintain a todo list (todowrite/todoread), and delegate read-only research to an 'explore' sub-agent — then you produce a plan or answer. The task tool here only offers read-only 'explore' sub-agents: whatever type you request, it is coerced to 'explore', which cannot modify files or run shell commands. Never dispatch a task that requires writing files or running commands; only delegate research, and always include your current plan, todo list, goal, and what has already been done in the sub-agent's prompt. |
| 453 | |
| 454 | Guidelines: |
| 455 | 1. Explore thoroughly first. Read the relevant files and search the codebase before proposing anything. |
| 456 | 2. Present a clear, concrete plan: the steps you would take, the files you would touch and what you would change in each, and how you would verify it. |
| 457 | 3. Do not attempt to edit, write, delete files or run commands — those tools are unavailable in this mode. |
| 458 | 4. If the request is ambiguous, ask clarifying questions before planning. |
| 459 | 5. Keep the plan focused; list risks or open questions at the end. |
| 460 | 6. When the user is ready to execute, they will switch to auto or accept-edits mode. |
| 461 | "#; |
| 462 | |
| 463 | const SYSTEM_PROMPT_MANUAL: &str |
| 464 | |
| 465 | You are in manual mode: you have no tools. You cannot read files, search the codebase, edit anything, or run commands — you can only respond with text. |
| 466 | |
| 467 | Guidelines: |
| 468 | 1. Answer questions, discuss approaches, and explain code from your knowledge. |
| 469 | 2. Do not invent file contents or claim to have read the user's code — you have no way to inspect it here. |
| 470 | 3. If answering well would require reading files, running commands, or making edits, say so and suggest the user switch mode (e.g. /mode plan for read-only research, /mode auto or /mode accept to make changes). |
| 471 | "#; |
| 472 | |
| 473 | /// Fallback prompt for a custom mode that shipped without a `prompt` field. |
| 474 | /// The tool allowlist from the mode's JSON is applied separately. |
| 475 | const SYSTEM_PROMPT_CUSTOM_FALLBACK: &str |
| 476 | |
| 477 | You are in a custom mode. Only the tools listed for this mode are available to you — anything else will fail. Work within exactly those tools, and never claim to have used a tool you were not given. |
| 478 | |
| 479 | Guidelines: |
| 480 | 1. Follow the user's instructions carefully and stay within this mode's scope. |
| 481 | 2. If you need a capability this mode does not allow, say so and suggest switching mode (e.g. /mode auto or /mode accept) rather than improvising. |
| 482 | 3. Verify work whenever the mode allows it, and never claim something works unless you verified it. |
| 483 | "#; |
| 484 | |
| 485 | #[cfg(test)] |
| 486 | mod tests { |
| 487 | use super::*; |
| 488 | use serde_json::json; |
| 489 | |
| 490 | fn defs() -> |
| 491 | vec![ |
| 492 | ToolDef { |
| 493 | name: "read_file", |
| 494 | description: "", |
| 495 | parameters: json! |
| 496 | }, |
| 497 | ToolDef { |
| 498 | name: "write_file", |
| 499 | description: "", |
| 500 | parameters: json! |
| 501 | }, |
| 502 | ToolDef { |
| 503 | name: "edit_file", |
| 504 | description: "", |
| 505 | parameters: json! |
| 506 | }, |
| 507 | ToolDef { |
| 508 | name: "delete_file", |
| 509 | description: "", |
| 510 | parameters: json! |
| 511 | }, |
| 512 | ToolDef { |
| 513 | name: "list_files", |
| 514 | description: "", |
| 515 | parameters: json! |
| 516 | }, |
| 517 | ToolDef { |
| 518 | name: "search", |
| 519 | description: "", |
| 520 | parameters: json! |
| 521 | }, |
| 522 | ToolDef { |
| 523 | name: "run_command", |
| 524 | description: "", |
| 525 | parameters: json! |
| 526 | }, |
| 527 | ToolDef { |
| 528 | name: "task", |
| 529 | description: "", |
| 530 | parameters: json! |
| 531 | }, |
| 532 | ToolDef { |
| 533 | name: "web_search", |
| 534 | description: "", |
| 535 | parameters: json! |
| 536 | }, |
| 537 | ToolDef { |
| 538 | name: "web_fetch", |
| 539 | description: "", |
| 540 | parameters: json! |
| 541 | }, |
| 542 | ToolDef { |
| 543 | name: "todowrite", |
| 544 | description: "", |
| 545 | parameters: json! |
| 546 | }, |
| 547 | ToolDef { |
| 548 | name: "todoread", |
| 549 | description: "", |
| 550 | parameters: json! |
| 551 | }, |
| 552 | ] |
| 553 | } |
| 554 | |
| 555 | fn names(tools: |
| 556 | tools.iter().map |
| 557 | } |
| 558 | |
| 559 | #[test] |
| 560 | fn parse_accepts_names_and_aliases() { |
| 561 | assert_eq!(Mode |
| 562 | assert_eq!(Mode |
| 563 | assert_eq!(Mode |
| 564 | assert_eq!(Mode |
| 565 | assert_eq!(Mode |
| 566 | assert_eq!(Mode |
| 567 | assert_eq!(Mode |
| 568 | assert_eq!(Mode |
| 569 | assert_eq!(Mode |
| 570 | } |
| 571 | |
| 572 | #[test] |
| 573 | fn auto_and_accept_keep_all_tools() { |
| 574 | assert_eq!(names(& |
| 575 | assert_eq!(names(& |
| 576 | } |
| 577 | |
| 578 | #[test] |
| 579 | fn plan_keeps_only_read_only_tools() { |
| 580 | let tools = |
| 581 | let mut got |
| 582 | got.sort_unstable(); |
| 583 | assert_eq!( |
| 584 | got, |
| 585 | vec![ |
| 586 | "list_files", |
| 587 | "read_file", |
| 588 | "search", |
| 589 | "task", |
| 590 | "todoread", |
| 591 | "todowrite", |
| 592 | "web_fetch", |
| 593 | "web_search" |
| 594 | ] |
| 595 | ); |
| 596 | } |
| 597 | |
| 598 | #[test] |
| 599 | fn manual_has_no_tools() { |
| 600 | assert!(Mode |
| 601 | } |
| 602 | |
| 603 | #[test] |
| 604 | fn accept_requires_approval_for_mutations_only() { |
| 605 | let m = |
| 606 | assert!(m.requires_approval |
| 607 | assert!(m.requires_approval |
| 608 | assert!(m.requires_approval |
| 609 | assert!(!m. |
| 610 | assert!(!m. |
| 611 | assert!(!m. |
| 612 | assert!(!Mode |
| 613 | assert!(!Mode |
| 614 | } |
| 615 | |
| 616 | #[test] |
| 617 | fn plan_blocks_mutations_at_execution() { |
| 618 | // The execution gate must agree with filter_tools: plan offers only |
| 619 | // read-only tools and denies everything else, even a hallucinated call. |
| 620 | assert!(Mode |
| 621 | assert!(Mode |
| 622 | assert!(Mode |
| 623 | assert!(Mode |
| 624 | assert!(Mode |
| 625 | assert!(Mode |
| 626 | assert!(Mode |
| 627 | assert!(Mode |
| 628 | assert!(!Mode |
| 629 | assert!(!Mode |
| 630 | assert!(!Mode |
| 631 | assert!(!Mode |
| 632 | assert!(!Mode |
| 633 | } |
| 634 | |
| 635 | #[test] |
| 636 | fn auto_accept_allow_all_manual_none() { |
| 637 | assert!(Mode |
| 638 | assert!(Mode |
| 639 | assert!(Mode |
| 640 | assert!(!Mode |
| 641 | assert!(!Mode |
| 642 | assert!(!Mode |
| 643 | } |
| 644 | |
| 645 | #[test] |
| 646 | fn plan_coerces_subagents_to_read_only() { |
| 647 | assert_eq!(Mode |
| 648 | assert_eq!(Mode |
| 649 | assert_eq!(Mode |
| 650 | assert_eq!(Mode |
| 651 | assert_eq!(Mode |
| 652 | } |
| 653 | |
| 654 | #[test] |
| 655 | fn plan_task_tool_only_offers_read_only_subagents() { |
| 656 | let task = |
| 657 | .filter_tools(crate::tools |
| 658 | .into_iter() |
| 659 | .find(|t| |
| 660 | .expect("plan mode keeps the task tool"); |
| 661 | assert!( |
| 662 | task.description.contains( |
| 663 | && task.description.contains |
| 664 | "description admits the sub-agent is read-only\n{}", |
| 665 | task.description |
| 666 | ); |
| 667 | assert!( |
| 668 | !task.description.contains |
| 669 | "no hint of writable sub-agents\n{}", |
| 670 | task.description |
| 671 | ); |
| 672 | let types = |
| 673 | .parameters |
| 674 | .get("properties") |
| 675 | .and_then(|p| |
| 676 | .and_then(|s| |
| 677 | .and_then(|e| |
| 678 | .expect("subagent_type enum"); |
| 679 | assert_eq!( |
| 680 | *types, |
| 681 | vec![serde_json:: |
| 682 | "plan mode advertises only 'explore'" |
| 683 | ); |
| 684 | } |
| 685 | |
| 686 | #[test] |
| 687 | fn auto_task_tool_keeps_general_subagent() { |
| 688 | let task = |
| 689 | .filter_tools(crate::tools |
| 690 | .into_iter() |
| 691 | .find(|t| |
| 692 | .expect("auto mode keeps the task tool"); |
| 693 | let types = |
| 694 | .parameters |
| 695 | .get("properties") |
| 696 | .and_then(|p| |
| 697 | .and_then(|s| |
| 698 | .and_then(|e| |
| 699 | .expect("subagent_type enum"); |
| 700 | assert!(types.contains |
| 701 | } |
| 702 | |
| 703 | #[test] |
| 704 | fn builtin_symbols_and_colors_are_distinct() { |
| 705 | let all: Vec |
| 706 | let uniq: std |
| 707 | assert_eq!(uniq.len |
| 708 | assert_eq!(Mode |
| 709 | assert_eq!(Mode |
| 710 | assert_eq!(Mode |
| 711 | } |
| 712 | |
| 713 | fn temp_modes_dir(name: |
| 714 | let dir = |
| 715 | std::env::temp_dir |
| 716 | let _ = |
| 717 | std::fs::create_dir_all |
| 718 | for (file, body |
| 719 | std::fs::write |
| 720 | } |
| 721 | dir |
| 722 | } |
| 723 | |
| 724 | #[test] |
| 725 | fn custom_mode_json_loads_and_resolves() { |
| 726 | let dir = |
| 727 | "load", |
| 728 | &[( |
| 729 | "review.json", |
| 730 | r##"{ |
| 731 | "name": "review", |
| 732 | "label": "code review", |
| 733 | "prompt": "Review the code.", |
| 734 | "tools": ["read_file", "list_files", "search"], |
| 735 | "color": "#81a2be", |
| 736 | "symbol": "🔍" |
| 737 | }"##, |
| 738 | )], |
| 739 | ); |
| 740 | let modes = |
| 741 | assert_eq!(modes.len |
| 742 | let m = |
| 743 | assert_eq!(m.slug |
| 744 | assert_eq!(m.label |
| 745 | assert_eq!(m.symbol |
| 746 | assert_eq!(m.color_hex |
| 747 | assert_eq!(m.system_prompt |
| 748 | let _ = |
| 749 | } |
| 750 | |
| 751 | #[test] |
| 752 | fn custom_mode_tools_are_allowlisted() { |
| 753 | let dir = |
| 754 | "tools", |
| 755 | &[( |
| 756 | "readonly.json", |
| 757 | r##"{"name": "readonly", "prompt": "x", "tools": ["read_file", "list_files"]}"##, |
| 758 | )], |
| 759 | ); |
| 760 | let modes = |
| 761 | let m = |
| 762 | assert_eq!( |
| 763 | names_of(&m.filter_tools |
| 764 | vec!["list_files", |
| 765 | ); |
| 766 | let _ = |
| 767 | } |
| 768 | |
| 769 | #[test] |
| 770 | fn star_means_every_tool_empty_means_none() { |
| 771 | let dir = |
| 772 | "star", |
| 773 | &[ |
| 774 | ( |
| 775 | "all.json", |
| 776 | r##"{"name": "all", "prompt": "x", "tools": ["*"]}"##, |
| 777 | ), |
| 778 | ( |
| 779 | "silent.json", |
| 780 | r##"{"name": "silent", "prompt": "x", "tools": []}"##, |
| 781 | ), |
| 782 | ], |
| 783 | ); |
| 784 | let modes = |
| 785 | assert_eq!(modes.len |
| 786 | for m in & |
| 787 | let any = |
| 788 | if any.slug() |
| 789 | assert_eq!(any. |
| 790 | } else { |
| 791 | assert!(any. |
| 792 | } |
| 793 | } |
| 794 | let _ = |
| 795 | } |
| 796 | |
| 797 | #[test] |
| 798 | fn custom_allows_tool_follows_the_allowlist() { |
| 799 | let dir = |
| 800 | "allows", |
| 801 | &[ |
| 802 | ( |
| 803 | "readonly.json", |
| 804 | r##"{"name": "readonly", "prompt": "x", "tools": ["read_file", "list_files"]}"##, |
| 805 | ), |
| 806 | ( |
| 807 | "all.json", |
| 808 | r##"{"name": "all", "prompt": "x", "tools": ["*"]}"##, |
| 809 | ), |
| 810 | ( |
| 811 | "silent.json", |
| 812 | r##"{"name": "silent", "prompt": "x", "tools": []}"##, |
| 813 | ), |
| 814 | ], |
| 815 | ); |
| 816 | let modes = |
| 817 | assert_eq!(modes.len |
| 818 | for m in & |
| 819 | let any = |
| 820 | match any.slug() |
| 821 | "readonly" => { |
| 822 | assert!(any. |
| 823 | assert!(any. |
| 824 | assert!(!any |
| 825 | assert!(!any |
| 826 | } |
| 827 | "all" => { |
| 828 | assert!(any. |
| 829 | assert!(any. |
| 830 | assert!(any. |
| 831 | } |
| 832 | "silent" => { |
| 833 | assert!(!any |
| 834 | assert!(!any |
| 835 | } |
| 836 | _ => panic! |
| 837 | } |
| 838 | } |
| 839 | let _ = |
| 840 | } |
| 841 | |
| 842 | #[test] |
| 843 | fn invalid_and_shadowing_modes_are_skipped() { |
| 844 | let dir = |
| 845 | "skip", |
| 846 | &[ |
| 847 | ("bad.json", r##"{ not json |
| 848 | ("shadow.json", r##"{"name": "plan", "prompt": "x"} |
| 849 | ( |
| 850 | "ok.json", |
| 851 | r##"{"name": "custom", "prompt": "x", "tools": ["*"]}"##, |
| 852 | ), |
| 853 | ], |
| 854 | ); |
| 855 | let modes = |
| 856 | let names: Vec |
| 857 | assert_eq!(names, |
| 858 | let _ = |
| 859 | } |
| 860 | |
| 861 | #[test] |
| 862 | fn resolve_falls_back_to_custom_modes() { |
| 863 | let dir = |
| 864 | "resolve", |
| 865 | &[("ninja.json", r##" |
| 866 | ); |
| 867 | assert_eq!( |
| 868 | resolve_from(&dir, |
| 869 | Some(AnyMode |
| 870 | ); |
| 871 | assert_eq!( |
| 872 | resolve_from(&dir, |
| 873 | Some(AnyMode |
| 874 | name: "ninja". |
| 875 | prompt: "x". |
| 876 | ..CustomMode::default() |
| 877 | })) |
| 878 | ); |
| 879 | assert_eq!(resolve_from( |
| 880 | let _ = |
| 881 | } |
| 882 | |
| 883 | fn names_of(tools: |
| 884 | let mut v |
| 885 | v.sort_unstable(); |
| 886 | v |
| 887 | } |
| 888 | } |