Skip to main content

vtcode_core/planning/
mod.rs

1//! Canonical plan-mode enter/exit phrase sets and matchers.
2//!
3//! Plan-mode intent detection lives in two consumers — the runloop
4//! (`detect_planning_intent`) and the Codex app-server bridge
5//! (`normalize_planning_input`). Both must agree on which phrases enter,
6//! exit, or stay in plan mode, so the phrase literals live here as the single
7//! source of truth. Each consumer keeps its own matching strategy (the runloop
8//! matches natural-language substrings; the Codex bridge matches bare tokens
9//! exactly) but draws the literals from this module so they cannot drift apart.
10
11/// Short imperative commands that exit plan mode and start implementing.
12/// Matched exactly (whole-trimmed) by the runloop, and recognized by the Codex
13/// bridge as implementation aliases.
14pub const EXIT_DIRECT_COMMANDS: &[&str] = &[
15    "implement",
16    "yes",
17    "go",
18    "start",
19    "approve",
20    "approved",
21    "implement now",
22    "start implementing",
23    "start implementation",
24    "execute plan",
25    "execute the plan",
26    "execute this plan",
27    "switch to agent mode",
28    "exit planning workflow",
29    "exit planning workflow and implement",
30];
31
32/// Whole-word approval tokens (so "disapprove" does not match "approve").
33pub const APPROVAL_WORDS: &[&str] = &["approve", "approved", "lgtm", "accepted", "accept"];
34
35/// Multi-word approval phrases.
36pub const APPROVAL_PHRASES: &[&str] = &[
37    "approve the plan",
38    "approve this plan",
39    "approve the proposed plan",
40    "looks good",
41    "ship it",
42    "accept the plan",
43    "accept this plan",
44];
45
46/// Longer exit trigger phrases (matched as substrings).
47pub const EXIT_TRIGGER_PHRASES: &[&str] = &[
48    "start implement",
49    "start implementation",
50    "start implementing",
51    "implement now",
52    "implement the plan",
53    "implement this plan",
54    "begin implement",
55    "begin implementation",
56    "begin coding",
57    "proceed to implement",
58    "proceed with implementation",
59    "proceed to coding",
60    "proceed with coding",
61    "execute the plan",
62    "execute this plan",
63    "let s implement",
64    "lets implement",
65    "go ahead and implement",
66    "go ahead and code",
67    "ready to implement",
68    "start coding",
69    "start building",
70    "switch to agent mode",
71    "exit planning workflow",
72    "exit planning workflow and implement",
73];
74
75/// Phrases that explicitly keep the user in plan mode (highest priority).
76/// `edit` and `revise` are the short-form replies to the yes/no/edit HITL
77/// prompt surfaced when `request_user_input` is unavailable — they route the
78/// user back into planning revision without requiring the longer "keep
79/// planning" phrase (checkpoint turn_725).
80pub const STAY_PHRASES: &[&str] = &[
81    "stay in planning workflow",
82    "keep in planning workflow",
83    "continue planning",
84    "keep planning",
85    "do not implement",
86    "don t implement",
87    "not ready to implement",
88    "don t exit planning workflow",
89    "do not exit planning workflow",
90    "edit the plan",
91    "edit plan",
92    "revise the plan",
93    "revise plan",
94];
95
96/// Phrases that enter plan mode (does not include the `/plan` slash command,
97/// which callers check separately).
98pub const ENTER_PHRASES: &[&str] = &[
99    "make a plan",
100    "create a plan",
101    "write a plan",
102    "come up with a plan",
103    "plan this",
104    "stay in planning workflow",
105    "keep planning",
106    "continue planning",
107    "before you implement make a plan",
108    "before implementing make a plan",
109    "outline the implementation plan",
110];
111
112/// Assistant cues that indicate a recent implementation prompt.
113pub const IMPLEMENTATION_CUES: &[&str] = &[
114    "implement this plan",
115    "implement the plan",
116    "ready to implement",
117    "exit planning workflow",
118    "execute the plan",
119    "switch out of planning workflow",
120    "start implementation",
121    "start implementing",
122    "start coding",
123];
124
125/// Aliases that clear the planning-active flag AND switch to execution mode.
126/// Used by the Codex bridge's bare-token exact matching. Includes the
127/// implementation intents that the runloop recognizes so the two paths cannot
128/// drift (previously the bridge silently missed `approve`/`lgtm`/`ship it`).
129pub const EXECUTION_MODE_ALIASES: &[&str] = &[
130    "implement",
131    "continue",
132    "go",
133    "start",
134    "yes",
135    "approve",
136    "approved",
137    "lgtm",
138    "accepted",
139    "accept",
140    "ship it",
141    "implement now",
142    "start implementing",
143    "start implementation",
144    "execute plan",
145    "execute the plan",
146    "execute this plan",
147    "switch to agent mode",
148    "exit planning workflow and implement",
149];
150
151/// Aliases that clear the planning-active flag but are NOT implementation
152/// intents — they pass through verbatim rather than being rewritten to the
153/// execution prompt.
154pub const FLAG_CLEARING_ONLY_ALIASES: &[&str] = &["exit planning workflow"];
155
156/// Normalize user text for plan-mode intent matching: lowercase and replace
157/// non-alphanumeric characters with spaces for flexible substring matching.
158pub fn normalize_plan_intent(text: &str) -> String {
159    text.chars()
160        .map(|c| {
161            if c.is_alphanumeric() {
162                c.to_ascii_lowercase()
163            } else {
164                ' '
165            }
166        })
167        .collect()
168}
169
170/// Whether the normalized text expresses an exit-and-implement intent.
171///
172/// Mirrors the runloop's detection priority: direct commands (exact), approval
173/// words (whole-word), approval phrases (substring), then trigger phrases
174/// (substring). The caller must check [`matches_stay_intent`] first so that
175/// stay phrases win.
176pub fn matches_exit_intent(normalized: &str) -> bool {
177    let trimmed = normalized.trim();
178    EXIT_DIRECT_COMMANDS.contains(&trimmed)
179        || APPROVAL_WORDS
180            .iter()
181            .any(|word| normalized.split_whitespace().any(|tok| tok == *word))
182        || APPROVAL_PHRASES.iter().any(|phrase| normalized.contains(phrase))
183        || EXIT_TRIGGER_PHRASES.iter().any(|phrase| normalized.contains(phrase))
184}
185
186/// Whether the normalized text keeps the user in plan mode (highest priority).
187pub fn matches_stay_intent(normalized: &str) -> bool {
188    STAY_PHRASES.iter().any(|phrase| normalized.contains(phrase))
189}
190
191/// Whether the normalized text is an explicit request to enter plan mode
192/// (excluding the `/plan` slash command, which callers check separately).
193pub fn matches_enter_intent(normalized: &str) -> bool {
194    ENTER_PHRASES.iter().any(|phrase| normalized.contains(phrase))
195}
196
197/// Whether the normalized assistant text contains an implementation prompt cue.
198pub fn contains_implementation_cue(normalized: &str) -> bool {
199    IMPLEMENTATION_CUES.iter().any(|cue| normalized.contains(cue))
200}
201
202/// Codex-bridge matcher: bare-token exact match against the execution/flag
203/// alias sets (preserves the bridge's verbatim-input contract).
204pub fn is_execution_mode_alias(input: &str) -> bool {
205    let normalized = input.trim().to_ascii_lowercase();
206    EXECUTION_MODE_ALIASES
207        .iter()
208        .chain(FLAG_CLEARING_ONLY_ALIASES.iter())
209        .any(|alias| *alias == normalized)
210}
211
212/// Whether a Codex-bridge alias is an implementation intent (rewrite to the
213/// execution prompt) vs. a flag-clearing-only alias (pass through verbatim).
214pub fn is_implementation_alias(input: &str) -> bool {
215    let normalized = input.trim().to_ascii_lowercase();
216    EXECUTION_MODE_ALIASES.iter().any(|alias| *alias == normalized)
217}