diff --git a/crates/ironclaw_engine/orchestrator/default.py b/crates/ironclaw_engine/orchestrator/default.py index 6cae692fbaa..dad91ac755e 100644 --- a/crates/ironclaw_engine/orchestrator/default.py +++ b/crates/ironclaw_engine/orchestrator/default.py @@ -126,6 +126,7 @@ def signals_tool_intent(text): "read the", "write the", "create", "run the", "execute", "query", "retrieve", "add it", "add the", "add this", "add that", "update the", "delete", "remove the", "look into", + "stop", "pause", "cancel", "halt", "disable", ] for prefix in PREFIXES: @@ -161,10 +162,33 @@ def signals_execution_intent(text): "ship it", "deploy it", "deploy that", "deploy this", "deploy the ", "send it", "send that", "send the ", "fetch it", "fetch that", "fetch the ", + "stop it", "stop that", "stop this", "stop the ", + "pause it", "pause that", "pause this", "pause the ", + "cancel it", "cancel that", "cancel this", "cancel the ", + "halt it", "halt that", "halt this", "halt the ", + "disable it", "disable that", "disable this", "disable the ", "please run ", "please execute ", "please fetch ", "please send ", "please deploy ", + "please stop ", "please pause ", "please cancel ", + "please halt ", "please disable ", ] - return any(phrase in lower for phrase in EXEC_PHRASES) + if any(phrase in lower for phrase in EXEC_PHRASES): + return True + + # Bare imperative commands at the start of the message. + # "stop pinging", "stop", "pause", "cancel" are unambiguous commands + # that don't match the "verb + pronoun/article" pattern above. + # Checking startswith avoids false positives like "I can't stop". + # Strip trailing punctuation so "Stop." and "cancel!" still match. + trimmed = lower.strip().rstrip(".,!?;:") + IMPERATIVE_STARTS = ["stop ", "pause ", "cancel ", "halt ", "disable "] + BARE_COMMANDS = ["stop", "pause", "cancel", "halt", "disable"] + if trimmed in BARE_COMMANDS: + return True + if any(trimmed.startswith(s) for s in IMPERATIVE_STARTS): + return True + + return False def format_output(result, max_chars=8000): diff --git a/crates/ironclaw_engine/src/executor/orchestrator.rs b/crates/ironclaw_engine/src/executor/orchestrator.rs index 3c173634491..a447f35d3c1 100644 --- a/crates/ironclaw_engine/src/executor/orchestrator.rs +++ b/crates/ironclaw_engine/src/executor/orchestrator.rs @@ -3158,6 +3158,114 @@ mod tests { )); } + // ── Stop / pause / cancel intent (mission lifecycle) ───────── + + #[test] + fn signals_tool_intent_stop_pause_cancel() { + assert!(eval_python_bool( + r#"signals_tool_intent("I'll stop the mission now.")"# + )); + assert!(eval_python_bool( + r#"signals_tool_intent("Let me pause the ticker.")"# + )); + assert!(eval_python_bool( + r#"signals_tool_intent("I'll cancel the monitoring.")"# + )); + assert!(eval_python_bool( + r#"signals_tool_intent("I'm going to halt the recurring task.")"# + )); + assert!(eval_python_bool( + r#"signals_tool_intent("I will disable the mission.")"# + )); + } + + #[test] + fn signals_tool_intent_no_false_positive_stop_discussion() { + // "let me explain" is in EXCLUSIONS — blocks the entire text + assert!(!eval_python_bool( + r#"signals_tool_intent("Let me explain how to stop the mission.")"# + )); + // Past tense should not trigger + assert!(!eval_python_bool( + r#"signals_tool_intent("I already stopped the mission.")"# + )); + } + + // ── Execution intent: stop / pause / cancel ──────────────── + + #[test] + fn signals_execution_intent_stop_pause_cancel() { + assert!(eval_python_bool(r#"signals_execution_intent("stop it")"#)); + assert!(eval_python_bool( + r#"signals_execution_intent("pause the mission")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("cancel that")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("please stop the ticker")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("please pause everything")"# + )); + } + + #[test] + fn signals_execution_intent_bare_stop() { + // Bare imperative commands — the exact user messages from #2808 + assert!(eval_python_bool(r#"signals_execution_intent("stop")"#)); + assert!(eval_python_bool( + r#"signals_execution_intent("stop pinging")"# + )); + assert!(eval_python_bool(r#"signals_execution_intent("pause")"#)); + assert!(eval_python_bool(r#"signals_execution_intent("cancel")"#)); + assert!(eval_python_bool(r#"signals_execution_intent("halt")"#)); + } + + #[test] + fn signals_execution_intent_no_false_positive_stop_in_sentence() { + // "stop" mid-sentence should NOT trigger — only at the start + assert!(!eval_python_bool( + r#"signals_execution_intent("I can't stop thinking about it")"# + )); + assert!(!eval_python_bool( + r#"signals_execution_intent("how do I stop a mission?")"# + )); + } + + #[test] + fn signals_execution_intent_halt_disable_phrases() { + // "halt/disable" pronoun+article phrases and "please halt/disable" + assert!(eval_python_bool(r#"signals_execution_intent("halt that")"#)); + assert!(eval_python_bool( + r#"signals_execution_intent("halt the mission")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("disable it")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("disable the ticker")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("please halt the mission")"# + )); + assert!(eval_python_bool( + r#"signals_execution_intent("please disable the routine")"# + )); + // Bare "disable" command + assert!(eval_python_bool(r#"signals_execution_intent("disable")"#)); + } + + #[test] + fn signals_execution_intent_bare_stop_with_punctuation() { + // Bare commands with trailing punctuation must still match + assert!(eval_python_bool(r#"signals_execution_intent("stop.")"#)); + assert!(eval_python_bool(r#"signals_execution_intent("cancel!")"#)); + assert!(eval_python_bool( + r#"signals_execution_intent("stop pinging.")"# + )); + } + // ── Skill activation: smart-quote / autocorrect resilience ─── // // Regression for the ceo-setup non-activation report. iOS / macOS / most