const INTENT_PHRASES: &[&str] = &[
"now let me ",
"now update ",
"now fix ",
"now add ",
"now bump ",
"now run ",
"now check ",
"now read ",
"now commit",
"now amend",
"now updating",
"now fixing",
"now committing",
"now amending",
"now pushing",
"now cherry-picking",
"now merging",
"now rebasing",
"now deploying",
"now building",
"now testing",
"now checking",
"now applying",
"now restarting",
"now creating",
"now writing",
"now editing",
"now adding",
"now removing",
"now deleting",
"now reading",
"now running",
"now starting",
"now finishing",
"now finalizing",
"now installing",
"now configuring",
"now wiring",
"now setting up",
"i'll update",
"i'll fix",
"i'll modify",
"i'll create",
"i'll write",
"i'll edit",
"i'll add",
"i'll change",
"i'll replace",
"i'll commit",
"i'll amend",
"i'll proceed",
"i'll start",
"i'll finish",
"i'll run",
"i'll check",
"i'll see",
"i'll look",
"i'll prepare",
"i'll take a look",
"i will proceed",
"let me update",
"let me fix",
"let me modify",
"let me create",
"let me write",
"let me edit",
"let me add",
"let me change",
"let me commit",
"let me amend",
"let me see",
"let me check",
"let me look",
"let me read",
"let me examine",
"let me verify",
"let me inspect",
"let me review",
"let me take", "let me actually", "let me prepare",
"let me proceed",
"let me start",
"let me first", "let me finish",
"let me finalize",
"let me run",
"let's update",
"let's fix",
"let's modify",
"let's create",
"let's write",
"let's edit",
"let's add",
"let's change",
"let's replace",
"let's commit",
"let's amend",
"let's see",
"let's check",
"let's look",
"let's read",
"let's examine",
"let's verify",
"let's inspect",
"let's review",
"let's take a look",
"let's prepare",
"let's proceed",
"let's start",
"let's first",
"let's finish",
"let's finalize",
"let's run",
"let's dig",
"let's investigate",
"let's explore",
"let's search",
"let's find",
"let's gather",
"let's pull",
"let's grab",
"let's get",
"let's fetch",
"let's query",
"let's scan",
"let's hunt",
"let's trace",
"let's track",
"let's look into",
"let's check into",
"let's find out",
"let's dig into",
"let me dig",
"let me investigate",
"let me explore",
"let me search",
"let me find",
"let me gather",
"let me pull",
"let me grab",
"let me get",
"let me fetch",
"let me query",
"let me scan",
"let me hunt",
"let me trace",
"let me track",
"let me look into",
"let me check into",
"let me find out",
"let me dig into",
"i'll dig",
"i'll investigate",
"i'll explore",
"i'll search",
"i'll find",
"i'll gather",
"i'll pull",
"i'll grab",
"i'll get",
"i'll fetch",
"i'll query",
"i'll scan",
"i'll hunt",
"i'll trace",
"i'll track",
"i'll look into",
"i'll check into",
"i'll find out",
"i'll dig into",
"let me build",
"let me push",
"let me deploy",
"let me sync",
"let me migrate",
"let me apply",
"let me install",
"let me configure",
"let me set up",
"let me wire",
"let's build",
"let's push",
"let's deploy",
"let's sync",
"let's migrate",
"let's apply",
"let's install",
"let's configure",
"let's set up",
"let's wire",
"i'll build",
"i'll push",
"i'll deploy",
"i'll sync",
"i'll migrate",
"i'll apply",
"i'll install",
"i'll configure",
"i'll set up",
"i'll wire",
"now build",
"now push",
"now deploy",
"now sync",
"now migrate",
"now apply",
];
pub fn has_phantom_tool_intent_no_tools(text: &str) -> bool {
let trimmed = text.trim();
if trimmed.len() < 20 {
return false;
}
let lead = prose_lead_in(trimmed);
if lead.is_empty() {
return false;
}
let lower = lead.to_lowercase();
if INTENT_PHRASES.iter().any(|p| lower.contains(p)) {
return true;
}
has_past_tense_action_claim(&lower)
}
fn has_past_tense_action_claim(lower: &str) -> bool {
const ACTION_VERBS: &[&str] = &[
"pushed",
"deployed",
"merged",
"migrated",
"committed",
"rebased",
"tagged",
"released",
"published",
"synced",
"rolled back",
"rolled out",
];
for raw_sentence in lower.split(['.', '\n', '!']) {
let s = raw_sentence.trim();
if s.is_empty() || s.len() > 80 {
continue;
}
for verb in ACTION_VERBS {
if s.split_whitespace().take(4).any(|w| {
let w = w.trim_matches(|c: char| !c.is_alphanumeric());
w == *verb
}) {
return true;
}
}
}
false
}
pub fn has_investigative_intent(text: &str) -> bool {
let lower = text.to_lowercase();
INTENT_PHRASES.iter().any(|p| lower.contains(p))
}
fn prose_lead_in(text: &str) -> &str {
let mut byte_offset: usize = 0;
for (idx, line) in text.lines().enumerate() {
let trimmed_line = line.trim_start();
let is_structural = trimmed_line.starts_with("```")
|| (trimmed_line.starts_with('|') && trimmed_line.contains('|'))
|| trimmed_line.starts_with("- ")
|| trimmed_line.starts_with("* ")
|| trimmed_line.starts_with("• ")
|| (trimmed_line
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
&& trimmed_line.contains(". "));
if is_structural {
return text[..byte_offset].trim_end();
}
if idx >= 6 {
break;
}
byte_offset += line.len() + 1; }
text
}
pub fn looks_truncated_mid_sentence(text: &str) -> bool {
let trimmed = text.trim_end();
if trimmed.chars().count() < 40 {
return false;
}
if trimmed.ends_with("```") {
return false;
}
if trimmed.ends_with('|') {
return false;
}
if ends_with_url(trimmed) {
return false;
}
let last = match trimmed.chars().next_back() {
Some(c) => c,
None => return false,
};
if last.is_alphanumeric() {
return true;
}
matches!(
last,
',' | ';' | ':' | '-' | '(' | '[' | '{' | '<' | '/' | '\\' | '&' | '@' | '#'
)
}
fn ends_with_url(text: &str) -> bool {
let trimmed = text.trim_end();
let boundary = trimmed
.rfind(|c: char| c.is_whitespace() || matches!(c, '(' | '[' | '{' | '<' | '"' | '\''))
.map(|i| i + 1)
.unwrap_or(0);
let tail = &trimmed[boundary..];
tail.contains("://")
}
pub fn has_phantom_tool_intent(text: &str) -> bool {
let trimmed = text.trim();
if trimmed.len() < 40 {
return false;
}
let lower = trimmed.to_lowercase();
use regex::Regex;
let now_imperative =
Regex::new(r"(?m)^[\s\-*]*(?:now\s+(?:let\s+me\s+)?|let\s+me\s+)\w").unwrap();
if now_imperative.find_iter(&lower).count() >= 2 {
return true;
}
let numbered_steps =
Regex::new(r"(?m)^\s*\d+\.\s+(?:update|fix|modify|create|write|edit|add|change|remove|delete|check|read|run|bump|amend|verify|test|deploy|install)")
.unwrap();
if numbered_steps.find_iter(&lower).count() >= 2 {
return true;
}
let past_tense_standalone = Regex::new(
r"(?m)^[\s\-*]*(?:amended|updated|fixed|modified|created|written|saved|deleted|removed|replaced|bumped|deployed|committed)[.!]"
).unwrap();
if past_tense_standalone.find_iter(&lower).count() >= 2 {
return true;
}
const COMPLETION_CLAIMS: &[&str] = &[
"here's what changed",
"here's what's changed",
"here are the changes",
"here's what i did",
"here is what i did",
"changes applied",
"updated the file",
"updated the code",
"updated src/",
"modified the file",
"modified src/",
"fixed the file",
"fixed the bug",
"fixed the issue",
"fixed src/",
"created the file",
"wrote the file",
"everything is updated",
"i've made the changes",
"i've completed",
"i've finished",
"i've updated",
"i've written",
"i've created",
"i've saved",
"i've modified",
"i've fixed",
"i've replaced",
"i've amended",
"i've committed",
"i've bumped",
"i've made all",
"all changes have been",
"all files have been",
"the changes have been applied",
"changes are now in place",
"the file now contains",
"the file has been",
"file updated",
"file created",
"file saved",
"changes saved",
"amended.",
"committed.",
"amended the commit",
"bumped the version",
"version bumped",
];
if COMPLETION_CLAIMS.iter().any(|c| lower.contains(c)) {
return true;
}
let now_gerund_re = Regex::new(
r"(?im)(?:^|[.!?]\s+)\s*now\s+(?:updating|fixing|committing|amending|pushing|cherry-picking|merging|rebasing|deploying|building|testing|checking|applying|restarting|creating|writing|editing|adding|removing|deleting|reading|running|starting|finishing|finalizing|installing|configuring|wiring)\b"
).unwrap();
if now_gerund_re.is_match(trimmed) {
return true;
}
let has_intent = INTENT_PHRASES.iter().any(|v| lower.contains(v));
let trailing_colon_intent = Regex::new(
r"(?im)(?:^|\n)\s*(?:let\s+me|i'll|i\s+will|now\s+let\s+me|now\s+i'll)\s+\w[^:\n]{0,80}:\s*$",
)
.unwrap();
if trailing_colon_intent.is_match(trimmed) {
return true;
}
if has_intent {
let path_re =
Regex::new(r"(?:^|[\s`(])(?:\./)?[a-zA-Z_][\w\-]*/[\w\-/]*\.\w{1,6}(?:[\s`),:;]|$)")
.unwrap();
let ext_re = Regex::new(
r"(?:^|[\s`(])[\w\-]+\.(?:rs|py|ts|tsx|js|jsx|go|sh|toml|yaml|yml|json|md)(?:[\s`),:;]|$)",
)
.unwrap();
let backtick_code_re = Regex::new(r"`[a-zA-Z_]\w+`").unwrap();
if path_re.is_match(trimmed)
|| ext_re.is_match(trimmed)
|| backtick_code_re.is_match(trimmed)
{
return true;
}
}
false
}