Skip to main content

winx_code_agent/utils/
mode_prompts.rs

1//! Behavioral prompts injected per operating mode, ported from wcgw's `modes.py`.
2//!
3//! The mode (wcgw / architect / `code_writer`) already enforces permissions
4//! technically (allowed commands, write globs, `bash -r`). But enforcement alone
5//! makes the model *discover* the rules by hitting errors. These prompts tell the
6//! model up front how to behave in the current mode — the "system-prompt policy"
7//! layer that research shows is as important as the tool descriptions themselves.
8//! Returned by `Initialize` so the agent reads them before its first action.
9
10use std::fmt::Write as _;
11
12use crate::types::{AllowedCommands, AllowedGlobs, CodeWriterConfig, Modes};
13
14/// Default (full-access) behavior guidance.
15const WCGW_PROMPT: &str = "# Operating mode: wcgw (full access)
16
17- Use the provided shell, file-read and file-write tools to accomplish the objective.
18- Do not provide code snippets unless asked — edit the code directly with the winx tools.
19- Do not install new tools/packages before checking that the tool (or an alternative) doesn't already exist.
20- Do not use echo/cat to write files; always use FileWriteOrEdit to create/update files.
21- Do not send Ctrl-c / interrupts without asking — programs often keep running with no visible output.
22- Provide as many file paths as you need to ReadFiles in a single call.
23- Run `pwd` if you hit a file/dir not found error, to make sure you're not lost.";
24
25/// Read-only architect behavior guidance.
26const ARCHITECT_PROMPT: &str = "# Operating mode: architect (read-only)
27
28- You may NOT edit, create, or overwrite any file. FileWriteOrEdit is disabled in this mode.
29- You may NOT run commands that change disk, packages, system config or environment — read-only commands only.
30- The shell runs with `-r` (restricted): you cannot change directory.
31- Only run commands that help you explore the repo, understand the system, or read what's relevant.
32- Do not send Ctrl-c / interrupts without asking the user.
33- When an implementation is requested, share minimal snippets (use `...` for elided lines), not whole files.
34
35# How to respond
36- Read as many relevant files as possible first; be comprehensive.
37- Start from the folder structure (ignore .git, node_modules, target, .venv, etc.).
38- Provide as many file paths as you need to ReadFiles in a single call.";
39
40/// Common command-discipline lines shared by `code_writer` variants.
41const RUN_COMMAND_COMMON: &str =
42    "- Do not send Ctrl-c / interrupts without asking — programs often keep running silently.
43- Do not use echo/cat to write files; always use FileWriteOrEdit.
44- Do not provide code snippets unless asked — edit the code directly with the winx tools.";
45
46/// Build the behavioral prompt for the active mode. `config` is only consulted
47/// for `code_writer`, where the allowed globs/commands shape the wording.
48pub fn mode_prompt(mode: Modes, config: Option<&CodeWriterConfig>) -> String {
49    match mode {
50        Modes::Wcgw => WCGW_PROMPT.to_string(),
51        Modes::Architect => ARCHITECT_PROMPT.to_string(),
52        Modes::CodeWriter => code_writer_prompt(config),
53    }
54}
55
56fn code_writer_prompt(config: Option<&CodeWriterConfig>) -> String {
57    let mut out = String::from("# Operating mode: code_writer\n\n");
58
59    // winx uses the same glob set for both edit and write-if-empty.
60    match config.map(|c| &c.allowed_globs) {
61        Some(AllowedGlobs::List(globs)) if globs.is_empty() => {
62            out.push_str("- You are NOT allowed to edit or create any file.\n");
63        }
64        Some(AllowedGlobs::List(globs)) => {
65            let _ = writeln!(
66                out,
67                "- You may edit/create files matching ONLY these globs: {}",
68                globs.join(", ")
69            );
70        }
71        _ => out.push_str("- You may edit/create files within the provided repository only.\n"),
72    }
73
74    match config.map(|c| &c.allowed_commands) {
75        Some(AllowedCommands::List(cmds)) if cmds.is_empty() => {
76            out.push_str("- You are NOT allowed to run any commands.\n");
77        }
78        Some(AllowedCommands::List(cmds)) => {
79            let _ = writeln!(out, "- You may run ONLY the following commands: {}", cmds.join(", "));
80            out.push_str(RUN_COMMAND_COMMON);
81        }
82        _ => {
83            out.push_str(
84                "- You may run commands for project setup, code writing, testing, running and debugging only.\n\
85                 - Do not add/remove packages or change system configuration or environment.\n",
86            );
87            out.push_str(RUN_COMMAND_COMMON);
88        }
89    }
90
91    out
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn architect_is_read_only_and_names_disabled_tool() {
100        let p = mode_prompt(Modes::Architect, None);
101        assert!(p.contains("read-only"));
102        assert!(p.contains("FileWriteOrEdit is disabled"));
103    }
104
105    #[test]
106    fn code_writer_lists_allowed_globs_and_commands() {
107        let config = CodeWriterConfig {
108            allowed_globs: AllowedGlobs::List(vec!["src/**/*.rs".to_string()]),
109            allowed_commands: AllowedCommands::List(vec!["cargo test".to_string()]),
110        };
111        let p = mode_prompt(Modes::CodeWriter, Some(&config));
112        assert!(p.contains("src/**/*.rs"));
113        assert!(p.contains("cargo test"));
114        assert!(p.contains("ONLY"));
115    }
116
117    #[test]
118    fn code_writer_empty_lists_forbid() {
119        let config = CodeWriterConfig {
120            allowed_globs: AllowedGlobs::List(vec![]),
121            allowed_commands: AllowedCommands::List(vec![]),
122        };
123        let p = mode_prompt(Modes::CodeWriter, Some(&config));
124        assert!(p.contains("NOT allowed to edit"));
125        assert!(p.contains("NOT allowed to run any commands"));
126    }
127
128    #[test]
129    fn wcgw_is_full_access() {
130        assert!(mode_prompt(Modes::Wcgw, None).contains("full access"));
131    }
132}