diff --git a/Cargo.lock b/Cargo.lock index 58077bef8..afb136b90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5355,6 +5355,7 @@ dependencies = [ "jcode-core", "jcode-experiment-flags", "jcode-gateway-types", + "jcode-keywords", "jcode-logging", "jcode-memory-types", "jcode-mempalace-adapter", @@ -5675,6 +5676,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "jcode-keywords" +version = "0.1.0" +dependencies = [ + "chrono", + "dirs 6.0.0", + "serde", + "strum 0.26.3", + "tempfile", + "tokio", + "toml", +] + [[package]] name = "jcode-logging" version = "0.1.0" @@ -6029,6 +6043,7 @@ dependencies = [ "jcode-build-meta", "jcode-core", "jcode-experiment-flags", + "jcode-keywords", "jcode-logging", "jcode-message-types", "jcode-plugin-runtime", diff --git a/Cargo.toml b/Cargo.toml index 9f02526dd..9548b0870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ members = [ "crates/jcode-plugin-runtime", "crates/jcode-mempalace-adapter", "crates/jcode-render-core", + "crates/jcode-keywords", "evals/jbench", ] diff --git a/crates/jcode-app-core/Cargo.toml b/crates/jcode-app-core/Cargo.toml index 990457ac5..23a11778e 100644 --- a/crates/jcode-app-core/Cargo.toml +++ b/crates/jcode-app-core/Cargo.toml @@ -120,6 +120,7 @@ jcode-build-meta = { path = "../jcode-build-meta" } # Re-exported via `pub use jcode_base::*` in lib.rs. default-features=false so # this crate controls jcode-base's optional features (see [features] below). jcode-base = { path = "../jcode-base", default-features = false } +jcode-keywords = { path = "../jcode-keywords" } jcode-experiment-flags = { path = "../jcode-experiment-flags" } jcode-compaction-core = { path = "../jcode-compaction-core" } jcode-config-types = { path = "../jcode-config-types" } diff --git a/crates/jcode-app-core/src/agent/prompting.rs b/crates/jcode-app-core/src/agent/prompting.rs index c0c64b5f6..d3470f841 100644 --- a/crates/jcode-app-core/src/agent/prompting.rs +++ b/crates/jcode-app-core/src/agent/prompting.rs @@ -113,12 +113,105 @@ impl Agent { .as_ref() .map(std::path::PathBuf::from); + // Detect keywords, update mode state, execute workflows, build prompt. + // We skip the whole pipeline on empty input so an empty/system-only turn + // does not burn a turn of any active mode's budget. + let keyword_prompt = { + let latest_input = self + .session + .messages + .iter() + .rev() + .find(|m| matches!(m.role, crate::message::Role::User)) + .and_then(|m| { + m.content.iter().find_map(|b| match b { + crate::message::ContentBlock::Text { text, .. } => Some(text.as_str()), + _ => None, + }) + }) + .unwrap_or(""); + if latest_input.is_empty() { + None + } else { + let detections = jcode_keywords::detect_keywords(latest_input); + let mut mode_state = + jcode_keywords::state::update_modes(&detections, working_dir.as_deref()); + + // Surface any mode conflicts (TDD + ultrawork, etc.) to logs. + let active_kinds: Vec = + mode_state.active_modes.iter().map(|m| m.workflow).collect(); + for conflict in jcode_keywords::conflict::check_conflicts(&active_kinds) { + crate::logging::warn(&jcode_keywords::conflict::format_warning(&conflict)); + } + + // Process PREVIOUS turn's LLM response (phase transitions, completion) + // This runs at the START of the current turn, using last turn's response + if let Some(last_assistant) = self + .session + .messages + .iter() + .rev() + .find(|m| matches!(m.role, crate::message::Role::Assistant)) + .and_then(|m| { + m.content.iter().find_map(|b| match b { + crate::message::ContentBlock::Text { text, .. } => Some(text.as_str()), + _ => None, + }) + }) + { + let response_actions = + jcode_keywords::process_turn_response(&mode_state, last_assistant); + if !response_actions.is_empty() { + let _ = jcode_keywords::apply_actions(&mut mode_state, &response_actions); + } + } + + // Classify task size so heavy workflows can suppress themselves for + // trivial requests (e.g. a one-line "$ultrawork fix typo"). + let task_size = jcode_keywords::task_size::classify(latest_input); + + // Execute active workflows for THIS turn + let actions = jcode_keywords::execute_active_workflows( + &mode_state, + latest_input, + working_dir.as_deref(), + &self.session.id, + task_size, + ); + if !actions.is_empty() { + let (summaries, deferred) = + jcode_keywords::apply_actions(&mut mode_state, &actions); + for s in &summaries { + crate::logging::info(&format!("Keyword workflow: {}", s)); + } + if !deferred.is_empty() { + crate::logging::warn(&format!( + "Keyword workflow: {} spawn action(s) deferred — they will not run until SubagentTool is wired from the agent runtime. (See issue #391 follow-up.)", + deferred.len() + )); + } + } + + // Persist metadata to disk + jcode_keywords::state::save_state(&mode_state, working_dir.as_deref()); + + // Build workflow prompt + let prompt = jcode_keywords::build_workflow_prompt(&mode_state); + if prompt.is_empty() { + None + } else { + Some(prompt) + } + } + }; + let (mut split, _context_info) = crate::prompt::build_system_prompt_split( skill_prompt.as_deref(), &available_skills, self.session.is_canary, memory_prompt, working_dir.as_deref(), + keyword_prompt, ); self.append_current_turn_system_reminder(&mut split); diff --git a/crates/jcode-base/src/prompt.rs b/crates/jcode-base/src/prompt.rs index 234da3d78..5dbe8c827 100644 --- a/crates/jcode-base/src/prompt.rs +++ b/crates/jcode-base/src/prompt.rs @@ -209,6 +209,7 @@ pub fn build_system_prompt_with_context_and_memory( is_selfdev, memory_prompt, None, + None, ) } @@ -219,6 +220,7 @@ pub fn build_system_prompt_full( is_selfdev: bool, memory_prompt: Option<&str>, working_dir: Option<&Path>, + keyword_prompt: Option, ) -> (String, ContextInfo) { // Resolve the effective system-prompt root: CLI/env > .jcode/SYSTEM.md // (closest to cwd, walking up to ~/.jcode/agent) > config > built-in default. @@ -281,6 +283,13 @@ pub fn build_system_prompt_full( parts.push(memory.to_string()); } + // Keyword mode prompt (changes per turn based on detected keywords) + if let Some(kw) = keyword_prompt { + if !kw.is_empty() { + parts.push(kw); + } + } + // Add available skills list if !available_skills.is_empty() { let mut skills_section = "# Available Skills\n\nYou have access to the following skills that the user can invoke with `/skillname`:\n".to_string(); @@ -313,6 +322,7 @@ pub fn build_system_prompt_split( is_selfdev: bool, memory_prompt: Option<&str>, working_dir: Option<&Path>, + keyword_prompt: Option, ) -> (SplitSystemPrompt, ContextInfo) { // Resolve effective system-prompt root (issue #22). let system_root = resolve_system_prompt_override(working_dir) @@ -390,6 +400,13 @@ pub fn build_system_prompt_split( dynamic_parts.push(memory.to_string()); } + // Keyword mode prompt (changes per turn based on detected keywords) + if let Some(kw) = keyword_prompt { + if !kw.is_empty() { + dynamic_parts.push(kw); + } + } + // Active skill prompt (changes per skill invocation) if let Some(skill) = skill_prompt { dynamic_parts.push(format!("# Active Skill\n\n{}", skill)); diff --git a/crates/jcode-base/src/prompt_tests.rs b/crates/jcode-base/src/prompt_tests.rs index 262c8c26e..2bb7f0c04 100644 --- a/crates/jcode-base/src/prompt_tests.rs +++ b/crates/jcode-base/src/prompt_tests.rs @@ -82,7 +82,7 @@ fn test_session_context_includes_time_timezone_and_system_info() { #[test] fn test_split_prompt_does_not_inject_session_context_per_turn() { - let (split, _info) = build_system_prompt_split(None, &[], false, None, None); + let (split, _info) = build_system_prompt_split(None, &[], false, None, None, None); assert!(!split.dynamic_part.contains("# Session Context")); assert!(!split.dynamic_part.contains("Time: ")); assert!(!split.dynamic_part.contains("Timezone: UTC")); @@ -122,7 +122,7 @@ fn test_prompt_overlay_files_are_loaded_from_project_and_global_jcode_dirs() { "expected global prompt overlay content" ); - let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(project_dir.path())); + let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(project_dir.path()), None); assert!(prompt.contains("project prompt overlay instructions")); assert!(prompt.contains("global prompt overlay instructions")); assert!(info.prompt_overlay_chars > 0); @@ -176,13 +176,13 @@ fn test_preferred_tools_files_are_loaded_from_project_and_global_jcode_dirs() { "expected global preferred tools content" ); - let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(project_dir.path())); + let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(project_dir.path()), None); assert!(prompt.contains("project preferred tools instructions")); assert!(prompt.contains("global preferred tools instructions")); assert!(info.preferred_tools_chars > 0); let (split, split_info) = - build_system_prompt_split(None, &[], false, None, Some(project_dir.path())); + build_system_prompt_split(None, &[], false, None, Some(project_dir.path()), None); assert!( split .static_part @@ -223,7 +223,7 @@ fn test_selfdev_prompt_uses_full_selfdev_instructions() { #[test] fn test_selfdev_prompt_uses_desktop_focus_for_desktop_working_dir() { let desktop_dir = std::path::Path::new("/tmp/jcode/crates/jcode-desktop/src"); - let (prompt, _info) = build_system_prompt_full(None, &[], true, None, Some(desktop_dir)); + let (prompt, _info) = build_system_prompt_full(None, &[], true, None, Some(desktop_dir), None); assert!(prompt.contains("launched from the desktop app context")); assert!(prompt.contains("selfdev build target=desktop")); assert!(!prompt.contains("launched from the TUI/root jcode context")); @@ -232,7 +232,7 @@ fn test_selfdev_prompt_uses_desktop_focus_for_desktop_working_dir() { #[test] fn test_split_selfdev_prompt_defaults_to_tui_focus_for_repo_root() { let repo_dir = std::path::Path::new("/tmp/jcode"); - let (split, _info) = build_system_prompt_split(None, &[], true, None, Some(repo_dir)); + let (split, _info) = build_system_prompt_split(None, &[], true, None, Some(repo_dir), None); assert!( split .static_part @@ -266,7 +266,7 @@ fn test_selfdev_prompt_template_placeholders_are_resolved() { #[test] fn split_prompt_estimated_tokens_is_positive_when_populated() { - let (split, _info) = build_system_prompt_split(None, &[], false, None, None); + let (split, _info) = build_system_prompt_split(None, &[], false, None, None, None); assert!(split.chars() > 0); assert!(split.estimated_tokens() > 0); } @@ -370,7 +370,7 @@ fn build_system_prompt_full_uses_jcode_system_md_root() { std::fs::create_dir_all(&dot).unwrap(); std::fs::write(dot.join("SYSTEM.md"), "MY_OVERRIDDEN_ROOT").unwrap(); - let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(temp.path())); + let (prompt, info) = build_system_prompt_full(None, &[], false, None, Some(temp.path()), None); if let Some(prev) = prev_env { crate::env::set_var("JCODE_SYSTEM_PROMPT", prev); diff --git a/crates/jcode-keywords/Cargo.toml b/crates/jcode-keywords/Cargo.toml new file mode 100644 index 000000000..33656ce85 --- /dev/null +++ b/crates/jcode-keywords/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "jcode-keywords" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false +description = "Magic keyword system — NL trigger detection, mode state, prompt injection, workflow dispatch" + +[dependencies] +serde = { version = "1", features = ["derive"] } +toml = { version = "0.8" } +strum = { workspace = true, features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } +dirs = "6" +tokio = { version = "1", features = ["rt", "time"] } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/jcode-keywords/src/conflict.rs b/crates/jcode-keywords/src/conflict.rs new file mode 100644 index 000000000..a93f94802 --- /dev/null +++ b/crates/jcode-keywords/src/conflict.rs @@ -0,0 +1,102 @@ +//! Conflict detection — warn about incompatible mode combinations. + +use crate::registry::WorkflowKind; + +/// A conflict between two workflow kinds. +#[derive(Debug, Clone)] +pub struct Conflict { + pub a: WorkflowKind, + pub b: WorkflowKind, + pub reason: &'static str, +} + +/// Check for conflicts among a set of active workflow kinds. +/// +/// Returns a list of conflicts found. Empty list means no conflicts. +pub fn check_conflicts(active: &[WorkflowKind]) -> Vec { + let mut conflicts = Vec::new(); + + for (i, &a) in active.iter().enumerate() { + for &b in &active[i + 1..] { + if let Some(conflict) = pair_conflict(a, b) { + conflicts.push(conflict); + } + } + } + + conflicts +} + +/// Check if two specific workflows conflict. +fn pair_conflict(a: WorkflowKind, b: WorkflowKind) -> Option { + use WorkflowKind::*; + + match (a, b) { + // TDD + ultrawork: TDD is sequential, ultrawork is parallel + (Tdd, Ultrawork) | (Ultrawork, Tdd) => Some(Conflict { + a, + b, + reason: "TDD is sequential (red-green-refactor) while ultrawork spawns parallel agents", + }), + // Cancel conflicts with everything except itself + (Cancel, other) | (other, Cancel) if other != Cancel => Some(Conflict { + a: Cancel, + b: other, + reason: "canceljcode will deactivate all other modes", + }), + // deep-interview + ultrawork: interview needs user interaction, ultrawork is autonomous + (DeepInterview, Ultrawork) | (Ultrawork, DeepInterview) => Some(Conflict { + a, + b, + reason: "deep-interview requires user interaction while ultrawork runs autonomously", + }), + _ => None, + } +} + +/// Format a conflict as a human-readable warning string. +pub fn format_warning(conflict: &Conflict) -> String { + format!( + "⚠ Conflict: {} + {} — {}", + conflict.a, conflict.b, conflict.reason + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_conflicts_empty() { + assert!(check_conflicts(&[]).is_empty()); + } + + #[test] + fn no_conflicts_compatible() { + assert!(check_conflicts(&[WorkflowKind::Ultrathink, WorkflowKind::Wiki]).is_empty()); + } + + #[test] + fn tdd_ultrawork_conflict() { + let conflicts = check_conflicts(&[WorkflowKind::Tdd, WorkflowKind::Ultrawork]); + assert_eq!(conflicts.len(), 1); + } + + #[test] + fn cancel_conflicts_with_all() { + let conflicts = check_conflicts(&[WorkflowKind::Cancel, WorkflowKind::Tdd]); + assert_eq!(conflicts.len(), 1); + } + + #[test] + fn format_warning_works() { + let conflict = Conflict { + a: WorkflowKind::Tdd, + b: WorkflowKind::Ultrawork, + reason: "test reason", + }; + let msg = format_warning(&conflict); + assert!(msg.contains("tdd")); + assert!(msg.contains("ultrawork")); + } +} diff --git a/crates/jcode-keywords/src/detector.rs b/crates/jcode-keywords/src/detector.rs new file mode 100644 index 000000000..1e62000d1 --- /dev/null +++ b/crates/jcode-keywords/src/detector.rs @@ -0,0 +1,260 @@ +//! Keyword detection — scan sanitized input for keyword triggers. + +use crate::registry::KeywordEntry; +use crate::sanitizer; + +/// A keyword detected in user input. +#[derive(Debug, Clone)] +pub struct DetectedKeyword { + /// The matched keyword entry from the registry. + pub entry: &'static KeywordEntry, + /// The actual text that triggered the match. + pub matched_text: String, + /// Byte offset range (start, end) in the sanitized input. + pub position: (usize, usize), + /// Confidence score: 1.0 for exact keyword, 0.8-0.9 for alias match. + pub confidence: f32, +} + +/// Detect keywords in user input. +/// +/// Returns all detected keywords, sorted by priority (highest first), +/// then by position (earliest first). +pub fn detect_keywords(input: &str) -> Vec { + let sanitized = sanitizer::sanitize(input); + if sanitized.is_empty() { + return Vec::new(); + } + let lower = sanitizer::to_lower(&sanitized); + let registry = crate::registry::build_registry(); + let mut results = Vec::new(); + + for entry in registry.iter() { + // Check canonical keyword (case-insensitive) + if let Some(pos) = lower.find(&entry.keyword.to_lowercase()) { + results.push(DetectedKeyword { + entry: *entry, + matched_text: sanitized[pos..pos + entry.keyword.len()].to_string(), + position: (pos, pos + entry.keyword.len()), + confidence: 1.0, + }); + continue; + } + + // Check aliases (case-insensitive, fuzzy with Levenshtein ≤ 2, min 5 chars) + for alias in entry.aliases { + let alias_lower = alias.to_lowercase(); + if alias_lower.len() < 5 { + // Short aliases: exact match only + if let Some(pos) = lower.find(&alias_lower) { + let end = (pos + alias.len()).min(sanitized.len()); + // Guard against non-char-boundary slicing + let end = sanitized + .char_indices() + .map(|(i, _)| i) + .take_while(|&i| i <= end) + .last() + .unwrap_or(pos); + results.push(DetectedKeyword { + entry: *entry, + matched_text: sanitized[pos..end].to_string(), + position: (pos, end), + confidence: 0.9, + }); + break; + } + continue; + } + if let Some(pos) = find_fuzzy(&lower, &alias_lower, 2) { + // Take the byte length of the actually-matched window, not the + // alias itself, so a multi-byte alias cannot cause a panic on + // a non-char-boundary slice. + let match_len = lower[pos..] + .char_indices() + .nth(alias.chars().count()) + .map(|(i, _)| i) + .unwrap_or(alias.len()); + let end = (pos + match_len).min(sanitized.len()); + results.push(DetectedKeyword { + entry: *entry, + matched_text: sanitized[pos..end].to_string(), + position: (pos, end), + confidence: 0.85, + }); + break; // Only one alias match per entry + } + } + } + + // Filter out fuzzy matches that overlap with exact matches + let exact_ranges: Vec<(usize, usize)> = results + .iter() + .filter(|r| r.confidence >= 1.0) + .map(|r| r.position) + .collect(); + results.retain(|r| { + if r.confidence >= 1.0 { + return true; + } + // Fuzzy match must not overlap any exact match + !exact_ranges + .iter() + .any(|&(es, ee)| r.position.0 < ee && r.position.1 > es) + }); + + // Sort by priority (highest first), then by position (earliest first) + results.sort_by(|a, b| { + b.entry + .priority + .cmp(&a.entry.priority) + .then(a.position.0.cmp(&b.position.0)) + }); + + // Deduplicate: keep highest-priority match per workflow kind + deduplicate_by_workflow(results) +} + +/// Find a substring with fuzzy matching (Levenshtein distance ≤ max_dist). +/// Returns the byte offset of the best match, or None. +fn find_fuzzy(haystack: &str, needle: &str, max_dist: usize) -> Option { + if needle.is_empty() { + return Some(0); + } + + // First try exact substring match (fast path) + if let Some(pos) = haystack.find(needle) { + return Some(pos); + } + + // Fuzzy match: slide a window of needle length ± max_dist + let needle_len = needle.chars().count(); + let min_len = needle_len.saturating_sub(max_dist); + let max_len = needle_len + max_dist; + + let haystack_chars: Vec = haystack.chars().collect(); + let _needle_chars: Vec = needle.chars().collect(); + + for window_len in min_len..=max_len { + for i in 0..haystack_chars.len().saturating_sub(window_len - 1) { + let window: String = haystack_chars[i..i + window_len].iter().collect(); + let dist = levenshtein_distance(&window, needle); + if dist <= max_dist { + // Convert char index back to byte offset + let byte_offset: usize = haystack_chars[..i].iter().map(|c| c.len_utf8()).sum(); + return Some(byte_offset); + } + } + } + + None +} + +/// Compute Levenshtein distance between two strings. +fn levenshtein_distance(a: &str, b: &str) -> usize { + let a_chars: Vec = a.chars().collect(); + let b_chars: Vec = b.chars().collect(); + let n = a_chars.len(); + let m = b_chars.len(); + + if n == 0 { + return m; + } + if m == 0 { + return n; + } + + let mut prev = (0..=m).collect::>(); + let mut curr = vec![0usize; m + 1]; + + for i in 1..=n { + curr[0] = i; + for j in 1..=m { + let cost = if a_chars[i - 1] == b_chars[j - 1] { + 0 + } else { + 1 + }; + curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost); + } + std::mem::swap(&mut prev, &mut curr); + } + + prev[m] +} + +/// Deduplicate detected keywords by workflow kind, keeping the highest-priority match. +fn deduplicate_by_workflow(mut results: Vec) -> Vec { + let mut seen = std::collections::HashSet::new(); + results.retain(|kw| seen.insert(kw.entry.workflow)); + results +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::registry::WorkflowKind; + + #[test] + fn detect_exact_keyword() { + let results = detect_keywords("$ultrawork fix the bug"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].entry.keyword, "$ultrawork"); + assert_eq!(results[0].confidence, 1.0); + } + + #[test] + fn detect_alias() { + let results = detect_keywords("please run ulw on this"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].entry.workflow, WorkflowKind::Ultrawork); + } + + #[test] + fn detect_cancel() { + let results = detect_keywords("canceljcode"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].entry.workflow, WorkflowKind::Cancel); + } + + #[test] + fn detect_natural_language() { + let results = detect_keywords("think deeply about this problem"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].entry.workflow, WorkflowKind::Ultrathink); + } + + #[test] + fn no_detection_on_plain_text() { + let results = detect_keywords("hello world"); + assert!(results.is_empty()); + } + + #[test] + fn detect_multiple_keywords_by_priority() { + let results = detect_keywords("$ultrawork $tdd fix this"); + assert!(!results.is_empty()); + // ultrawork (priority 10) should come before tdd (priority 7) + assert_eq!(results[0].entry.workflow, WorkflowKind::Ultrawork); + } + + #[test] + fn levenshtein_basic() { + assert_eq!(levenshtein_distance("kitten", "sitting"), 3); + assert_eq!(levenshtein_distance("hello", "hello"), 0); + assert_eq!(levenshtein_distance("", "abc"), 3); + } + + #[test] + fn detector_handles_multibyte_input_safely() { + // Mixed CJK + ASCII should never panic, even if the alias slice + // logic would have hit a non-char-boundary in the old impl. + let results = detect_keywords("please 分析 this 代码 for me"); + // No alias is multi-byte in the current registry, so this is a no-op + // detection but the call must not panic. + for r in &results { + // Each match's position must lie on char boundaries + assert!(r.position.0 <= r.position.1); + assert!(r.position.1 <= "please 分析 this 代码 for me".len()); + } + } +} diff --git a/crates/jcode-keywords/src/intent.rs b/crates/jcode-keywords/src/intent.rs new file mode 100644 index 000000000..05f393841 --- /dev/null +++ b/crates/jcode-keywords/src/intent.rs @@ -0,0 +1,95 @@ +//! Intent disambiguation — resolve overlapping keyword matches. + +use crate::detector::DetectedKeyword; +use crate::registry::WorkflowKind; + +/// Disambiguate overlapping keyword detections. +/// +/// Rules: +/// 1. Higher priority wins +/// 2. Equal priority → longer match wins (more specific) +/// 3. Equal priority + equal length → earlier position wins +/// 4. Cancel always wins over everything +pub fn disambiguate(detections: Vec) -> Vec { + if detections.len() <= 1 { + return detections; + } + + let mut result = Vec::new(); + let mut used_ranges: Vec<(usize, usize)> = Vec::new(); + + for detection in detections { + // Cancel always passes through + if detection.entry.workflow == WorkflowKind::Cancel { + result.push(detection); + continue; + } + + // Check if this overlaps with an already-accepted detection + let overlaps = used_ranges + .iter() + .any(|&(start, end)| detection.position.0 < end && detection.position.1 > start); + + if !overlaps { + used_ranges.push(detection.position); + result.push(detection); + } + } + + result +} + +/// Check if two detections conflict (same position range). +pub fn are_conflicting(a: &DetectedKeyword, b: &DetectedKeyword) -> bool { + a.position.0 < b.position.1 && b.position.0 < a.position.1 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::registry::{KeywordEntry, WorkflowKind}; + + fn make_detection( + keyword: &'static str, + workflow: WorkflowKind, + priority: u8, + pos: (usize, usize), + ) -> DetectedKeyword { + DetectedKeyword { + entry: Box::leak(Box::new(KeywordEntry { + keyword, + aliases: &[], + priority, + workflow, + description: "", + })), + matched_text: keyword.to_string(), + position: pos, + confidence: 1.0, + } + } + + #[test] + fn cancel_always_wins() { + let detections = vec![ + make_detection("$ultrawork", WorkflowKind::Ultrawork, 10, (0, 10)), + make_detection("canceljcode", WorkflowKind::Cancel, 9, (11, 22)), + ]; + let result = disambiguate(detections); + assert!( + result + .iter() + .any(|d| d.entry.workflow == WorkflowKind::Cancel) + ); + } + + #[test] + fn non_overlapping_both_kept() { + let detections = vec![ + make_detection("$tdd", WorkflowKind::Tdd, 7, (0, 4)), + make_detection("$wiki", WorkflowKind::Wiki, 5, (10, 15)), + ]; + let result = disambiguate(detections); + assert_eq!(result.len(), 2); + } +} diff --git a/crates/jcode-keywords/src/lib.rs b/crates/jcode-keywords/src/lib.rs new file mode 100644 index 000000000..7f6bdd30b --- /dev/null +++ b/crates/jcode-keywords/src/lib.rs @@ -0,0 +1,43 @@ +//! Magic keyword system for jcode. +//! +//! Detects natural-language keyword triggers in user input, manages persistent +//! mode state, builds prompt injections for the system prompt, and dispatches +//! to 14 workflow handlers. +//! +//! # Architecture +//! +//! ```text +//! User types "$ultrawork fix the bug" +//! ↓ +//! detector::detect_keywords() → DetectedKeyword +//! ↓ +//! state::update_modes() → ModeState (persisted to .jcode/state/modes.toml) +//! ↓ +//! workflow::executor::execute_active_workflows() → Vec<(idx, kind, WorkflowAction)> +//! ↓ +//! workflow::executor::apply_actions() → updates ModeState metadata, removes completed +//! ↓ +//! workflow::executor::build_workflow_prompt() → String (injected into system prompt) +//! ``` + +pub mod conflict; +pub mod detector; +pub mod intent; +pub mod registry; +pub mod sanitizer; +pub mod state; +pub mod task_size; +pub mod visual; +pub mod workflow; + +// Re-exports for convenience +pub use detector::{DetectedKeyword, detect_keywords}; +pub use registry::{KeywordEntry, WorkflowKind}; +pub use state::ModeState; +pub use visual::KeywordHighlight; +pub use workflow::executor::DeferredSpawn; +pub use workflow::executor::{ + apply_actions, build_workflow_prompt, execute_active_workflows, process_turn, + process_turn_response, +}; +pub use workflow::{WorkflowAction, WorkflowContext, WorkflowHandler}; diff --git a/crates/jcode-keywords/src/registry.rs b/crates/jcode-keywords/src/registry.rs new file mode 100644 index 000000000..dc6fafa3e --- /dev/null +++ b/crates/jcode-keywords/src/registry.rs @@ -0,0 +1,210 @@ +//! Keyword registry — all supported keywords, aliases, priorities, and workflow mappings. + +use serde::{Deserialize, Serialize}; +use std::sync::OnceLock; +use strum::{Display, EnumIter, EnumString}; + +/// Workflow kinds that can be triggered by keywords. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumIter, EnumString, Serialize, Deserialize, +)] +#[strum(serialize_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] +pub enum WorkflowKind { + /// ParallelExecution — spawn sub-agents, coordinate, aggregate + Ultrawork, + /// GoalTracking — durable goal + token budget across turns + Ultragoal, + /// QACycling — implement → test → fix → repeat + Ultraqa, + /// ConsensusPlanning — plan → adversarial review → revise → approve + Ralplan, + /// RequirementsGathering — ask questions → score ambiguity → threshold + DeepInterview, + /// TestDrivenDev — write test → fail → implement → pass + Tdd, + /// CodeReview — spawn reviewer → analyze → report + CodeReview, + /// SecurityReview — OWASP scan → secrets → report + SecurityReview, + /// ExtendedThinking — deep reasoning, single-turn + Ultrathink, + /// CodebaseSearch — multi-strategy search → context map + Deepsearch, + /// DeepAnalysis — structured analysis → report + Analyze, + /// DocLookup — local + web docs → summary + Wiki, + /// SlopCleanup — detect + fix AI low-quality code + AiSlopCleaner, + /// CancelAll — stop all modes + cancel tasks + Cancel, +} + +/// A single keyword entry in the registry. +#[derive(Debug, Clone)] +pub struct KeywordEntry { + /// The canonical keyword trigger (e.g. "$ultrawork") + pub keyword: &'static str, + /// Alternative triggers (natural language aliases) + pub aliases: &'static [&'static str], + /// Priority: 11 (highest) .. 5 (lowest) + pub priority: u8, + /// The workflow this keyword activates + pub workflow: WorkflowKind, + /// Human-readable description + pub description: &'static str, +} + +/// Build the full keyword registry, sorted by priority (highest first). +/// +/// Returns a `&'static` slice backed by a lazily-initialised static, so callers +/// (e.g. `detector`) can hold onto `&'static KeywordEntry` references without +/// leaking memory on every detection. +pub fn build_registry() -> &'static [&'static KeywordEntry] { + static REGISTRY: OnceLock<&'static [&'static KeywordEntry]> = OnceLock::new(); + REGISTRY.get_or_init(|| { + let mut entries: Vec = vec![ + // Priority 11 — highest + KeywordEntry { + keyword: "$ralplan", + aliases: &["ralplan", "consensus plan"], + priority: 11, + workflow: WorkflowKind::Ralplan, + description: "Consensus planning — plan → adversarial review → revise → approve", + }, + // Priority 10 + KeywordEntry { + keyword: "$ultrawork", + aliases: &["ulw", "uw", "parallel", "dont stop", "must complete"], + priority: 10, + workflow: WorkflowKind::Ultrawork, + description: "Parallel execution — spawn sub-agents, coordinate, aggregate", + }, + KeywordEntry { + keyword: "$ultragoal", + aliases: &["ultragoal"], + priority: 10, + workflow: WorkflowKind::Ultragoal, + description: "Goal tracking — durable goal + token budget across turns", + }, + // Priority 9 + KeywordEntry { + keyword: "canceljcode", + aliases: &["stopjcode"], + priority: 9, + workflow: WorkflowKind::Cancel, + description: "Cancel all active modes and stop running tasks", + }, + // Priority 8 + KeywordEntry { + keyword: "$ultraqa", + aliases: &["ultraqa", "qa cycle"], + priority: 8, + workflow: WorkflowKind::Ultraqa, + description: "QA cycling — implement → test → fix → repeat", + }, + KeywordEntry { + keyword: "$deep-interview", + aliases: &["ouroboros", "interview me", "gather requirements"], + priority: 8, + workflow: WorkflowKind::DeepInterview, + description: "Requirements gathering — ask questions → score ambiguity → threshold", + }, + // Priority 7 + KeywordEntry { + keyword: "$ultrathink", + aliases: &["think hard", "think deeply"], + priority: 7, + workflow: WorkflowKind::Ultrathink, + description: "Extended thinking — deep reasoning, single-turn", + }, + KeywordEntry { + keyword: "$deepsearch", + aliases: &["search the codebase", "find in codebase"], + priority: 7, + workflow: WorkflowKind::Deepsearch, + description: "Codebase search — multi-strategy search → context map", + }, + KeywordEntry { + keyword: "$tdd", + aliases: &["test first", "red green"], + priority: 7, + workflow: WorkflowKind::Tdd, + description: "Test-driven development — write test → fail → implement → pass", + }, + // Priority 6 + KeywordEntry { + keyword: "$code-review", + aliases: &["code review", "review code"], + priority: 6, + workflow: WorkflowKind::CodeReview, + description: "Code review — spawn reviewer → analyze → report", + }, + KeywordEntry { + keyword: "$security-review", + aliases: &["security review", "audit security"], + priority: 6, + workflow: WorkflowKind::SecurityReview, + description: "Security review — OWASP scan → secrets → report", + }, + KeywordEntry { + keyword: "$analyze", + aliases: &["deep-analyze", "deep analysis"], + priority: 6, + workflow: WorkflowKind::Analyze, + description: "Deep analysis — structured analysis → report", + }, + // Priority 5 + KeywordEntry { + keyword: "$wiki", + aliases: &["wiki this", "look up docs"], + priority: 5, + workflow: WorkflowKind::Wiki, + description: "Doc lookup — local + web docs → summary", + }, + KeywordEntry { + keyword: "ai-slop-cleaner", + aliases: &["clean ai slop", "fix ai code"], + priority: 5, + workflow: WorkflowKind::AiSlopCleaner, + description: "AI slop cleanup — detect + fix AI low-quality code", + }, + ]; + + // Sort by priority (highest first) + entries.sort_by(|a, b| b.priority.cmp(&a.priority)); + let leaked: &'static [KeywordEntry] = Box::leak(entries.into_boxed_slice()); + let refs: &'static [&'static KeywordEntry] = + Box::leak(leaked.iter().collect::>().into_boxed_slice()); + refs + }) +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registry_sorted_by_priority() { + let registry = build_registry(); + for window in registry.windows(2) { + assert!( + window[0].priority >= window[1].priority, + "Registry not sorted: {} ({}) > {} ({})", + window[0].keyword, + window[0].priority, + window[1].keyword, + window[1].priority, + ); + } + } + + #[test] + fn registry_has_all_workflows() { + let registry = build_registry(); + let kinds: std::collections::HashSet = + registry.iter().map(|e| e.workflow).collect(); + // All 14 workflows should be represented + assert_eq!(kinds.len(), 14); + } +} diff --git a/crates/jcode-keywords/src/sanitizer.rs b/crates/jcode-keywords/src/sanitizer.rs new file mode 100644 index 000000000..154d37a82 --- /dev/null +++ b/crates/jcode-keywords/src/sanitizer.rs @@ -0,0 +1,83 @@ +//! Input sanitization — normalize whitespace, strip ANSI, lowercase for matching. + +/// Sanitize user input for keyword detection. +/// +/// - Strips ANSI escape sequences +/// - Normalizes whitespace (collapse runs, trim) +/// - Preserves original positions for highlight mapping +pub fn sanitize(input: &str) -> String { + let stripped = strip_ansi(input); + normalize_whitespace(&stripped) +} + +/// Strip ANSI escape sequences from text. +fn strip_ansi(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut in_escape = false; + for ch in input.chars() { + if ch == '\x1b' { + in_escape = true; + continue; + } + if in_escape { + if ch.is_alphabetic() { + in_escape = false; + } + continue; + } + out.push(ch); + } + out +} + +/// Normalize whitespace: collapse runs of whitespace into single spaces, trim. +fn normalize_whitespace(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut prev_was_space = false; + for ch in input.chars() { + if ch.is_whitespace() { + if !prev_was_space { + out.push(' '); + } + prev_was_space = true; + } else { + out.push(ch); + prev_was_space = false; + } + } + out.trim().to_string() +} + +/// Lowercase a string for case-insensitive matching. +pub fn to_lower(input: &str) -> String { + input.to_lowercase() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strip_ansi_removes_escape_sequences() { + assert_eq!(strip_ansi("\x1b[31mhello\x1b[0m"), "hello"); + assert_eq!(strip_ansi("no escapes"), "no escapes"); + assert_eq!(strip_ansi(""), ""); + } + + #[test] + fn normalize_whitespace_collapses_runs() { + assert_eq!(normalize_whitespace(" hello world "), "hello world"); + assert_eq!(normalize_whitespace("single"), "single"); + assert_eq!(normalize_whitespace(""), ""); + } + + #[test] + fn sanitize_full_pipeline() { + assert_eq!(sanitize("\x1b[1m $ultrawork \x1b[0m"), "$ultrawork"); + } + + #[test] + fn to_lower_converts() { + assert_eq!(to_lower("Hello WORLD"), "hello world"); + } +} diff --git a/crates/jcode-keywords/src/state.rs b/crates/jcode-keywords/src/state.rs new file mode 100644 index 000000000..b0efd9f22 --- /dev/null +++ b/crates/jcode-keywords/src/state.rs @@ -0,0 +1,215 @@ +//! Mode state — persistent activation state for keyword-triggered workflows. +//! +//! State is persisted to `.jcode/state/modes.toml` (project-local) or +//! `~/.jcode/state/modes.toml` (global fallback). + +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use crate::detector::DetectedKeyword; +use crate::registry::WorkflowKind; + +/// Default number of turns before a mode auto-deactivates. +const DEFAULT_TURN_LIMIT: u32 = 10; + +/// Persistent mode state. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ModeState { + /// Currently active modes. + pub active_modes: Vec, + /// ISO 8601 timestamp of last update. + pub updated_at: Option, +} + +/// A single active mode. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActiveMode { + /// The workflow kind. + pub workflow: WorkflowKind, + /// ISO 8601 timestamp when activated. + pub activated_at: String, + /// Number of turns since activation. Auto-deactivates at turn limit. + pub turn_count: u32, + /// Turn limit before auto-deactivation. + pub turn_limit: u32, + /// Workflow-specific metadata (iteration counts, scores, goals, etc.). + #[serde(default)] + pub metadata: HashMap, +} + +impl ActiveMode { + /// Check if this mode has expired. + pub fn is_expired(&self) -> bool { + self.turn_count >= self.turn_limit + } +} + +/// Update mode state based on detected keywords. +/// +/// - Activates new modes from detections +/// - Increments turn count for existing modes +/// - Deactivates expired modes +/// - Cancel clears everything +/// - Persists state to disk +pub fn update_modes(detections: &[DetectedKeyword], working_dir: Option<&Path>) -> ModeState { + let mut state = load_state(working_dir); + + // Cancel clears everything + if detections + .iter() + .any(|d| d.entry.workflow == WorkflowKind::Cancel) + { + state.active_modes.clear(); + state.updated_at = Some(Utc::now().to_rfc3339()); + return state; + } + + // Increment turn counts for existing modes + for mode in &mut state.active_modes { + mode.turn_count += 1; + } + + // Remove expired modes + state.active_modes.retain(|m| !m.is_expired()); + + // Activate new modes from detections + for detection in detections { + let workflow = detection.entry.workflow; + + // Skip if already active + if state.active_modes.iter().any(|m| m.workflow == workflow) { + continue; + } + + state.active_modes.push(ActiveMode { + workflow, + activated_at: Utc::now().to_rfc3339(), + turn_count: 0, + turn_limit: DEFAULT_TURN_LIMIT, + metadata: HashMap::new(), + }); + } + + state.updated_at = Some(Utc::now().to_rfc3339()); + state +} + +/// Load mode state from disk. +pub fn load_state(working_dir: Option<&Path>) -> ModeState { + let path = state_path(working_dir); + if !path.exists() { + return ModeState::default(); + } + match std::fs::read_to_string(&path) { + Ok(content) => match toml::from_str(&content) { + Ok(state) => state, + Err(e) => { + eprintln!( + "[jcode-keywords] failed to parse mode state at {}: {} — using default", + path.display(), + e, + ); + ModeState::default() + } + }, + Err(e) => { + eprintln!( + "[jcode-keywords] failed to read mode state at {}: {} — using default", + path.display(), + e, + ); + ModeState::default() + } + } +} + +/// Save mode state to disk. +pub fn save_state(state: &ModeState, working_dir: Option<&Path>) { + let path = state_path(working_dir); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Ok(content) = toml::to_string_pretty(state) { + let _ = std::fs::write(&path, content); + } +} + +/// Resolve the state file path. +fn state_path(working_dir: Option<&Path>) -> PathBuf { + // Project-local takes priority + if let Some(dir) = working_dir { + return dir.join(".jcode").join("state").join("modes.toml"); + } + + // Global fallback + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".jcode") + .join("state") + .join("modes.toml") +} + +/// Clear all active modes (used by cancel). +pub fn clear_modes(working_dir: Option<&Path>) { + let state = ModeState::default(); + save_state(&state, working_dir); +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn load_state_missing_file_returns_default() { + let tmp = TempDir::new().unwrap(); + // Use a subdir that definitely doesn't have .jcode/state/modes.toml + let state = load_state(Some(tmp.path())); + assert!(state.active_modes.is_empty()); + } + + #[test] + fn save_and_load_roundtrip() { + let tmp = TempDir::new().unwrap(); + let state = ModeState { + active_modes: vec![ActiveMode { + workflow: WorkflowKind::Ultrawork, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 3, + turn_limit: 10, + metadata: HashMap::new(), + }], + updated_at: Some("2026-01-01T00:00:00Z".to_string()), + }; + save_state(&state, Some(tmp.path())); + let loaded = load_state(Some(tmp.path())); + assert_eq!(loaded.active_modes.len(), 1); + assert_eq!(loaded.active_modes[0].workflow, WorkflowKind::Ultrawork); + } + + #[test] + fn active_mode_expires() { + let mode = ActiveMode { + workflow: WorkflowKind::Ultrawork, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 10, + turn_limit: 10, + metadata: HashMap::new(), + }; + assert!(mode.is_expired()); + } + + #[test] + fn active_mode_not_expired() { + let mode = ActiveMode { + workflow: WorkflowKind::Ultrawork, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 5, + turn_limit: 10, + metadata: HashMap::new(), + }; + assert!(!mode.is_expired()); + } +} diff --git a/crates/jcode-keywords/src/task_size.rs b/crates/jcode-keywords/src/task_size.rs new file mode 100644 index 000000000..652f2f995 --- /dev/null +++ b/crates/jcode-keywords/src/task_size.rs @@ -0,0 +1,102 @@ +//! Task size classification — suppress heavy modes for simple tasks. + +/// Task size classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TaskSize { + /// Simple: under 50 chars, no code blocks, no multi-line + Simple, + /// Medium: 50-200 chars, or has some structure + Medium, + /// Heavy: over 200 chars, has code blocks, multi-step instructions + Heavy, +} + +/// Classify the task size from user input. +/// +/// Simple tasks suppress Heavy workflows (ultrawork, ralplan, ultraqa) +/// to avoid unnecessary overhead. +pub fn classify(input: &str) -> TaskSize { + let trimmed = input.trim(); + + if trimmed.is_empty() { + return TaskSize::Simple; + } + + let has_code_block = trimmed.contains("```"); + let line_count = trimmed.lines().count(); + let char_count = trimmed.len(); + + if char_count > 200 || (has_code_block && line_count > 5) { + TaskSize::Heavy + } else if char_count > 50 || has_code_block || line_count > 3 { + TaskSize::Medium + } else { + TaskSize::Simple + } +} + +/// Check if a workflow should be suppressed given the task size. +/// +/// Heavy workflows are suppressed for Simple tasks. +pub fn should_suppress(workflow: crate::registry::WorkflowKind, task_size: TaskSize) -> bool { + use crate::registry::WorkflowKind; + + if task_size != TaskSize::Simple { + return false; + } + + matches!( + workflow, + WorkflowKind::Ultrawork + | WorkflowKind::Ralplan + | WorkflowKind::Ultraqa + | WorkflowKind::DeepInterview + | WorkflowKind::SecurityReview + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::registry::WorkflowKind; + + #[test] + fn simple_task() { + assert_eq!(classify("fix the bug"), TaskSize::Simple); + assert_eq!(classify("hello"), TaskSize::Simple); + assert_eq!(classify(""), TaskSize::Simple); + } + + #[test] + fn medium_task() { + assert_eq!( + classify( + "Please refactor the authentication module to use JWT tokens instead of sessions" + ), + TaskSize::Medium + ); + assert_eq!(classify("```\nfn main() {}\n```"), TaskSize::Medium); + } + + #[test] + fn heavy_task() { + let heavy_input = "a".repeat(250); + assert_eq!(classify(&heavy_input), TaskSize::Heavy); + + let code_heavy = "```\nline1\nline2\nline3\nline4\nline5\nline6\nline7\n```"; + assert_eq!(classify(code_heavy), TaskSize::Heavy); + } + + #[test] + fn suppress_heavy_for_simple() { + assert!(should_suppress(WorkflowKind::Ultrawork, TaskSize::Simple)); + assert!(!should_suppress(WorkflowKind::Ultrawork, TaskSize::Medium)); + assert!(!should_suppress(WorkflowKind::Ultrawork, TaskSize::Heavy)); + } + + #[test] + fn never_suppress_lightweight() { + assert!(!should_suppress(WorkflowKind::Ultrathink, TaskSize::Simple)); + assert!(!should_suppress(WorkflowKind::Wiki, TaskSize::Simple)); + } +} diff --git a/crates/jcode-keywords/src/visual.rs b/crates/jcode-keywords/src/visual.rs new file mode 100644 index 000000000..7d66b7fd9 --- /dev/null +++ b/crates/jcode-keywords/src/visual.rs @@ -0,0 +1,136 @@ +//! Visual effects — keyword highlight spans for TUI rendering. + +use crate::detector::detect_keywords; + +/// A highlight span for a detected keyword in the input. +#[derive(Debug, Clone)] +pub struct KeywordHighlight { + /// Byte offset start in the input string. + pub start: usize, + /// Byte offset end in the input string. + pub end: usize, + /// RGB color for rainbow effect. + pub color: (u8, u8, u8), + /// The keyword label (e.g. "$ultrawork"). + pub label: String, + /// Priority of the matched keyword. + pub priority: u8, +} + +/// Compute highlight spans for detected keywords in input text. +pub fn compute_highlights(input: &str) -> Vec { + let detections = detect_keywords(input); + detections + .into_iter() + .enumerate() + .map(|(i, det)| { + let color = rainbow_color(i, det.entry.priority); + KeywordHighlight { + start: det.position.0, + end: det.position.1, + color, + label: det.matched_text, + priority: det.entry.priority, + } + }) + .collect() +} + +/// Generate a rainbow RGB color based on index and priority. +/// +/// Higher priority → warmer colors (red/orange). +/// Lower priority → cooler colors (blue/purple). +fn rainbow_color(index: usize, priority: u8) -> (u8, u8, u8) { + // Base hue from priority: 0 (red) to 270 (purple) + let base_hue = ((11 - priority) as f32 / 11.0) * 270.0; + // Offset by index for variety + let hue = (base_hue + (index as f32 * 30.0)) % 360.0; + hsv_to_rgb(hue, 0.8, 0.95) +} + +/// Convert HSV to RGB. +fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { + let c = v * s; + let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs()); + let m = v - c; + + let (r, g, b) = if h < 60.0 { + (c, x, 0.0) + } else if h < 120.0 { + (x, c, 0.0) + } else if h < 180.0 { + (0.0, c, x) + } else if h < 240.0 { + (0.0, x, c) + } else if h < 300.0 { + (x, 0.0, c) + } else { + (c, 0.0, x) + }; + + ( + ((r + m) * 255.0) as u8, + ((g + m) * 255.0) as u8, + ((b + m) * 255.0) as u8, + ) +} + +/// Format a highlight as a display string for status notices. +pub fn format_highlight_notice(highlights: &[KeywordHighlight]) -> Option { + if highlights.is_empty() { + return None; + } + + let labels: Vec<&str> = highlights.iter().map(|h| h.label.as_str()).collect(); + Some(format!("✨ Keywords: {}", labels.join(", "))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compute_highlights_empty_input() { + assert!(compute_highlights("").is_empty()); + } + + #[test] + fn compute_highlights_detects_keyword() { + let highlights = compute_highlights("$ultrawork fix the bug"); + assert_eq!(highlights.len(), 1); + assert_eq!(highlights[0].label, "$ultrawork"); + } + + #[test] + fn rainbow_color_varies() { + let c1 = rainbow_color(0, 10); + let c2 = rainbow_color(1, 10); + assert_ne!(c1, c2); + } + + #[test] + fn hsv_to_rgb_pure_red() { + let (r, g, b) = hsv_to_rgb(0.0, 1.0, 1.0); + assert_eq!(r, 255); + assert_eq!(g, 0); + assert_eq!(b, 0); + } + + #[test] + fn format_highlight_notice_empty() { + assert!(format_highlight_notice(&[]).is_none()); + } + + #[test] + fn format_highlight_notice_with_keywords() { + let highlights = vec![KeywordHighlight { + start: 0, + end: 10, + color: (255, 0, 0), + label: "$ultrawork".to_string(), + priority: 10, + }]; + let notice = format_highlight_notice(&highlights); + assert!(notice.unwrap().contains("$ultrawork")); + } +} diff --git a/crates/jcode-keywords/src/workflow/ai_slop_cleaner.rs b/crates/jcode-keywords/src/workflow/ai_slop_cleaner.rs new file mode 100644 index 000000000..b6438157a --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ai_slop_cleaner.rs @@ -0,0 +1,33 @@ +//! AiSlopCleaner — SlopCleanup workflow handler. +//! +//! Tier 1: Prompt-only. Injects AI code quality improvement instructions. + +use super::WorkflowHandler; +use crate::registry::WorkflowKind; + +pub struct AiSlopCleanerHandler; + +impl WorkflowHandler for AiSlopCleanerHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::AiSlopCleaner + } + + fn build_prompt(&self) -> String { + "# ai-slop-cleaner — AI Slop Cleanup Mode\n\n\ + Detect and fix low-quality AI-generated code.\n\n\ + ## Look For\n\ + 1. Redundant comments (restating the code)\n\ + 2. Over-abstraction (unnecessary wrappers)\n\ + 3. Dead code (unused imports, variables)\n\ + 4. Verbose patterns (could be simplified)\n\ + 5. Generic names (data, result, temp, helper)\n\ + 6. Unnecessary .clone() calls\n\n\ + ## Rules\n\ + - Don't change behavior\n\ + - Preserve public API contracts\n\ + - Keep fixes minimal and focused" + .to_string() + } + + // Use trait default: Continue +} diff --git a/crates/jcode-keywords/src/workflow/analyze.rs b/crates/jcode-keywords/src/workflow/analyze.rs new file mode 100644 index 000000000..75bd3211c --- /dev/null +++ b/crates/jcode-keywords/src/workflow/analyze.rs @@ -0,0 +1,32 @@ +//! Analyze — DeepAnalysis workflow handler. +//! +//! Tier 1: Prompt-only. Injects structured analysis instructions. + +use super::WorkflowHandler; +use crate::registry::WorkflowKind; + +pub struct AnalyzeHandler; + +impl WorkflowHandler for AnalyzeHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Analyze + } + + fn build_prompt(&self) -> String { + "# $analyze — Deep Analysis Mode\n\n\ + Perform structured, thorough analysis.\n\n\ + ## Strategy\n\ + 1. Map architecture and dependencies\n\ + 2. Identify patterns and anti-patterns\n\ + 3. Assess complexity and quality\n\ + 4. Generate ranked recommendations\n\n\ + ## Output\n\ + - Summary paragraph\n\ + - Findings with severity (Critical/High/Medium/Low)\n\ + - file:line references\n\ + - Top 3 priority actions" + .to_string() + } + + // Use trait default: Continue +} diff --git a/crates/jcode-keywords/src/workflow/cancel.rs b/crates/jcode-keywords/src/workflow/cancel.rs new file mode 100644 index 000000000..486b7bc8f --- /dev/null +++ b/crates/jcode-keywords/src/workflow/cancel.rs @@ -0,0 +1,28 @@ +//! Cancel — CancelAll workflow handler. +//! +//! Tier 6: System action. Cancel is handled entirely by `state::update_modes()` +//! which clears all modes before execute() is ever called. These methods are +//! no-ops in the normal flow. + +use super::WorkflowHandler; +use crate::registry::WorkflowKind; + +pub struct CancelHandler; + +impl WorkflowHandler for CancelHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Cancel + } + + fn build_prompt(&self) -> String { + "# canceljcode — All Modes Cancelled\n\n\ + Returning to normal operation." + .to_string() + } + + // Note: execute() and on_turn_complete() are intentionally not overridden. + // Cancel is handled by state::update_modes() which clears all modes + // before execute_active_workflows() iterates them. The trait defaults + // (returning Continue) are correct — this handler is unreachable in + // the normal flow. +} diff --git a/crates/jcode-keywords/src/workflow/code_review.rs b/crates/jcode-keywords/src/workflow/code_review.rs new file mode 100644 index 000000000..aeb9243ed --- /dev/null +++ b/crates/jcode-keywords/src/workflow/code_review.rs @@ -0,0 +1,46 @@ +//! CodeReview — workflow handler. +//! +//! Tier 2: Sub-agent spawning. Spawns a reviewer agent. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler, sanitize_user_input}; +use crate::registry::WorkflowKind; + +pub struct CodeReviewHandler; + +impl WorkflowHandler for CodeReviewHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::CodeReview + } + + fn build_prompt(&self) -> String { + "# $code-review — Code Review Mode\n\n\ + Perform thorough code review.\n\n\ + ## Checklist\n\ + - Correctness: logic errors, edge cases\n\ + - Style: naming, conventions\n\ + - Performance: unnecessary allocations\n\ + - Security: input validation, injection\n\ + - Testing: coverage, missing tests\n\n\ + ## Output\n\ + Overall: Pass / Needs Changes / Critical\n\ + Findings: Severity + Location + Issue + Suggestion" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let safe_input = sanitize_user_input(ctx.user_input); + WorkflowAction::SpawnAgent { + description: "Code reviewer".to_string(), + prompt: format!( + "Review the following code/task thoroughly:\n\n{}\n\n\ + Provide a structured review with severity ratings.", + safe_input + ), + system_prompt: "You are an expert code reviewer. Be thorough but fair. \ + Focus on correctness, security, and maintainability. \ + Rate each finding by severity." + .to_string(), + max_turns: 8, + } + } +} diff --git a/crates/jcode-keywords/src/workflow/deep_interview.rs b/crates/jcode-keywords/src/workflow/deep_interview.rs new file mode 100644 index 000000000..227841890 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/deep_interview.rs @@ -0,0 +1,192 @@ +//! DeepInterview — RequirementsGathering workflow handler. +//! +//! Tier 4: Interactive. Asks clarifying questions, tracks ambiguity score. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct DeepInterviewHandler; + +const MAX_ROUNDS: u32 = 5; +const AMBIGUITY_THRESHOLD: u32 = 3; + +impl WorkflowHandler for DeepInterviewHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::DeepInterview + } + + fn build_prompt(&self) -> String { + "# $deep-interview — Requirements Gathering Mode\n\n\ + Gather requirements through Q&A.\n\n\ + ## Process\n\ + 1. Analyze request for ambiguity\n\ + 2. Ask clarifying questions (max 3 per round)\n\ + 3. Score ambiguity 1-10\n\ + 4. Repeat until ambiguity < 3\n\n\ + ## Ambiguity Score\n\ + Report as: `Ambiguity: N/10`\n\n\ + ## Completion Marker\n\ + When done: `[INTERVIEW:COMPLETE]`" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let round: u32 = ctx + .metadata + .get("interview_round") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let ambiguity: u32 = ctx + .metadata + .get("ambiguity_score") + .and_then(|s| s.parse().ok()) + .unwrap_or(5); + + if round >= MAX_ROUNDS { + return WorkflowAction::Complete(format!("Interview complete after {} rounds.", round)); + } + + if ambiguity < AMBIGUITY_THRESHOLD { + return WorkflowAction::Complete("Requirements are clear. Proceeding.".to_string()); + } + + let reminder = if round == 0 { + format!( + "## Deep Interview — Round {}/{}\n\n\ + Analyze for ambiguity:\n{}\n\n\ + Ask up to 3 clarifying questions.\n\ + Report ambiguity as: `Ambiguity: N/10`", + round + 1, + MAX_ROUNDS, + ctx.user_input + ) + } else { + format!( + "## Deep Interview — Round {}/{}\n\n\ + Current ambiguity: {}/10\n\ + Target: below {}/10\n\ + Ask follow-up questions.", + round + 1, + MAX_ROUNDS, + ambiguity, + AMBIGUITY_THRESHOLD + ) + }; + + let mut metadata = ctx.metadata.clone(); + metadata.insert("interview_round".to_string(), (round + 1).to_string()); + if !metadata.contains_key("ambiguity_score") { + metadata.insert("ambiguity_score".to_string(), "5".to_string()); + } + + WorkflowAction::ContinueWithMetadata { reminder, metadata } + } + + fn on_turn_complete( + &self, + response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + // Check for explicit completion marker + if response.contains("[INTERVIEW:COMPLETE]") { + return WorkflowAction::Complete("Requirements gathered.".to_string()); + } + + let round: u32 = metadata + .get("interview_round") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + // Extract ambiguity score using tighter pattern + let new_ambiguity = extract_ambiguity_score(response).unwrap_or(4); + + if new_ambiguity < AMBIGUITY_THRESHOLD { + return WorkflowAction::Complete( + "Requirements gathered. Ambiguity is low.".to_string(), + ); + } + + if round >= MAX_ROUNDS { + return WorkflowAction::Complete(format!( + "Interview complete after {} rounds. Ambiguity: {}/10", + round, new_ambiguity + )); + } + + let mut updated = metadata.clone(); + updated.insert("ambiguity_score".to_string(), new_ambiguity.to_string()); + + WorkflowAction::ContinueWithMetadata { + reminder: format!("Ambiguity: {}/10. Continuing interview...", new_ambiguity), + metadata: updated, + } + } +} + +/// Extract ambiguity score from LLM response. +/// Uses tight pattern: requires "ambiguity" on the same line as a N/10 pattern. +fn extract_ambiguity_score(response: &str) -> Option { + let lower = response.to_lowercase(); + + for line in lower.lines() { + if !line.contains("ambiguity") { + continue; + } + // Look for N/10 pattern specifically + if let Some(pos) = line.find("/10") { + let before = &line[..pos]; + let num_str: String = before + .chars() + .rev() + .take_while(|c| c.is_ascii_digit()) + .collect::>() + .into_iter() + .rev() + .collect(); + if let Ok(n) = num_str.parse::() { + if n <= 10 { + return Some(n); + } + } + } + // Fallback: look for "ambiguity.*N" pattern + for word in line.split_whitespace() { + if let Ok(n) = word.parse::() { + if n <= 10 { + return Some(n); + } + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_score_from_n_over_10() { + assert_eq!(extract_ambiguity_score("Ambiguity: 7/10"), Some(7)); + assert_eq!( + extract_ambiguity_score("The ambiguity is about 3/10"), + Some(3) + ); + } + + #[test] + fn extract_score_requires_ambiguity_keyword() { + // Should NOT match "score" without "ambiguity" + assert_eq!(extract_ambiguity_score("The security score is 8/10"), None); + assert_eq!(extract_ambiguity_score("Performance score: 6"), None); + } + + #[test] + fn extract_score_no_match() { + assert_eq!(extract_ambiguity_score("No score here"), None); + assert_eq!(extract_ambiguity_score(""), None); + } +} diff --git a/crates/jcode-keywords/src/workflow/deepsearch.rs b/crates/jcode-keywords/src/workflow/deepsearch.rs new file mode 100644 index 000000000..815508ad2 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/deepsearch.rs @@ -0,0 +1,66 @@ +//! Deepsearch — CodebaseSearch workflow handler. +//! +//! Tier 2: Sub-agent spawning. Spawns parallel search agents with different strategies. + +use super::{SpawnSpec, WorkflowAction, WorkflowContext, WorkflowHandler, sanitize_user_input}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct DeepsearchHandler; + +impl WorkflowHandler for DeepsearchHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Deepsearch + } + + fn build_prompt(&self) -> String { + "# $deepsearch — Codebase Search Mode\n\n\ + Use multiple search strategies.\n\n\ + ## Strategies\n\ + 1. Text/Regex: grep for keywords, patterns\n\ + 2. Structural: find functions, types, modules\n\ + 3. Semantic: find related concepts, similar code\n\n\ + ## Output\n\ + Context Map: file:line — Description\n\ + Summary: How found code relates to query" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + // Guard: don't re-spawn if already spawned + if ctx.metadata.contains_key("deepsearch_spawned") { + return WorkflowAction::Continue; + } + + let safe_input = sanitize_user_input(ctx.user_input); + let specs = vec![ + SpawnSpec { + description: "Text/regex search".to_string(), + prompt: format!("Search the codebase for text patterns related to:\n{}\n\nReport file:line matches.", safe_input), + system_prompt: "You are a text search agent. Use file_grep tool extensively. Report results as file:line:content.".to_string(), + max_turns: 5, + }, + SpawnSpec { + description: "Structural search".to_string(), + prompt: format!("Search for structural elements (functions, types, modules) related to:\n{}", safe_input), + system_prompt: "You are a structural search agent. Find code structures — function signatures, type definitions, module structure.".to_string(), + max_turns: 5, + }, + ]; + + WorkflowAction::SpawnParallel(specs) + } + + fn on_turn_complete( + &self, + _response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + if metadata.contains_key("deepsearch_spawned") { + return WorkflowAction::Complete( + "Codebase search complete. Context map generated.".to_string(), + ); + } + WorkflowAction::Continue + } +} diff --git a/crates/jcode-keywords/src/workflow/executor.rs b/crates/jcode-keywords/src/workflow/executor.rs new file mode 100644 index 000000000..2e062a6fa --- /dev/null +++ b/crates/jcode-keywords/src/workflow/executor.rs @@ -0,0 +1,464 @@ +//! Workflow execution engine. +//! +//! Bridges the keyword system with the agent runtime. Called from the turn loop +//! to execute active workflows and produce actions (spawn agents, inject reminders, etc.). + +use super::{WorkflowAction, WorkflowContext}; +use crate::registry::WorkflowKind; +use crate::state::ModeState; +use crate::task_size::TaskSize; + +/// Truncate a string to at most `max_chars` Unicode scalar values +/// (i.e. characters), respecting UTF-8 boundaries. +fn truncate_str(s: &str, max_chars: usize) -> &str { + if s.chars().count() <= max_chars { + return s; + } + // Walk char indices and stop at the max_chars-th character. + match s.char_indices().nth(max_chars) { + Some((byte_idx, _)) => &s[..byte_idx], + None => s, + } +} + +/// Execute all active workflows for the current turn. +/// +/// Returns actions paired with the index of the active mode that produced them. +/// The caller is responsible for persisting metadata from `ContinueWithMetadata`. +pub fn execute_active_workflows( + mode_state: &ModeState, + user_input: &str, + working_dir: Option<&std::path::Path>, + session_id: &str, + task_size: TaskSize, +) -> Vec<(usize, WorkflowKind, WorkflowAction)> { + let mut actions = Vec::new(); + + for (i, active_mode) in mode_state.active_modes.iter().enumerate() { + // Skip cancel — it's handled by state::update_modes() + if active_mode.workflow == WorkflowKind::Cancel { + continue; + } + + let Some(handler) = crate::workflow::get_handler(active_mode.workflow) else { + continue; + }; + + // Heavy workflows are suppressed for Simple tasks (e.g. one-line requests) + // so we don't burn tokens on a multi-agent workflow for a trivial fix. + if handler.should_suppress_for_task_size(task_size) { + continue; + } + + let ctx = WorkflowContext { + user_input, + working_dir: working_dir.map(|p| p), + session_id, + mode_state, + metadata: &active_mode.metadata, + }; + + let action = handler.execute(&ctx); + actions.push((i, active_mode.workflow, action)); + } + + actions +} + +/// Process the LLM's response through all active workflow handlers. +/// +/// Returns actions paired with the index of the active mode that produced them. +pub fn process_turn_response( + mode_state: &ModeState, + response: &str, +) -> Vec<(usize, WorkflowKind, WorkflowAction)> { + let mut actions = Vec::new(); + + for (i, active_mode) in mode_state.active_modes.iter().enumerate() { + if active_mode.workflow == WorkflowKind::Cancel { + continue; + } + + let Some(handler) = crate::workflow::get_handler(active_mode.workflow) else { + continue; + }; + + let action = handler.on_turn_complete(response, &active_mode.metadata); + actions.push((i, active_mode.workflow, action)); + } + + actions +} + +/// Spawn actions whose execution was deferred to the caller. +/// +/// `SpawnAgent` and `SpawnParallel` need access to the agent runtime +/// (provider, tool registry, etc.) which lives in `jcode-app-core`, not in +/// `jcode-keywords`. `apply_actions` records the spawn in metadata and +/// returns these so the caller can dispatch them via `SubagentTool`. +#[derive(Debug, Clone)] +pub struct DeferredSpawn { + /// Index of the active mode that produced the spawn. + pub mode_index: usize, + /// The workflow kind that requested the spawn. + pub kind: WorkflowKind, + /// The action to dispatch. + pub action: WorkflowAction, +} + +/// Apply workflow actions to mode state (metadata persistence, mode deactivation). +/// +/// This is the key function that persists `ContinueWithMetadata` and `Complete` actions. +/// Returns `(summaries, deferred_spawns)`. Spawn actions are recorded in +/// metadata so we do not loop, and surfaced to the caller for execution. +pub fn apply_actions( + mode_state: &mut ModeState, + actions: &[(usize, WorkflowKind, WorkflowAction)], +) -> (Vec, Vec) { + let mut summaries = Vec::new(); + let mut to_remove = Vec::new(); + let mut deferred_spawns = Vec::new(); + + for (idx, kind, action) in actions { + match action { + WorkflowAction::ContinueWithMetadata { metadata, reminder } => { + if let Some(mode) = mode_state.active_modes.get_mut(*idx) { + // Merge new metadata into existing (don't discard) + for (k, v) in metadata { + mode.metadata.insert(k.clone(), v.clone()); + } + summaries.push(format!( + "{}: updated metadata, reminder: {}", + kind, + truncate_str(reminder, 50) + )); + } + } + WorkflowAction::Complete(msg) => { + to_remove.push(*idx); + summaries.push(format!("{}: completed — {}", kind, msg)); + } + WorkflowAction::Error(msg) => { + to_remove.push(*idx); + summaries.push(format!("{}: error — {}", kind, msg)); + } + WorkflowAction::InjectReminder(r) => { + summaries.push(format!( + "{}: inject reminder — {}", + kind, + truncate_str(r, 50) + )); + } + WorkflowAction::SpawnAgent { description, .. } => { + if let Some(mode) = mode_state.active_modes.get_mut(*idx) { + mode.metadata + .insert(format!("{}_spawned", kind), "true".to_string()); + } + summaries.push(format!( + "{}: spawn agent deferred — {} (caller must dispatch via SubagentTool)", + kind, description + )); + deferred_spawns.push(DeferredSpawn { + mode_index: *idx, + kind: *kind, + action: action.clone(), + }); + } + WorkflowAction::SpawnParallel(specs) => { + if let Some(mode) = mode_state.active_modes.get_mut(*idx) { + mode.metadata + .insert(format!("{}_spawned", kind), "true".to_string()); + } + summaries.push(format!( + "{}: spawn {} agents deferred (caller must dispatch via SubagentTool)", + kind, + specs.len() + )); + for spec in specs { + deferred_spawns.push(DeferredSpawn { + mode_index: *idx, + kind: *kind, + action: WorkflowAction::SpawnAgent { + description: spec.description.clone(), + prompt: spec.prompt.clone(), + system_prompt: spec.system_prompt.clone(), + max_turns: spec.max_turns, + }, + }); + } + } + WorkflowAction::AskUser(q) => { + summaries.push(format!("{}: ask user — {}", kind, truncate_str(q, 50))); + } + WorkflowAction::Continue => {} + } + } + + // Remove completed/errored modes (reverse order to preserve indices) + to_remove.sort_unstable(); + to_remove.dedup(); + for idx in to_remove.into_iter().rev() { + if idx < mode_state.active_modes.len() { + mode_state.active_modes.remove(idx); + } + } + + mode_state.updated_at = Some(chrono::Utc::now().to_rfc3339()); + (summaries, deferred_spawns) +} + +/// Result of a turn's keyword processing. +pub struct TurnResult { + /// Prompt section to inject into the system prompt's dynamic part. + /// `None` means no active workflow (or empty input). + pub keyword_prompt: Option, + /// Mode conflicts (TDD + ultrawork, etc.) detected among the now-active + /// modes. Callers are expected to surface these to logs/UI. + pub conflicts: Vec, +} + +/// One-shot keyword processing for a turn. +/// +/// This is the canonical entry point used by both the agent runtime and the +/// TUI: it runs the full detect -> update -> process-response -> execute +/// pipeline against the latest user input, persists state to disk, and +/// returns the workflow prompt section to inject into the system prompt. +/// +/// `keyword_prompt` is `None` if the input is empty or no workflow is active. +/// `conflicts` is always computed when there is input and at least one +/// active mode. +pub fn process_turn( + latest_input: &str, + last_assistant: Option<&str>, + working_dir: Option<&std::path::Path>, + session_id: &str, +) -> TurnResult { + if latest_input.is_empty() { + return TurnResult { + keyword_prompt: None, + conflicts: Vec::new(), + }; + } + + let detections = crate::detector::detect_keywords(latest_input); + let mut mode_state = crate::state::update_modes(&detections, working_dir); + + // Process PREVIOUS turn's LLM response (phase transitions, completion) + if let Some(prev) = last_assistant { + let response_actions = process_turn_response(&mode_state, prev); + if !response_actions.is_empty() { + let _ = apply_actions(&mut mode_state, &response_actions); + } + } + + // Execute active workflows for THIS turn (heavy ones suppress on simple input) + let task_size = crate::task_size::classify(latest_input); + let actions = execute_active_workflows( + &mode_state, + latest_input, + working_dir, + session_id, + task_size, + ); + if !actions.is_empty() { + let _ = apply_actions(&mut mode_state, &actions); + } + + // Detect conflicts among the now-active modes (TDD + ultrawork, etc.) + let active_kinds: Vec = + mode_state.active_modes.iter().map(|m| m.workflow).collect(); + let conflicts = crate::conflict::check_conflicts(&active_kinds); + + // Persist state + crate::state::save_state(&mode_state, working_dir); + + let prompt = build_workflow_prompt(&mode_state); + TurnResult { + keyword_prompt: if prompt.is_empty() { + None + } else { + Some(prompt) + }, + conflicts, + } +} + +/// Build the combined workflow prompt injection for all active modes. +/// +/// This is the text that gets injected into the system prompt's dynamic_part. +pub fn build_workflow_prompt(mode_state: &ModeState) -> String { + if mode_state.active_modes.is_empty() { + return String::new(); + } + + let mut sections = Vec::new(); + sections.push("# Active Workflow Modes\n".to_string()); + + for active_mode in &mode_state.active_modes { + let Some(handler) = crate::workflow::get_handler(active_mode.workflow) else { + continue; + }; + + let prompt = handler.build_prompt(); + let remaining = active_mode + .turn_limit + .saturating_sub(active_mode.turn_count); + sections.push(format!( + "## {} ({} turns remaining)\n\n{}\n", + active_mode.workflow, remaining, prompt + )); + } + + sections.join("") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::ActiveMode; + use std::collections::HashMap; + + #[test] + fn execute_empty_state() { + let state = ModeState::default(); + let actions = + execute_active_workflows(&state, "hello", None, "test-session", TaskSize::Medium); + assert!(actions.is_empty()); + } + + #[test] + fn process_empty_state() { + let state = ModeState::default(); + let actions = process_turn_response(&state, "hello"); + assert!(actions.is_empty()); + } + + #[test] + fn build_workflow_prompt_empty() { + let state = ModeState::default(); + assert!(build_workflow_prompt(&state).is_empty()); + } + + #[test] + fn build_workflow_prompt_with_active_mode() { + let state = ModeState { + active_modes: vec![ActiveMode { + workflow: WorkflowKind::Ultrathink, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 0, + turn_limit: 10, + metadata: HashMap::new(), + }], + updated_at: None, + }; + let prompt = build_workflow_prompt(&state); + assert!(prompt.contains("ultrathink")); + assert!(prompt.contains("10 turns remaining")); + } + + #[test] + fn apply_actions_persists_metadata() { + let mut state = ModeState { + active_modes: vec![ActiveMode { + workflow: WorkflowKind::Tdd, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 0, + turn_limit: 10, + metadata: HashMap::new(), + }], + updated_at: None, + }; + let mut new_meta = HashMap::new(); + new_meta.insert("tdd_phase".to_string(), "green".to_string()); + let actions = vec![( + 0, + WorkflowKind::Tdd, + WorkflowAction::ContinueWithMetadata { + reminder: "test".to_string(), + metadata: new_meta, + }, + )]; + apply_actions(&mut state, &actions); + assert_eq!( + state.active_modes[0].metadata.get("tdd_phase").unwrap(), + "green" + ); + } + + #[test] + fn apply_actions_removes_completed() { + let mut state = ModeState { + active_modes: vec![ + ActiveMode { + workflow: WorkflowKind::Tdd, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 0, + turn_limit: 10, + metadata: HashMap::new(), + }, + ActiveMode { + workflow: WorkflowKind::Ultrathink, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 0, + turn_limit: 10, + metadata: HashMap::new(), + }, + ], + updated_at: None, + }; + let actions = vec![( + 0, + WorkflowKind::Tdd, + WorkflowAction::Complete("done".to_string()), + )]; + let (_summaries, deferred) = apply_actions(&mut state, &actions); + assert!(deferred.is_empty()); + assert_eq!(state.active_modes.len(), 1); + assert_eq!(state.active_modes[0].workflow, WorkflowKind::Ultrathink); + } + + #[test] + fn apply_actions_defers_spawn_actions() { + let mut state = ModeState { + active_modes: vec![ActiveMode { + workflow: WorkflowKind::CodeReview, + activated_at: "2026-01-01T00:00:00Z".to_string(), + turn_count: 0, + turn_limit: 10, + metadata: HashMap::new(), + }], + updated_at: None, + }; + let actions = vec![( + 0, + WorkflowKind::CodeReview, + WorkflowAction::SpawnAgent { + description: "test agent".to_string(), + prompt: "do thing".to_string(), + system_prompt: "you are a tester".to_string(), + max_turns: 5, + }, + )]; + let (_summaries, deferred) = apply_actions(&mut state, &actions); + assert_eq!(deferred.len(), 1); + // Metadata flag is set so we do not loop + assert_eq!( + state.active_modes[0].metadata.get("code-review_spawned"), + Some(&"true".to_string()) + ); + } + + #[test] + fn truncate_str_respects_char_boundaries() { + // 50 chars of CJK = 150 bytes; must keep all 50 chars, not truncate to 50 bytes. + let s: String = "中".repeat(100); + let out = truncate_str(&s, 50); + assert_eq!(out.chars().count(), 50); + assert!(out.chars().all(|c| c == '中')); + } + + #[test] + fn truncate_str_short_input_passes_through() { + assert_eq!(truncate_str("hello", 50), "hello"); + } +} diff --git a/crates/jcode-keywords/src/workflow/mod.rs b/crates/jcode-keywords/src/workflow/mod.rs new file mode 100644 index 000000000..846285045 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/mod.rs @@ -0,0 +1,139 @@ +//! Workflow handlers — trait definition, execution context, and dispatch for keyword-triggered workflows. + +use crate::registry::WorkflowKind; +use crate::state::ModeState; +use std::collections::HashMap; + +pub mod ai_slop_cleaner; +pub mod analyze; +pub mod cancel; +pub mod code_review; +pub mod deep_interview; +pub mod deepsearch; +pub mod executor; +pub mod ralplan; +pub mod security_review; +pub mod spawn; +pub mod tdd; +pub mod ultragoal; +pub mod ultraqa; +pub mod ultrathink; +pub mod ultrawork; +pub mod wiki; + +/// Execution context passed to workflow handlers. +pub struct WorkflowContext<'a> { + /// The user's original input (with keyword stripped). + pub user_input: &'a str, + /// Working directory. + pub working_dir: Option<&'a std::path::Path>, + /// Session ID. + pub session_id: &'a str, + /// Current mode state (borrowed, not cloned). + pub mode_state: &'a ModeState, + /// Metadata from the current active mode. + pub metadata: &'a HashMap, +} + +/// Action a workflow handler wants the turn loop to take. +#[derive(Debug, Clone)] +pub enum WorkflowAction { + /// Inject a system reminder into the current turn's dynamic prompt. + InjectReminder(String), + /// Spawn a single sub-agent and wait for result. + SpawnAgent { + description: String, + prompt: String, + system_prompt: String, + max_turns: u32, + }, + /// Spawn multiple sub-agents in parallel, aggregate results. + SpawnParallel(Vec), + /// Ask the user a question (pauses workflow, resumes next turn). + AskUser(String), + /// Continue with normal LLM turn (prompt-only mode). + Continue, + /// Workflow complete, deactivate mode. Contains summary message. + Complete(String), + /// Workflow needs more turns, continue with updated metadata. + ContinueWithMetadata { + reminder: String, + metadata: HashMap, + }, + /// Workflow encountered an error. + Error(String), +} + +/// Specification for spawning a sub-agent. +#[derive(Debug, Clone)] +pub struct SpawnSpec { + pub description: String, + pub prompt: String, + pub system_prompt: String, + pub max_turns: u32, +} + +/// Result of a spawned sub-agent. +#[derive(Debug, Clone)] +pub struct SpawnResult { + pub description: String, + pub output: String, + pub success: bool, +} + +/// Enhanced workflow handler trait. +pub trait WorkflowHandler: Send + Sync { + /// The workflow kind this handler implements. + fn kind(&self) -> WorkflowKind; + + /// Build the prompt injection for this workflow (shown in system prompt). + fn build_prompt(&self) -> String; + + /// Execute the workflow. Called at the start of each turn while mode is active. + /// Default: prompt-only mode (just inject instructions). + fn execute(&self, _ctx: &WorkflowContext) -> WorkflowAction { + WorkflowAction::Continue + } + + /// Called after each turn to process the LLM's response and decide next action. + /// Default: no-op, workflow continues. + fn on_turn_complete( + &self, + _response: &str, + _metadata: &HashMap, + ) -> WorkflowAction { + WorkflowAction::Continue + } + + /// Whether this workflow should suppress its heavy behavior for simple tasks. + fn should_suppress_for_task_size(&self, task_size: crate::task_size::TaskSize) -> bool { + crate::task_size::should_suppress(self.kind(), task_size) + } +} + +/// Get a handler reference for a workflow kind (zero-allocation dispatch). +pub fn get_handler(kind: WorkflowKind) -> Option<&'static dyn WorkflowHandler> { + Some(match kind { + WorkflowKind::Ultrawork => &ultrawork::UltraworkHandler, + WorkflowKind::Ultragoal => &ultragoal::UltragoalHandler, + WorkflowKind::Ultraqa => &ultraqa::UltraqaHandler, + WorkflowKind::Ralplan => &ralplan::RalplanHandler, + WorkflowKind::DeepInterview => &deep_interview::DeepInterviewHandler, + WorkflowKind::Tdd => &tdd::TddHandler, + WorkflowKind::CodeReview => &code_review::CodeReviewHandler, + WorkflowKind::SecurityReview => &security_review::SecurityReviewHandler, + WorkflowKind::Ultrathink => &ultrathink::UltrathinkHandler, + WorkflowKind::Deepsearch => &deepsearch::DeepsearchHandler, + WorkflowKind::Analyze => &analyze::AnalyzeHandler, + WorkflowKind::Wiki => &wiki::WikiHandler, + WorkflowKind::AiSlopCleaner => &ai_slop_cleaner::AiSlopCleanerHandler, + WorkflowKind::Cancel => &cancel::CancelHandler, + }) +} + +/// Wrap user input in delimiters to prevent prompt injection in sub-agent prompts. +/// Escapes the closing delimiter within the input to prevent breakout attacks. +pub fn sanitize_user_input(input: &str) -> String { + let escaped = input.replace("", "<\\/user_request>"); + format!("\n{}\n", escaped) +} diff --git a/crates/jcode-keywords/src/workflow/ralplan.rs b/crates/jcode-keywords/src/workflow/ralplan.rs new file mode 100644 index 000000000..92983c244 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ralplan.rs @@ -0,0 +1,111 @@ +//! Ralplan — ConsensusPlanning workflow handler. +//! +//! Tier 3: Loop orchestration. Runs plan → review → revise → approve cycles. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct RalplanHandler; + +impl WorkflowHandler for RalplanHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Ralplan + } + + fn build_prompt(&self) -> String { + "# $ralplan — Consensus Planning Mode\n\n\ + Generate, review, and refine plans.\n\n\ + ## Cycle\n\ + 1. PLAN: Generate a detailed plan\n\ + 2. REVIEW: Self-review for risks and gaps\n\ + 3. REVISE: Address issues found\n\ + 4. APPROVE: Present for user approval\n\n\ + ## Completion Markers\n\ + Plan ready: `[PHASE:PLAN_DONE]`\n\ + Review done: `[PHASE:REVIEW_DONE]`\n\ + Revision done: `[PHASE:REVISED]`\n\ + User approved: `[PHASE:APPROVED]`\n\ + Execution done: `[PHASE:EXECUTED]`" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let phase = ctx + .metadata + .get("ralplan_phase") + .map(|s| s.as_str()) + .unwrap_or("plan"); + + let reminder = match phase { + "plan" => format!( + "## Ralplan — Phase: PLAN\n\n\ + Generate a detailed plan for:\n{}\n\n\ + Include: Goal, Steps, Risks, Assumptions.\n\ + Say `[PHASE:PLAN_DONE]` when done.", + ctx.user_input + ), + "review" => "## Ralplan — Phase: REVIEW\n\n\ + Self-review the plan:\n\ + - What could go wrong?\n\ + - What assumptions are we making?\n\ + - What's missing?\n\ + Say `[PHASE:REVIEW_DONE]` when done." + .to_string(), + "revise" => "## Ralplan — Phase: REVISE\n\n\ + Revise the plan addressing review issues.\n\ + Say `[PHASE:REVISED]` when done." + .to_string(), + "approve" => "## Ralplan — Phase: APPROVE\n\n\ + Present the final plan. Wait for user approval.\n\ + Say `[PHASE:APPROVED]` when user confirms." + .to_string(), + "execute" => "## Ralplan — Phase: EXECUTE\n\n\ + Execute the approved plan step by step.\n\ + Say `[PHASE:EXECUTED]` when done." + .to_string(), + _ => "Continue planning.".to_string(), + }; + + // DON'T advance phase here + let mut metadata = ctx.metadata.clone(); + if !metadata.contains_key("ralplan_phase") { + metadata.insert("ralplan_phase".to_string(), "plan".to_string()); + } + + WorkflowAction::ContinueWithMetadata { reminder, metadata } + } + + fn on_turn_complete( + &self, + response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + let phase = metadata + .get("ralplan_phase") + .map(|s| s.as_str()) + .unwrap_or("plan"); + + let next_phase = match phase { + "plan" if response.contains("[PHASE:PLAN_DONE]") => Some("review"), + "review" if response.contains("[PHASE:REVIEW_DONE]") => Some("revise"), + "revise" if response.contains("[PHASE:REVISED]") => Some("approve"), + "approve" if response.contains("[PHASE:APPROVED]") => Some("execute"), + "execute" if response.contains("[PHASE:EXECUTED]") => { + return WorkflowAction::Complete("Plan executed successfully.".to_string()); + } + _ => None, + }; + + if let Some(next) = next_phase { + let mut updated = metadata.clone(); + updated.insert("ralplan_phase".to_string(), next.to_string()); + WorkflowAction::ContinueWithMetadata { + reminder: format!("Advancing to {} phase.", next), + metadata: updated, + } + } else { + WorkflowAction::Continue + } + } +} diff --git a/crates/jcode-keywords/src/workflow/security_review.rs b/crates/jcode-keywords/src/workflow/security_review.rs new file mode 100644 index 000000000..a9fa93a7d --- /dev/null +++ b/crates/jcode-keywords/src/workflow/security_review.rs @@ -0,0 +1,55 @@ +//! SecurityReview — workflow handler. +//! +//! Tier 2: Sub-agent spawning. Spawns a security auditor agent. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler, sanitize_user_input}; +use crate::registry::WorkflowKind; + +pub struct SecurityReviewHandler; + +impl WorkflowHandler for SecurityReviewHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::SecurityReview + } + + fn build_prompt(&self) -> String { + "# $security-review — Security Review Mode\n\n\ + Perform comprehensive security audit.\n\n\ + ## OWASP Top 10\n\ + A01: Broken Access Control\n\ + A02: Cryptographic Failures\n\ + A03: Injection\n\ + A04: Insecure Design\n\ + A05: Security Misconfiguration\n\ + A06: Vulnerable Components\n\ + A07: Auth Failures\n\ + A08: Data Integrity\n\ + A09: Logging Failures\n\ + A10: SSRF\n\n\ + ## Also Check\n\ + - Hardcoded secrets/keys/tokens\n\ + - SQL injection, XSS, CSRF\n\ + - Path traversal\n\n\ + ## Output\n\ + Risk Summary: Critical/High/Medium/Low counts\n\ + Findings: Severity + OWASP Category + Location + Remediation" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let safe_input = sanitize_user_input(ctx.user_input); + WorkflowAction::SpawnAgent { + description: "Security auditor".to_string(), + prompt: format!( + "Perform a security audit on:\n\n{}\n\n\ + Check for OWASP Top 10 vulnerabilities, hardcoded secrets, \ + and common security issues. Provide severity ratings.", + safe_input + ), + system_prompt: "You are a security auditor. Be paranoid. Check for every \ + possible vulnerability. Rate findings by OWASP severity." + .to_string(), + max_turns: 10, + } + } +} diff --git a/crates/jcode-keywords/src/workflow/spawn.rs b/crates/jcode-keywords/src/workflow/spawn.rs new file mode 100644 index 000000000..d519b014b --- /dev/null +++ b/crates/jcode-keywords/src/workflow/spawn.rs @@ -0,0 +1,121 @@ +//! Sub-agent spawning utility for workflow execution. +//! +//! Provides helpers to spawn child agents using the same pattern as `SubagentTool` +//! in `jcode-app-core/src/tool/task.rs`. + +use super::{SpawnResult, SpawnSpec}; + +/// Maximum concurrent sub-agents per spawn call. +const MAX_CONCURRENT: usize = 4; + +/// Spawn a single sub-agent synchronously and return its output. +/// +/// This is a placeholder that will be wired to the actual Agent spawning +/// mechanism via the `WorkflowExecutor` in `jcode-app-core`. +pub async fn spawn_agent(spec: &SpawnSpec) -> SpawnResult { + // Stub implementation — real wiring happens in app-core + SpawnResult { + description: spec.description.clone(), + output: format!( + "[Workflow sub-agent '{}']: {}", + spec.description, spec.prompt + ), + success: true, + } +} + +/// Spawn multiple sub-agents in parallel and collect results. +/// Concurrency is capped at MAX_CONCURRENT. +pub async fn spawn_parallel(specs: &[SpawnSpec]) -> Vec { + let mut results = Vec::new(); + + for chunk in specs.chunks(MAX_CONCURRENT) { + let mut handles = Vec::new(); + for spec in chunk { + let spec = spec.clone(); + handles.push(tokio::spawn(async move { spawn_agent(&spec).await })); + } + for handle in handles { + match handle.await { + Ok(result) => results.push(result), + Err(e) => { + // Log JoinError instead of silently dropping + results.push(SpawnResult { + description: "unknown".to_string(), + output: format!("Sub-agent panicked: {}", e), + success: false, + }); + } + } + } + } + + results +} + +/// Aggregate results from parallel sub-agents into a single summary. +pub fn aggregate_results(results: &[SpawnResult]) -> String { + if results.is_empty() { + return "No results from sub-agents.".to_string(); + } + + let mut output = String::new(); + output.push_str("# Parallel Execution Results\n\n"); + + for (i, result) in results.iter().enumerate() { + let status = if result.success { "✅" } else { "❌" }; + output.push_str(&format!( + "## {} Task {}: {}\n\n{}\n\n", + status, i, result.description, result.output + )); + } + + let success_count = results.iter().filter(|r| r.success).count(); + output.push_str(&format!( + "---\n**Summary**: {}/{} tasks completed successfully.", + success_count, + results.len() + )); + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aggregate_empty_results() { + assert!(aggregate_results(&[]).contains("No results")); + } + + #[test] + fn aggregate_single_result() { + let results = vec![SpawnResult { + description: "test task".to_string(), + output: "done".to_string(), + success: true, + }]; + let summary = aggregate_results(&results); + assert!(summary.contains("1/1")); + assert!(summary.contains("test task")); + } + + #[test] + fn aggregate_mixed_results() { + let results = vec![ + SpawnResult { + description: "task 1".to_string(), + output: "ok".to_string(), + success: true, + }, + SpawnResult { + description: "task 2".to_string(), + output: "failed".to_string(), + success: false, + }, + ]; + let summary = aggregate_results(&results); + assert!(summary.contains("1/2")); + } +} diff --git a/crates/jcode-keywords/src/workflow/tdd.rs b/crates/jcode-keywords/src/workflow/tdd.rs new file mode 100644 index 000000000..7b96c2644 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/tdd.rs @@ -0,0 +1,101 @@ +//! Tdd — TestDrivenDev workflow handler. +//! +//! Tier 3: Loop orchestration. Runs red → green → refactor cycles. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct TddHandler; + +impl WorkflowHandler for TddHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Tdd + } + + fn build_prompt(&self) -> String { + "# $tdd — Test-Driven Development Mode\n\n\ + Follow the Red → Green → Refactor cycle.\n\n\ + ## Cycle\n\ + 1. RED: Write a failing test\n\ + 2. GREEN: Write minimal code to pass\n\ + 3. REFACTOR: Clean up while keeping tests green\n\n\ + ## Rules\n\ + - Never write code without a failing test\n\ + - Write the simplest code that works\n\ + - Refactor only when tests are green\n\n\ + ## Completion Markers\n\ + When done with RED phase, say: `[PHASE:RED_DONE]`\n\ + When done with GREEN phase, say: `[PHASE:GREEN_DONE]`\n\ + When done with REFACTOR, say: `[PHASE:REFACTORED]`" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let phase = ctx + .metadata + .get("tdd_phase") + .map(|s| s.as_str()) + .unwrap_or("red"); + + let reminder = match phase { + "red" => format!( + "## TDD — Phase: RED\n\n\ + Write a FAILING test for:\n{}\n\n\ + The test must fail. Say `[PHASE:RED_DONE]` when done.", + ctx.user_input + ), + "green" => "## TDD — Phase: GREEN\n\n\ + Write MINIMAL code to make the failing test pass.\n\ + Say `[PHASE:GREEN_DONE]` when done." + .to_string(), + "refactor" => "## TDD — Phase: REFACTOR\n\n\ + Clean up the code. Keep all tests green.\n\ + Say `[PHASE:REFACTORED]` when done." + .to_string(), + _ => "Continue TDD cycle.".to_string(), + }; + + // DON'T advance phase here — let on_turn_complete do it + let mut metadata = ctx.metadata.clone(); + if !metadata.contains_key("tdd_phase") { + metadata.insert("tdd_phase".to_string(), "red".to_string()); + } + + WorkflowAction::ContinueWithMetadata { reminder, metadata } + } + + fn on_turn_complete( + &self, + response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + let phase = metadata + .get("tdd_phase") + .map(|s| s.as_str()) + .unwrap_or("red"); + + // Use structured markers instead of fragile string matching + let next_phase = match phase { + "red" if response.contains("[PHASE:RED_DONE]") => Some("green"), + "green" if response.contains("[PHASE:GREEN_DONE]") => Some("refactor"), + "refactor" if response.contains("[PHASE:REFACTORED]") => { + return WorkflowAction::Complete( + "TDD cycle complete. Code is tested and refactored.".to_string(), + ); + } + _ => None, + }; + + if let Some(next) = next_phase { + let mut updated = metadata.clone(); + updated.insert("tdd_phase".to_string(), next.to_string()); + WorkflowAction::ContinueWithMetadata { + reminder: format!("Advancing to {} phase.", next), + metadata: updated, + } + } else { + WorkflowAction::Continue + } + } +} diff --git a/crates/jcode-keywords/src/workflow/ultragoal.rs b/crates/jcode-keywords/src/workflow/ultragoal.rs new file mode 100644 index 000000000..173a73165 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ultragoal.rs @@ -0,0 +1,160 @@ +//! Ultragoal — GoalTracking workflow handler. +//! +//! Tier 5: State management. Tracks durable goals across turns. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct UltragoalHandler; + +impl WorkflowHandler for UltragoalHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Ultragoal + } + + fn build_prompt(&self) -> String { + "# $ultragoal — Goal Tracking Mode\n\n\ + Track a durable goal across turns.\n\n\ + ## Tracking\n\ + - Goal: What we're achieving\n\ + - Progress: Percentage complete\n\ + - Budget: Token usage\n\n\ + ## Rules\n\ + Report progress after each turn.\n\ + Report as: `Progress: N%`\n\n\ + ## Completion Marker\n\ + When goal achieved: `[GOAL:ACHIEVED]`" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let goal = ctx + .metadata + .get("goal_description") + .cloned() + .unwrap_or_else(|| ctx.user_input.to_string()); + + let progress: f32 = ctx + .metadata + .get("goal_progress") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0); + + if progress >= 100.0 { + return WorkflowAction::Complete(format!("Goal achieved: {}", goal)); + } + + let reminder = format!( + "## Ultragoal — Tracking\n\n\ + **Goal**: {}\n\ + **Progress**: {:.0}%\n\n\ + Continue working. Report as: `Progress: N%`", + goal, progress + ); + + let mut metadata = ctx.metadata.clone(); + if !metadata.contains_key("goal_description") { + metadata.insert("goal_description".to_string(), goal); + } + if !metadata.contains_key("goal_progress") { + metadata.insert("goal_progress".to_string(), "0".to_string()); + } + + WorkflowAction::ContinueWithMetadata { reminder, metadata } + } + + fn on_turn_complete( + &self, + response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + // Check for explicit completion + if response.contains("[GOAL:ACHIEVED]") { + return WorkflowAction::Complete("Goal achieved!".to_string()); + } + + let mut updated = metadata.clone(); + + // Only update progress if LLM actually reported it + if let Some(new_progress) = extract_progress(response) { + updated.insert("goal_progress".to_string(), new_progress.to_string()); + if new_progress >= 100.0 { + return WorkflowAction::Complete("Goal achieved!".to_string()); + } + } + + let current_progress: f32 = updated + .get("goal_progress") + .and_then(|s| s.parse().ok()) + .unwrap_or(0.0); + + WorkflowAction::ContinueWithMetadata { + reminder: format!("Progress: {:.0}%", current_progress), + metadata: updated, + } + } +} + +/// Extract progress percentage from LLM response. +/// Requires "progress" keyword on the same line as a percentage. +fn extract_progress(response: &str) -> Option { + let lower = response.to_lowercase(); + + for line in lower.lines() { + // Only match lines with "progress" keyword + if !line.contains("progress") { + continue; + } + + // Look for N% pattern + if let Some(pos) = line.find('%') { + let before = &line[..pos]; + let num_str: String = before + .chars() + .rev() + .take_while(|c| c.is_ascii_digit() || *c == '.') + .collect::>() + .into_iter() + .rev() + .collect(); + if let Ok(num) = num_str.parse::() { + if num.is_finite() && num <= 100.0 { + return Some(num); + } + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_progress_with_keyword() { + assert_eq!(extract_progress("Progress: 45%"), Some(45.0)); + assert_eq!(extract_progress("Overall progress is 75%"), Some(75.0)); + } + + #[test] + fn extract_progress_requires_keyword() { + // Should NOT match % without "progress" + assert_eq!(extract_progress("The code has 10% test coverage"), None); + assert_eq!(extract_progress("We're 75% done"), None); + } + + #[test] + fn extract_progress_no_match() { + assert_eq!(extract_progress("No progress here"), None); + assert_eq!(extract_progress(""), None); + } + + #[test] + fn extract_progress_rejects_infinite() { + // Very large numbers should not match + assert_eq!(extract_progress("Progress: 9999999999%"), None); + } +} diff --git a/crates/jcode-keywords/src/workflow/ultraqa.rs b/crates/jcode-keywords/src/workflow/ultraqa.rs new file mode 100644 index 000000000..707b9c2da --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ultraqa.rs @@ -0,0 +1,127 @@ +//! Ultraqa — QACycling workflow handler. +//! +//! Tier 3: Loop orchestration. Runs implement → test → fix cycles. + +use super::{WorkflowAction, WorkflowContext, WorkflowHandler}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct UltraqaHandler; + +const MAX_ITERATIONS: u32 = 5; + +impl WorkflowHandler for UltraqaHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Ultraqa + } + + fn build_prompt(&self) -> String { + "# $ultraqa — QA Cycling Mode\n\n\ + Run QA cycles until all tests pass (max 5 iterations).\n\n\ + ## Cycle\n\ + 1. IMPLEMENT: Write/modify code\n\ + 2. TEST: Run tests, report results\n\ + 3. FIX: Fix failures\n\n\ + ## Completion Markers\n\ + Implementation done: `[PHASE:IMPL_DONE]`\n\ + Tests pass: `[PHASE:TESTS_PASS]`\n\ + Fix done: `[PHASE:FIX_DONE]`" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + let iteration: u32 = ctx + .metadata + .get("qa_iteration") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + if iteration >= MAX_ITERATIONS { + return WorkflowAction::Complete(format!( + "QA cycling complete after {} iterations.", + iteration + )); + } + + let phase = ctx + .metadata + .get("qa_phase") + .map(|s| s.as_str()) + .unwrap_or("implement"); + + let reminder = match phase { + "implement" => format!( + "## QA Cycle — Iteration {}/{}\n\n\ + **Phase: IMPLEMENT**\n\ + Implement:\n{}\n\n\ + Say `[PHASE:IMPL_DONE]` when done.", + iteration + 1, + MAX_ITERATIONS, + ctx.user_input + ), + "test" => "## QA Cycle — Phase: TEST\n\n\ + Run all tests. Report results.\n\ + If all pass, say `[PHASE:TESTS_PASS]`." + .to_string(), + "fix" => "## QA Cycle — Phase: FIX\n\n\ + Fix test failures. Re-run tests.\n\ + Say `[PHASE:FIX_DONE]` when done." + .to_string(), + _ => "Continue QA cycle.".to_string(), + }; + + // DON'T advance phase here + let mut metadata = ctx.metadata.clone(); + if !metadata.contains_key("qa_phase") { + metadata.insert("qa_phase".to_string(), "implement".to_string()); + } + if !metadata.contains_key("qa_iteration") { + metadata.insert("qa_iteration".to_string(), "0".to_string()); + } + + WorkflowAction::ContinueWithMetadata { reminder, metadata } + } + + fn on_turn_complete( + &self, + response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + let iteration: u32 = metadata + .get("qa_iteration") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let phase = metadata + .get("qa_phase") + .map(|s| s.as_str()) + .unwrap_or("implement"); + + let mut updated = metadata.clone(); + + let next_phase = match phase { + "implement" if response.contains("[PHASE:IMPL_DONE]") => "test", + "test" if response.contains("[PHASE:TESTS_PASS]") => { + return WorkflowAction::Complete(format!( + "All tests passing after {} iterations.", + iteration + )); + } + "test" => "fix", // Tests failed, move to fix + "fix" if response.contains("[PHASE:FIX_DONE]") => "test", + _ => return WorkflowAction::Continue, + }; + + // Increment iteration when cycling back to test from fix + if phase == "fix" && next_phase == "test" { + updated.insert("qa_iteration".to_string(), (iteration + 1).to_string()); + } + + updated.insert("qa_phase".to_string(), next_phase.to_string()); + + WorkflowAction::ContinueWithMetadata { + reminder: format!("Advancing to {} phase.", next_phase), + metadata: updated, + } + } +} diff --git a/crates/jcode-keywords/src/workflow/ultrathink.rs b/crates/jcode-keywords/src/workflow/ultrathink.rs new file mode 100644 index 000000000..877678ff5 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ultrathink.rs @@ -0,0 +1,29 @@ +//! Ultrathink — ExtendedThinking workflow handler. +//! +//! Tier 1: Prompt-only. Injects deep reasoning instructions into system prompt. + +use super::WorkflowHandler; +use crate::registry::WorkflowKind; + +pub struct UltrathinkHandler; + +impl WorkflowHandler for UltrathinkHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Ultrathink + } + + fn build_prompt(&self) -> String { + "# $ultrathink — Extended Thinking Mode\n\n\ + Reason deeply and thoroughly about the problem.\n\n\ + ## Strategy\n\ + 1. Break the problem into components\n\ + 2. Consider edge cases and boundary conditions\n\ + 3. Evaluate trade-offs between approaches\n\ + 4. Consider alternatives and implications\n\ + 5. Provide thorough analysis with reasoning chain" + .to_string() + } + + // Use trait default: Continue (no-op execute, no-op on_turn_complete) + // Defer to turn-limit expiration in state::update_modes +} diff --git a/crates/jcode-keywords/src/workflow/ultrawork.rs b/crates/jcode-keywords/src/workflow/ultrawork.rs new file mode 100644 index 000000000..027b6b0f2 --- /dev/null +++ b/crates/jcode-keywords/src/workflow/ultrawork.rs @@ -0,0 +1,75 @@ +//! Ultrawork — ParallelExecution workflow handler. +//! +//! Tier 2: Sub-agent spawning. Spawns parallel sub-agents for independent subtasks. + +use super::{SpawnSpec, WorkflowAction, WorkflowContext, WorkflowHandler, sanitize_user_input}; +use crate::registry::WorkflowKind; +use std::collections::HashMap; + +pub struct UltraworkHandler; + +impl WorkflowHandler for UltraworkHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Ultrawork + } + + fn build_prompt(&self) -> String { + "# $ultrawork — Parallel Execution Mode\n\n\ + Execute the task using parallel sub-agents.\n\n\ + ## Strategy\n\ + 1. Break task into independent subtasks\n\ + 2. Launch up to 4 parallel sub-agents\n\ + 3. Coordinate results, handle failures\n\ + 4. Aggregate into unified response" + .to_string() + } + + fn execute(&self, ctx: &WorkflowContext) -> WorkflowAction { + // Guard: don't re-spawn if already spawned this session + if ctx.metadata.contains_key("ultrawork_spawned") { + return WorkflowAction::Continue; + } + + let safe_input = sanitize_user_input(ctx.user_input); + let specs = vec![ + SpawnSpec { + description: "Analysis subtask".to_string(), + prompt: format!("Analyze the following task:\n{}", safe_input), + system_prompt: + "You are an analysis sub-agent. Identify key components and dependencies." + .to_string(), + max_turns: 5, + }, + SpawnSpec { + description: "Implementation subtask".to_string(), + prompt: format!("Implement the core functionality for:\n{}", safe_input), + system_prompt: "You are an implementation sub-agent. Write clean, working code." + .to_string(), + max_turns: 10, + }, + SpawnSpec { + description: "Testing subtask".to_string(), + prompt: format!("Write tests for:\n{}", safe_input), + system_prompt: "You are a testing sub-agent. Ensure comprehensive test coverage." + .to_string(), + max_turns: 5, + }, + ]; + + WorkflowAction::SpawnParallel(specs) + } + + fn on_turn_complete( + &self, + _response: &str, + metadata: &HashMap, + ) -> WorkflowAction { + // If we already spawned, mark as complete + if metadata.contains_key("ultrawork_spawned") { + return WorkflowAction::Complete( + "Parallel execution complete. Results aggregated.".to_string(), + ); + } + WorkflowAction::Continue + } +} diff --git a/crates/jcode-keywords/src/workflow/wiki.rs b/crates/jcode-keywords/src/workflow/wiki.rs new file mode 100644 index 000000000..14b91374d --- /dev/null +++ b/crates/jcode-keywords/src/workflow/wiki.rs @@ -0,0 +1,31 @@ +//! Wiki — DocLookup workflow handler. +//! +//! Tier 1: Prompt-only. Injects documentation search instructions. + +use super::WorkflowHandler; +use crate::registry::WorkflowKind; + +pub struct WikiHandler; + +impl WorkflowHandler for WikiHandler { + fn kind(&self) -> WorkflowKind { + WorkflowKind::Wiki + } + + fn build_prompt(&self) -> String { + "# $wiki — Documentation Lookup Mode\n\n\ + Search and synthesize documentation.\n\n\ + ## Search Strategy\n\ + 1. Local docs: README, AGENTS.md, docs/\n\ + 2. Code docs: docstrings, comments\n\ + 3. Web docs: official documentation\n\ + 4. Cross-reference multiple sources\n\n\ + ## Output\n\ + - Direct answer\n\ + - file:line references for local sources\n\ + - URLs for web sources" + .to_string() + } + + // Use trait default: Continue +} diff --git a/crates/jcode-tui/Cargo.toml b/crates/jcode-tui/Cargo.toml index 5403bac7a..43c7ddb75 100644 --- a/crates/jcode-tui/Cargo.toml +++ b/crates/jcode-tui/Cargo.toml @@ -22,6 +22,7 @@ path = "src/lib.rs" # Re-exported via `pub use jcode_app_core::*` in lib.rs. default-features=false # so this crate (and ultimately the root crate) controls app-core's features. jcode-app-core = { path = "../jcode-app-core", default-features = false } +jcode-keywords = { path = "../jcode-keywords" } # Pure math kernels for the idle animations (3D samplers, glyph chooser, # HSV->RGB). Pinned to opt-level = 3 in all profiles via workspace Cargo.toml so diff --git a/crates/jcode-tui/src/tui/app/turn_memory.rs b/crates/jcode-tui/src/tui/app/turn_memory.rs index 65459cc15..4660d3941 100644 --- a/crates/jcode-tui/src/tui/app/turn_memory.rs +++ b/crates/jcode-tui/src/tui/app/turn_memory.rs @@ -27,12 +27,62 @@ impl App { description: s.description.clone(), }) .collect(); + // Run the same keyword pipeline as the agent runtime so TUI users see + // workflow prompts and have their mode state persisted. (See issue #391.) + let keyword_prompt = { + let latest_input = self + .session + .messages + .iter() + .rev() + .find(|m| { + use crate::message::Role; + matches!(m.role, Role::User) + }) + .and_then(|m| { + m.content.iter().find_map(|b| match b { + crate::message::ContentBlock::Text { text, .. } => Some(text.as_str()), + _ => None, + }) + }) + .unwrap_or(""); + let last_assistant = self + .session + .messages + .iter() + .rev() + .find(|m| { + use crate::message::Role; + matches!(m.role, Role::Assistant) + }) + .and_then(|m| { + m.content.iter().find_map(|b| match b { + crate::message::ContentBlock::Text { text, .. } => Some(text.as_str()), + _ => None, + }) + }); + let result = jcode_keywords::process_turn( + latest_input, + last_assistant, + self.session + .working_dir + .as_deref() + .map(std::path::Path::new), + &self.session.id, + ); + for conflict in &result.conflicts { + crate::logging::warn(&jcode_keywords::conflict::format_warning(conflict)); + } + result.keyword_prompt + }; + let (mut split, context_info) = crate::prompt::build_system_prompt_split( skill_prompt.as_deref(), &available_skills, self.session.is_canary, memory_prompt, None, + keyword_prompt, ); self.append_current_turn_system_reminder(&mut split); self.context_info = context_info; diff --git a/docs/keyword-system-master-plan.md b/docs/keyword-system-master-plan.md new file mode 100644 index 000000000..2297266fd --- /dev/null +++ b/docs/keyword-system-master-plan.md @@ -0,0 +1,2209 @@ +# Magic Keyword System — Master Plan +> Issue #391 | Full keyword detection + workflow handlers +> Research: oh-my-codex, oh-my-openagent, oh-my-claudecode, claude-code, codebuff, oh-my-pi + +## Context + +jcode has `$` skill invocation but no natural language keyword detection or workflow orchestration. This plan adds: +1. **Keyword detection** — NL triggers with sanitization, intent disambiguation, multilingual +2. **Workflow handlers** — full execution logic for each keyword (spawn agents, enforce rules, aggregate results) +3. **Mode state** — persistent across turns and session restarts +4. **Visual effects** — rainbow highlighting, shimmer, toasts +5. **Task size classification** — suppress heavy modes for simple tasks +6. **Cancel system** — stopjcode/canceljcode + +**Removed:** ralph loop, autopilot pipeline, Oracle verification. + +--- + +## Architecture + +### Crate: `jcode-keywords` + +``` +crates/jcode-keywords/ +├── Cargo.toml +└── src/ + ├── lib.rs # public API + ├── registry.rs # KEYWORD_REGISTRY static + ├── detector.rs # detection engine + ├── sanitizer.rs # strip code blocks, URLs, quotes, etc. + ├── intent.rs # informational vs activation intent + ├── task_size.rs # small/medium/large classification + ├── conflict.rs # priority resolution + ├── state.rs # TOML mode state persistence + ├── prompt_builder.rs # build prompt injections per mode + ├── visual.rs # visual effect types + ├── workflow/ + │ ├── mod.rs # WorkflowHandler trait + dispatch + │ ├── ultrawork.rs # parallel agent orchestration + │ ├── ultragoal.rs # durable goal tracking + │ ├── ultraqa.rs # QA cycling + │ ├── ralplan.rs # consensus planning + │ ├── deep_interview.rs # requirements gathering + │ ├── tdd.rs # test-driven development + │ ├── code_review.rs # code review agent + │ ├── security_review.rs # security review agent + │ ├── ultrathink.rs # extended thinking + │ ├── deepsearch.rs # thorough codebase search + │ ├── analyze.rs # deep analysis + │ ├── wiki.rs # documentation lookup + │ └── ai_slop_cleaner.rs # fix AI-generated code + └── tests.rs +``` + +### Integration + +``` +User types message + → TUI input.rs: detect keywords, highlight rainbow, show toast + → Send to agent + → prompting.rs: build_system_prompt_split() + → keyword detector runs on latest user message + → for each matched keyword: + → activate mode (persist to .jcode/state/modes.toml) + → build mode-specific prompt injection + → if workflow handler needed → spawn workflow + → append to dynamic_part + → Agent processes with enhanced prompt + → Workflow handlers manage sub-agents, state, termination +``` + +--- + +## Part 1: Keyword Detection + +### 1.1 Registry (`registry.rs`) + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeywordEntry { + pub keyword: &'static str, + pub skill: &'static str, + pub priority: u8, // 5-11 + pub guidance: &'static str, // prompt injection text + pub aliases: &'static [&'static str], + pub requires_explicit: bool, // $prefix or activation verbs + pub is_heavy: bool, // suppress for small tasks + pub visual_effect: VisualEffect, // Rainbow, Shimmer, Toast, None + pub workflow_type: WorkflowType, // determines handler +} +``` + +**Complete registry:** + +| Pri | Skill | Keyword | Aliases | Heavy | Visual | Workflow | +|-----|-------|---------|---------|-------|--------|----------| +| 11 | ralplan | `$ralplan` | "ralplan", "consensus plan" | yes | Toast | ConsensusPlanning | +| 10 | ultrawork | `$ultrawork` | "ulw", "uw", "parallel", "don't stop", "must complete", "keep going" | yes | Rainbow+Shimmer | ParallelExecution | +| 10 | ultragoal | `$ultragoal` | "ultragoal" | yes | Toast | GoalTracking | +| 8 | ultraqa | `$ultraqa` | "ultraqa", "qa cycle" | yes | Toast | QACycling | +| 8 | deep-interview | `$deep-interview` | "ouroboros", "interview me", "gather requirements", "ask me" | no | Toast | RequirementsGathering | +| 7 | ultrathink | `$ultrathink` | "think hard", "think deeply", "think carefully" | no | Rainbow+Shimmer | ExtendedThinking | +| 7 | deepsearch | `$deepsearch` | "search the codebase", "find in codebase", "thorough search" | no | Toast | CodebaseSearch | +| 7 | tdd | `$tdd` | "test first", "red green", "test-driven" | no | Toast | TestDrivenDev | +| 6 | code-review | `$code-review` | "code review", "review code", "review this" | no | Toast | CodeReview | +| 6 | security-review | `$security-review` | "security review", "review security", "audit security" | no | Toast | SecurityReview | +| 6 | analyze | `$analyze` | "deep-analyze", "deepanalyze", "deep analysis" | no | Toast | DeepAnalysis | +| 5 | wiki | `$wiki` | "wiki this", "look up docs" | no | Toast | DocLookup | +| 5 | ai-slop-cleaner | — | compound: action + smell word | no | Toast | SlopCleanup | +| 9 | cancel | `canceljcode` | "stopjcode" | no | Toast | CancelAll | + +**Multilingual triggers** (from oh-my-openagent): +- Search: 64 triggers (EN: search/find/locate, KO: 검색/찾아, JA: 検索/探して, ZH: 搜索/查找, VI: tìm kiếm/tra cứu) +- Analyze: 64 triggers (EN: analyze/investigate, KO: 분석/조사, JA: 分析/調査, ZH: 分析/调查, VI: phân tích/điều tra) + +### 1.2 Sanitizer (`sanitizer.rs`) + +```rust +pub fn sanitize(input: &str) -> String { + // 1. Strip fenced code blocks ```...``` + // 2. Strip inline code `...` + // 3. Strip URLs https?://... + // 4. Strip XML tags ... + // 5. Strip HTML comments + // 6. Strip block quotes > ... + // 7. Strip quoted spans "..." and '...' + // 8. Strip system echo blocks (previously injected mode banners) +} +``` + +### 1.3 Intent Disambiguation (`intent.rs`) + +```rust +// "what is ultrawork?" → Informational (DON'T activate) +// "run ultrawork" → Activation (DO activate) +// "ultrawork keeps failing" → Diagnostic (DON'T activate) +// "use ultrawork to fix this" → Activation (DO activate) + +pub fn classify_intent(text: &str, keyword: &str) -> Intent; +pub fn is_informational_context(text: &str) -> bool; +pub fn has_activation_intent(text: &str, keyword: &str) -> bool; +pub fn is_diagnostic_context(text: &str) -> bool; +``` + +### 1.4 Task Size (`task_size.rs`) + +```rust +// Escape hatches: "quick:", "simple:", "tiny:", "minor:", "small:", "just:", "only:" +// Small signals: typo, rename, single file, one-liner, bump version +// Large signals: architecture, refactor, from scratch, migration, full-stack +// Word count: <50 = small bias, >200 = large bias + +pub fn classify(input: &str) -> TaskSize; // Small | Medium | Large +``` + +### 1.5 Conflict Resolution (`conflict.rs`) + +```rust +// When multiple keywords match: +// 1. Cancel always wins (priority 9, exclusive) +// 2. Highest priority wins per skill +// 3. Combo types (hyperplan-ultrawork) suppress standalone variants +// 4. Already-active modes are filtered out + +pub fn resolve(matches: &mut Vec); +``` + +--- + +## Part 2: Workflow Handlers + +### 2.1 WorkflowHandler Trait (`workflow/mod.rs`) + +```rust +#[async_trait] +pub trait WorkflowHandler: Send + Sync { + /// Name of this workflow + fn name(&self) -> &str; + + /// Is this workflow heavy? (suppress for small tasks) + fn is_heavy(&self) -> bool; + + /// Build prompt injection for system prompt + fn build_prompt(&self, entry: &KeywordEntry, config: &KeywordConfig) -> String; + + /// Execute the workflow (called after prompt injection) + /// Returns workflow state for tracking + async fn execute(&self, ctx: &WorkflowContext) -> Result; + + /// Check if workflow should continue or terminate + fn should_continue(&self, state: &WorkflowState) -> bool; + + /// Build continuation prompt for next turn + fn build_continuation(&self, state: &WorkflowState) -> String; + + /// Cleanup on cancel/terminate + async fn on_cancel(&self, state: &WorkflowState) -> Result<()>; +} + +pub struct WorkflowContext { + pub session_id: String, + pub user_message: String, + pub working_dir: PathBuf, + pub config: KeywordConfig, + pub state_store: ModeStateStore, + pub agent_tx: AgentEventSender, +} + +pub enum WorkflowResult { + /// Single-turn workflow (no persistence needed) + SingleTurn { response: String }, + /// Multi-turn workflow (persist state, continue next turn) + MultiTurn { state: WorkflowState }, + /// Spawned sub-agents (track their completion) + Spawned { agent_ids: Vec }, +} + +pub struct WorkflowState { + pub workflow_type: WorkflowType, + pub iteration: u32, + pub max_iterations: u32, + pub started_at: DateTime, + pub session_id: String, + pub data: serde_json::Value, // workflow-specific state +} +``` + +### 2.2 Ultrawork — Parallel Execution (`workflow/ultrawork.rs`) + +**Source:** oh-my-openagent (ultrawork mode), codebuff (4-agent pipeline) + +**Behavior:** +1. Analyze user task → decompose into N parallel subtasks +2. Spawn up to 4 sub-agents (configurable `max_concurrency`) +3. Each sub-agent works on one subtask independently +4. Coordinator monitors progress, aggregates results +5. If subtask fails → retry or redistribute + +**State:** +```rust +struct UltraworkState { + subtasks: Vec, + agent_handles: Vec, + completed: usize, + failed: usize, + max_concurrency: usize, // default 4 +} + +struct Subtask { + id: String, + description: String, + agent_id: Option, + status: SubtaskStatus, // Pending | Running | Completed | Failed + result: Option, +} +``` + +**Prompt injection:** +``` + +**ULTRAWORK MODE ENABLED!** + +Execute with maximum parallelism: +1. Decompose the task into independent subtasks +2. Spawn up to {max_concurrency} concurrent sub-agents +3. Each sub-agent works independently on its subtask +4. Aggregate results when all complete +5. If a subtask fails, retry or redistribute + +Use `subagent` tool with `run_in_background=true` for parallel execution. +Use `task_create`/`task_update` to track subtask progress. + +``` + +**Termination:** All subtasks completed OR max_iterations (10) reached. + +**Per-model variants:** +- Claude: detailed decomposition instructions +- GPT: structured task breakdown with explicit tool calls +- Gemini: visual-oriented decomposition + +### 2.3 Ultragoal — Durable Goal Tracking (`workflow/ultragoal.rs`) + +**Source:** codex (goals system), oh-my-codex (ultragoal) + +**Behavior:** +1. Create a structured goal with success criteria +2. Set token budget for the goal +3. Track progress across turns +4. Inject remaining budget and progress into each turn +5. Auto-continue until goal complete or budget exhausted + +**State:** +```rust +struct UltragoalState { + goal: String, + success_criteria: Vec, + token_budget: usize, + tokens_used: usize, + progress: Vec, + status: GoalStatus, // Active | Complete | Blocked | Exhausted +} +``` + +**Prompt injection:** +``` + +**ULTRAGOAL MODE ENABLED!** + +Active goal: {goal} +Success criteria: +{criteria_list} + +Progress: {completed}/{total} criteria met +Token budget: {remaining}/{total} remaining + +Continue working toward the goal. Update progress after each step. +Output when all criteria are met. + +``` + +**Termination:** All criteria met OR budget exhausted OR user cancels. + +### 2.4 Ultraqa — QA Cycling (`workflow/ultraqa.rs`) + +**Source:** oh-my-codex (ultraqa), codebuff (reviewer agent) + +**Behavior:** +1. Implement the requested change +2. Run tests (cargo test, or project-specific) +3. If tests fail → analyze failures → fix → repeat +4. If tests pass → run additional checks (clippy, format) +5. Max cycles: 10 + +**State:** +```rust +struct UltraqaState { + cycle: u32, + max_cycles: u32, // default 10 + test_results: Vec, + status: QaStatus, // Implementing | Testing | Fixing | Passed | Failed +} + +struct TestRun { + cycle: u32, + passed: usize, + failed: usize, + errors: Vec, +} +``` + +**Prompt injection:** +``` + +**ULTRAQA MODE ENABLED!** + +QA Cycle {cycle}/{max_cycles}: +1. Implement the requested change +2. Run tests: cargo test +3. If failures: analyze, fix, repeat +4. If pass: run cargo clippy and cargo fmt +5. Continue until all tests pass + +Previous run: {passed} passed, {failed} failed +{failure_details} + +``` + +**Termination:** All tests pass OR max_cycles reached. + +### 2.5 Ralplan — Consensus Planning (`workflow/ralplan.rs`) + +**Source:** oh-my-codex (ralplan), oh-my-openagent (hyperplan) + +**Behavior:** +1. Generate implementation plan +2. Spawn adversarial critic agent (Metis/Momus pattern) +3. Critic reviews plan, finds gaps +4. Revise plan based on feedback +5. Repeat until critic approves (consensus) +6. Present final plan to user + +**State:** +```rust +struct RalplanState { + plan: String, + revision: u32, + max_revisions: u32, // default 5 + critic_feedback: Vec, + status: PlanStatus, // Drafting | Reviewing | Revising | Approved | Rejected +} + +struct CriticReview { + revision: u32, + gaps: Vec, + approved: bool, + rationale: String, +} +``` + +**Prompt injection:** +``` + +**RALPLAN MODE ENABLED!** + +Consensus planning workflow: +1. Draft implementation plan +2. Self-review for gaps and risks +3. Revise until plan is solid +4. Present final plan before execution + +Plan revision: {revision}/{max_revisions} +Previous feedback: {feedback_summary} + +``` + +**Termination:** Critic approves OR max_revisions reached. + +### 2.6 Deep-Interview — Requirements Gathering (`workflow/deep_interview.rs`) + +**Source:** oh-my-codex (deep-interview), oh-my-openagent (ouroboros) + +**Behavior:** +1. Analyze user request for ambiguities +2. Ask clarifying questions (max 5 per round) +3. Score ambiguity level (0-100) +4. If ambiguity > threshold → ask more questions +5. If ambiguity ≤ threshold → summarize requirements → proceed +6. Max rounds: 3 + +**State:** +```rust +struct DeepInterviewState { + round: u32, + max_rounds: u32, // default 3 + questions_asked: Vec, + answers: Vec, + ambiguity_score: u32, // 0-100 + requirements: Option, + status: InterviewStatus, // Analyzing | Questioning | Summarizing | Complete +} +``` + +**Prompt injection:** +``` + +**DEEP INTERVIEW MODE ENABLED!** + +Before implementing, gather requirements: +1. Identify ambiguities in the request +2. Ask clarifying questions (max 5 per round) +3. Score ambiguity level +4. Continue until ambiguity ≤ 20 + +Round {round}/{max_rounds} +Ambiguity score: {ambiguity_score}/100 +Questions asked: {questions_count} + +``` + +**Termination:** Ambiguity ≤ 20 OR max_rounds reached. + +### 2.7 TDD — Test-Driven Development (`workflow/tdd.rs`) + +**Source:** oh-my-codex (tdd), oh-my-pi (edit benchmark) + +**Behavior:** +1. Write test first (expect fail) +2. Run test → verify it fails +3. Implement minimal code to pass +4. Run test → verify it passes +5. Refactor if needed +6. Repeat for next test case + +**State:** +```rust +struct TddState { + phase: TddPhase, // WriteTest | VerifyFail | Implement | VerifyPass | Refactor + tests_written: usize, + tests_passed: usize, + current_test: Option, + cycle: u32, +} +``` + +**Prompt injection:** +``` + +**TDD MODE ENABLED!** + +Test-Driven Development workflow: +1. Write test FIRST (before implementation) +2. Run test → verify it FAILS (red) +3. Implement minimal code to pass +4. Run test → verify it PASSES (green) +5. Refactor if needed +6. Repeat for next test + +Phase: {phase} +Tests written: {tests_written}, passed: {tests_passed} + +``` + +**Termination:** All tests pass. + +### 2.8 Code Review (`workflow/code_review.rs`) + +**Source:** codebuff (reviewer agent), oh-my-claudecode + +**Behavior:** +1. Identify changed files (git diff) +2. Spawn reviewer agent with review prompt +3. Reviewer analyzes: correctness, style, performance, security +4. Generate structured review report +5. Present findings to user + +**State:** +```rust +struct CodeReviewState { + files_changed: Vec, + findings: Vec, + status: ReviewStatus, // Scanning | Reviewing | Complete +} + +struct Finding { + file: String, + line: Option, + severity: Severity, // Critical | Warning | Info + category: Category, // Correctness | Style | Performance | Security + description: String, + suggestion: String, +} +``` + +**Prompt injection:** +``` + +**CODE REVIEW MODE ENABLED!** + +Review the recent changes: +1. Check correctness (logic errors, edge cases) +2. Check style (consistency, readability) +3. Check performance (inefficiencies, allocations) +4. Check security (input validation, secrets) + +Files to review: {file_list} + +``` + +**Termination:** Review complete. + +### 2.9 Security Review (`workflow/security_review.rs`) + +**Source:** codex (guardian), oh-my-claudecode + +**Behavior:** +1. Scan for common vulnerabilities (OWASP Top 10) +2. Check for secrets in code +3. Validate input handling +4. Check authentication/authorization patterns +5. Generate security report with severity levels + +**State:** +```rust +struct SecurityReviewState { + vulnerabilities: Vec, + secrets_found: Vec, + status: ReviewStatus, +} + +struct Vulnerability { + file: String, + line: Option, + severity: Severity, + category: String, // SQLi, XSS, CSRF, etc. + description: String, + fix_suggestion: String, +} +``` + +**Prompt injection:** +``` + +**SECURITY REVIEW MODE ENABLED!** + +Security audit: +1. Check OWASP Top 10 vulnerabilities +2. Scan for hardcoded secrets/credentials +3. Validate input sanitization +4. Check auth/authz patterns +5. Review dependency vulnerabilities + +Generate structured security report. + +``` + +**Termination:** Review complete. + +### 2.10 Ultrathink — Extended Thinking (`workflow/ultrathink.rs`) + +**Source:** claude-code (rainbow on ultrathink) + +**Behavior:** +1. Inject "think deeply" directive into system prompt +2. Enable extended thinking/reasoning mode (if provider supports) +3. No sub-agents needed — single-turn enhancement +4. Visual: rainbow highlighting on keyword in input + +**State:** None (single-turn). + +**Prompt injection:** +``` + +**ULTRATHINK MODE ENABLED!** + +Think deeply and thoroughly before acting: +- Consider all edge cases +- Analyze trade-offs +- Think step-by-step +- Consider alternative approaches +- Verify your reasoning before executing + +``` + +**Termination:** Single turn (no persistence). + +### 2.11 Deepsearch — Codebase Search (`workflow/deepsearch.rs`) + +**Source:** oh-my-codex (deepsearch), codebuff (file-picker) + +**Behavior:** +1. Analyze what the user is looking for +2. Search across codebase using multiple strategies: + - Grep for text patterns + - AST search for structural matches + - File name matching + - Symbol search via LSP +3. Build context map of relevant files +4. Present organized findings + +**State:** +```rust +struct DeepsearchState { + query: String, + strategies_used: Vec, + files_found: Vec, + status: SearchStatus, // Analyzing | Searching | Organizing | Complete +} +``` + +**Prompt injection:** +``` + +**DEEPSEARCH MODE ENABLED!** + +Thorough codebase search: +1. Use grep, glob, lsp tools to search +2. Search for: {query} +3. Check multiple strategies (text, structure, symbols) +4. Build context map of relevant files +5. Present organized findings + +``` + +**Termination:** Search complete. + +### 2.12 Analyze — Deep Analysis (`workflow/analyze.rs`) + +**Source:** oh-my-codex (analyze) + +**Behavior:** +1. Identify what to analyze +2. Gather relevant code/context +3. Perform structured analysis +4. Generate report with findings and recommendations + +**State:** Single-turn or multi-turn depending on scope. + +**Prompt injection:** +``` + +**ANALYZE MODE ENABLED!** + +Deep analysis workflow: +1. Identify the subject of analysis +2. Gather all relevant context +3. Analyze systematically (structure, patterns, issues) +4. Generate structured report with findings and recommendations + +``` + +### 2.13 Wiki — Documentation Lookup (`workflow/wiki.rs`) + +**Source:** oh-my-codex (wiki) + +**Behavior:** +1. Identify what documentation is needed +2. Search local docs (README, docs/, etc.) +3. Search web if needed (websearch, webfetch) +4. Generate summary + +**State:** Single-turn. + +**Prompt injection:** +``` + +**WIKI MODE ENABLED!** + +Documentation lookup: +1. Search local documentation first +2. Check README, docs/, comments +3. If needed, search the web +4. Provide clear, cited summary + +``` + +### 2.14 AI Slop Cleaner (`workflow/ai_slop_cleaner.rs`) + +**Source:** oh-my-claudecode (ai-slop-cleaner) + +**Behavior:** +1. Detect AI-generated low-quality patterns: + - Excessive comments explaining obvious code + - Overly defensive error handling + - Unnecessary abstractions + - Verbose variable names + - Redundant code patterns +2. Clean up detected slop +3. Improve code quality + +**State:** +```rust +struct SlopCleanerState { + files_scanned: usize, + slop_found: Vec, + fixes_applied: usize, + status: CleanupStatus, +} +``` + +**Prompt injection:** +``` + +**AI SLOP CLEANER MODE ENABLED!** + +Detect and fix AI-generated low-quality code: +1. Look for excessive/obvious comments +2. Check for over-engineering +3. Find redundant patterns +4. Simplify verbose code +5. Maintain functionality while improving clarity + +``` + +### 2.15 Cancel (`workflow/cancel.rs`) + +**Behavior:** +1. Clear all active mode states +2. Cancel running background tasks +3. Show toast notification +4. No prompt injection needed + +```rust +fn handle_cancel(state: &mut ModeStateStore, session_id: &str) { + state.clear_all(); + // Cancel background tasks + event_tx.send(AppEvent::CancelBackgroundTasks { session_id }); + event_tx.send(AppEvent::ShowToast { + message: "All modes cancelled".to_string(), + style: ToastStyle::Info, + }); +} +``` + +--- + +## Part 3: Mode State Persistence + +### File: `.jcode/state/modes.toml` + +```toml +[active_modes.ultrawork] +active = true +started_at = "2026-06-06T00:00:00Z" +session_id = "ses_abc123" +iteration = 3 +max_iterations = 10 +workflow_state = '{ "subtasks": [...], "completed": 2 }' + +[active_modes.deep-interview] +active = true +started_at = "2026-06-06T00:01:00Z" +session_id = "ses_abc123" +iteration = 1 +max_iterations = 3 +workflow_state = '{ "round": 1, "ambiguity_score": 45 }' +``` + +### State Operations + +```rust +pub struct ModeStateStore { ... } + +impl ModeStateStore { + pub fn load() -> Result; // Load from .jcode/state/modes.toml + pub fn save(&self) -> Result<()>; // Save to .jcode/state/modes.toml + pub fn activate(&mut self, skill: &str, ctx: &WorkflowContext); + pub fn deactivate(&mut self, skill: &str); + pub fn clear_all(&mut self); + pub fn is_active(&self, skill: &str) -> bool; + pub fn active_modes(&self) -> Vec; + pub fn update_workflow_state(&mut self, skill: &str, state: serde_json::Value); + pub fn get_workflow_state(&self, skill: &str) -> Option<&serde_json::Value>; + pub fn is_stale(&self, skill: &str) -> bool; // 2-hour TTL +} +``` + +--- + +## Part 4: TUI Visual Effects + +### 4.1 Rainbow Highlighting (from claude-code) + +```rust +const RAINBOW_COLORS: [Color; 7] = [ + Color::Rgb(235, 95, 87), // red + Color::Rgb(245, 139, 87), // orange + Color::Rgb(250, 195, 95), // yellow + Color::Rgb(145, 200, 130), // green + Color::Rgb(130, 170, 220), // blue + Color::Rgb(155, 130, 200), // indigo + Color::Rgb(200, 130, 180), // violet +]; + +fn highlight_keyword_rainbow(input: &str, keyword_positions: &[Range]) -> Vec { + // Each character of the keyword gets RAINBOW_COLORS[char_index % 7] +} +``` + +### 4.2 Shimmer Animation (from claude-code) + +```rust +// 20fps animation loop +struct ShimmerState { + glimmer_index: usize, + direction: Direction, // LeftToRight +} + +// Characters within ±1 of glimmer_index get brighter shimmer color +fn update_shimmer(state: &mut ShimmerState, keyword_len: usize) { + state.glimmer_index = (state.glimmer_index + 1) % keyword_len; +} +``` + +### 4.3 Toast Notifications + +```rust +pub enum ToastStyle { + Success, // green + Info, // blue + Warning, // yellow + Error, // red +} + +// On mode activation: +event_tx.send(AppEvent::ShowToast { + message: format!("{} Mode Activated", skill_name.to_uppercase()), + duration: Duration::from_secs(3), + style: ToastStyle::Success, +}); +``` + +### 4.4 Mode Indicator in Info Widget + +Show active modes in the TUI info widget: +``` +[ultrawork: cycle 3/10] [deep-interview: round 1/3] +``` + +--- + +## Part 5: Config + +### `.jcode/config.json` + +```json +{ + "keywords": { + "enabled": true, + "disabled_keywords": [], + "max_concurrency": 4, + "task_size": { + "small_word_limit": 50, + "large_word_limit": 200 + }, + "visual_effects": { + "rainbow": true, + "shimmer": true, + "toast": true + } + } +} +``` + +--- + +## Part 6: Implementation Order + +### Phase 1: Detection Engine +1. Create `jcode-keywords` crate +2. `registry.rs` — all 50+ keyword entries +3. `sanitizer.rs` — code blocks, URLs, quotes, system echoes +4. `detector.rs` — matching engine +5. `intent.rs` — informational vs activation +6. `task_size.rs` — classification +7. `conflict.rs` — priority resolution +8. Unit tests + +### Phase 2: State + Prompt Injection +9. `state.rs` — TOML persistence +10. `prompt_builder.rs` — build injections per mode +11. Integrate with `prompting.rs` (Point C) +12. Integration tests + +### Phase 3: Workflow Handlers (Core) +13. `workflow/mod.rs` — trait + dispatch +14. `workflow/ultrawork.rs` — parallel execution +15. `workflow/deep_interview.rs` — requirements gathering +16. `workflow/tdd.rs` — test-driven dev +17. `workflow/code_review.rs` — code review +18. `workflow/ultrathink.rs` — extended thinking + +### Phase 4: Workflow Handlers (Extended) +19. `workflow/ultragoal.rs` — goal tracking +20. `workflow/ultraqa.rs` — QA cycling +21. `workflow/ralplan.rs` — consensus planning +22. `workflow/security_review.rs` — security review +23. `workflow/deepsearch.rs` — codebase search +24. `workflow/analyze.rs` — deep analysis +25. `workflow/wiki.rs` — doc lookup +26. `workflow/ai_slop_cleaner.rs` — slop cleanup +27. `workflow/cancel.rs` — cancel all + +### Phase 5: TUI Effects +28. Rainbow highlighting in input +29. Shimmer animation +30. Toast notifications +31. Mode indicator in info widget + +### Phase 6: Config + Polish +32. Config integration +33. E2E tests +34. Documentation + +--- + +## Files Modified/Created + +| File | Change | +|------|--------| +| `Cargo.toml` | Add `jcode-keywords` workspace member | +| `crates/jcode-keywords/` | **NEW** — 20+ files | +| `crates/jcode-app-core/src/agent/prompting.rs` | Keyword detection + prompt injection | +| `crates/jcode-app-core/src/agent/turn_loops.rs` | Pass user message to detector | +| `crates/jcode-tui/src/tui/app/input.rs` | Rainbow + shimmer effects | +| `crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs` | Cancel command | +| `crates/jcode-tui/src/tui/info_widget.rs` | Active modes display | +| `crates/jcode-config-types/src/lib.rs` | KeywordsConfig | +| `crates/jcode-base/src/config.rs` | Load keywords config | + +## Verification + +1. `cargo check -p jcode-keywords` — compiles +2. `cargo test -p jcode-keywords` — unit tests pass +3. `cargo test -p jcode-app-core` — integration tests pass +4. Manual: "run ultrawork" → mode activates, parallel agents spawn +5. Manual: "what is ultrawork?" → no activation (informational) +6. Manual: "fix typo" → heavy modes suppressed (small task) +7. Manual: "canceljcode" → all modes cleared +8. Visual: "ultrathink" → rainbow highlighting in input +9. Workflow: ultrawork → 4 agents spawn → results aggregate +10. Workflow: deep-interview → questions asked → requirements summarized +11. Workflow: tdd → test written → fails → implements → passes + +--- + +## Part 7: Detailed Detection Logic Per Keyword + +### 7.1 Keyword Detection Regex (Exact) + +Each keyword has primary match + aliases. Detection uses word-boundary matching to prevent false positives. + +```rust +// Example: ultrawork +fn match_ultrawork(text: &str) -> bool { + // Primary: $ultrawork (explicit, always match) + if text.contains("$ultrawork") || text.contains("$ulw") || text.contains("$uw") { + return true; + } + // Word-boundary matches + let re = Regex::new(r"(?i)\b(ultrawork|ulw|uw)\b").unwrap(); + if re.is_match(text) { + // Check it's not in a code block or URL (already sanitized) + // Check it's not informational ("what is ultrawork?") + return !is_informational_context(text, "ultrawork"); + } + // Aliases with context + let alias_re = Regex::new(r"(?i)\b(don't stop|must complete|keep going|until done)\b").unwrap(); + if alias_re.is_match(text) { + // These are general phrases — need activation intent + return has_activation_intent(text, "ultrawork"); + } + false +} +``` + +### 7.2 Full Keyword Match Table + +| # | Keyword | Primary Match | Aliases (word-boundary) | Requires Explicit | Activation Verbs | +|---|---------|--------------|------------------------|-------------------|-----------------| +| 1 | ultrawork | `$ultrawork`, `$ulw`, `$uw` | `ultrawork`, `ulw`, `uw` | No | — | +| 2 | ultrawork (NL) | — | `don't stop`, `must complete`, `keep going`, `until done` | Yes | `run`, `start`, `use`, `enable` | +| 3 | ultragoal | `$ultragoal` | `ultragoal` | Yes | `run`, `start`, `use` | +| 4 | ultraqa | `$ultraqa` | `ultraqa`, `qa cycle` | No | — | +| 5 | ralplan | `$ralplan` | `ralplan`, `consensus plan` | Yes | `run`, `start`, `use` | +| 6 | deep-interview | `$deep-interview` | `ouroboros`, `deep interview` | No | — | +| 7 | deep-interview (NL) | — | `interview me`, `gather requirements`, `ask me questions` | Yes | `run`, `start`, `use` | +| 8 | ultrathink | `$ultrathink` | `ultrathink`, `ultra think` | No | — | +| 9 | ultrathink (NL) | — | `think hard`, `think deeply`, `think carefully`, `think step by step` | Yes | `run`, `start`, `use`, `please` | +| 10 | deepsearch | `$deepsearch` | `deepsearch`, `deep search` | No | — | +| 11 | deepsearch (NL) | — | `search the codebase`, `find in codebase`, `thorough search` | Yes | `run`, `start`, `use` | +| 12 | tdd | `$tdd` | `tdd`, `test driven`, `test-driven` | No | — | +| 13 | tdd (NL) | — | `test first`, `red green`, `write test first` | Yes | `run`, `start`, `use` | +| 14 | code-review | `$code-review` | `code review`, `review code` | No | — | +| 15 | code-review (NL) | — | `review this`, `review my changes`, `check my code` | Yes | `run`, `start`, `use` | +| 16 | security-review | `$security-review` | `security review`, `review security` | No | — | +| 17 | security-review (NL) | — | `audit security`, `check vulnerabilities`, `security audit` | Yes | `run`, `start`, `use` | +| 18 | analyze | `$analyze` | `analyze`, `analyse` | No | — | +| 19 | analyze (NL) | — | `deep analyze`, `deep analysis`, `deepanalyze` | Yes | `run`, `start`, `use` | +| 20 | wiki | `$wiki` | `wiki`, `wiki this` | No | — | +| 21 | wiki (NL) | — | `look up docs`, `find documentation`, `check docs` | Yes | `run`, `start`, `use` | +| 22 | cancel | `canceljcode` | `stopjcode` | No | — | +| 23 | ai-slop-cleaner | — | compound only | No | — | + +### 7.3 Multilingual Detection Patterns + +```rust +// Search triggers (64 total) +const SEARCH_PATTERN_EN: &[&str] = &[ + "search", "find", "locate", "lookup", "look up", "explore", + "discover", "scan", "grep", "query", "browse", "detect", + "trace", "seek", "track", "pinpoint", "hunt", +]; +const SEARCH_PATTERN_KO: &[&str] = &[ + "검색", "찾아", "탐색", "조회", "스캔", "서치", "뒤져", + "찾기", "어디", "추적", "탐지", "찾아봐", "찾아내", "보여줘", "목록", +]; +const SEARCH_PATTERN_JA: &[&str] = &[ + "検索", "探して", "見つけて", "サーチ", "探索", "スキャン", + "どこ", "発見", "捜索", "見つけ出す", "一覧", +]; +const SEARCH_PATTERN_ZH: &[&str] = &[ + "搜索", "查找", "寻找", "查询", "检索", "定位", "扫描", + "发现", "在哪里", "找出来", "列出", +]; +const SEARCH_PATTERN_VI: &[&str] = &[ + "tìm kiếm", "tra cứu", "định vị", "quét", "phát hiện", + "truy tìm", "tìm ra", "ở đâu", "liệt kê", +]; + +// Analyze triggers (64 total) +const ANALYZE_PATTERN_EN: &[&str] = &[ + "analyze", "analyse", "investigate", "examine", "research", + "study", "deep dive", "inspect", "audit", "evaluate", "assess", + "review", "diagnose", "scrutinize", "dissect", "debug", + "comprehend", "interpret", "breakdown", "understand", +]; +const ANALYZE_PATTERN_KO: &[&str] = &[ + "분석", "조사", "파악", "연구", "검토", "진단", "이해", + "설명", "원인", "이유", "뜯어봐", "따져봐", "평가", "해석", + "디버깅", "디버그", "어떻게", "왜", "살펴", +]; +const ANALYZE_PATTERN_JA: &[&str] = &[ + "分析", "調査", "解析", "検討", "研究", "診断", "理解", + "説明", "検証", "精査", "究明", "デバッグ", "なぜ", "どう", "仕組み", +]; +const ANALYZE_PATTERN_ZH: &[&str] = &[ + "分析", "调查", "检查", "剖析", "深入", "诊断", "解释", + "调试", "为什么", "原理", "搞清楚", "弄明白", +]; +const ANALYZE_PATTERN_VI: &[&str] = &[ + "phân tích", "điều tra", "nghiên cứu", "kiểm tra", "xem xét", + "chẩn đoán", "giải thích", "tìm hiểu", "gỡ lỗi", "tại sao", +]; +``` + +### 7.4 Compound Keyword Detection (ai-slop-cleaner) + +```rust +// AI Slop Cleaner requires BOTH an action word AND a smell word +const SLOP_ACTION_WORDS: &[&str] = &[ + "clean", "fix", "refactor", "improve", "remove", "simplify", + "cleanup", "clean up", "tidy", "polish", +]; +const SLOP_SMELL_WORDS: &[&str] = &[ + "slop", "ai slop", "ai-generated", "low quality", "verbose", + "redundant", "over-engineered", "obvious comments", + "unnecessary", "boilerplate", "copy-paste", +]; + +fn match_ai_slop_cleaner(text: &str) -> bool { + let has_action = SLOP_ACTION_WORDS.iter().any(|w| text.to_lowercase().contains(w)); + let has_smell = SLOP_SMELL_WORDS.iter().any(|w| text.to_lowercase().contains(w)); + has_action && has_smell +} +``` + +### 7.5 False Positive Prevention + +```rust +// System echo stripping — prevent self-reinforcing loops +fn strip_system_echoes(text: &str) -> String { + // Remove previously injected mode banners + // Pattern: [SYSTEM DIRECTIVE: JCODE - MODE_NAME ...] + let re = Regex::new(r"\[SYSTEM DIRECTIVE: JCODE[^\]]*\]").unwrap(); + re.replace_all(text, "").to_string() +} + +// Quoted span exclusion +fn is_within_quoted_span(text: &str, pos: usize) -> bool { + // Check if position is inside "..." or '...' or `...` + let before = &text[..pos]; + let double_quotes = before.matches('"').count(); + let single_quotes = before.matches('\'').count(); + let backticks = before.matches('`').count(); + double_quotes % 2 == 1 || single_quotes % 2 == 1 || backticks % 2 == 1 +} + +// Informational context detection +fn is_informational_context(text: &str, keyword: &str) -> bool { + let lower = text.to_lowercase(); + // Question patterns + let question_patterns = [ + format!("what is {}", keyword), + format!("what's {}", keyword), + format!("how does {} work", keyword), + format!("explain {}", keyword), + format!("tell me about {}", keyword), + format!("what does {} do", keyword), + format!("{} là gì", keyword), // Vietnamese + format!("{}이 뭐야", keyword), // Korean + format!("{}とは", keyword), // Japanese + format!("{}是什么", keyword), // Chinese + ]; + question_patterns.iter().any(|p| lower.contains(p)) +} + +// Diagnostic context detection +fn is_diagnostic_context(text: &str, keyword: &str) -> bool { + let lower = text.to_lowercase(); + let patterns = [ + format!("{} keeps", keyword), + format!("{} is broken", keyword), + format!("{} failed", keyword), + format!("{} not working", keyword), + format!("{} keeps looping", keyword), + format!("{} error", keyword), + ]; + patterns.iter().any(|p| lower.contains(p)) +} + +// Review seed context — prevent re-triggering from echoed review text +fn is_review_seed_context(text: &str) -> bool { + let patterns = [ + "CRITICAL:", "WARNING:", "INFO:", + "## Finding", "### Severity", + "Review complete", "Security audit", + ]; + patterns.iter().any(|p| text.contains(p)) +} +``` + +--- + +## Part 8: Detailed Prompt Injections Per Keyword + +### 8.1 Ultrawork — Per-Model Prompts + +**Claude (default):** +``` + +**ULTRAWORK MODE ENABLED!** + +You are in ULTRAWORK mode. Execute with maximum parallelism and efficiency. + +## Rules: +1. Decompose the task into independent subtasks +2. Spawn up to {max_concurrency} concurrent sub-agents using `subagent` tool with `run_in_background=true` +3. Each sub-agent works independently on its assigned subtask +4. Use `task_create`/`task_update` to track progress +5. Monitor sub-agents and aggregate results when complete +6. If a subtask fails, retry or redistribute the work +7. Do NOT stop until all subtasks are complete + +## Current State: +- Iteration: {iteration}/{max_iterations} +- Subtasks completed: {completed}/{total} +- Active agents: {active_count} + +Output when ALL subtasks are finished. + +``` + +**GPT variant:** +``` + +ULTRAWORK MODE ACTIVE. + +Instructions: +- Break this task into parallelizable subtasks +- For each subtask, call subagent tool with run_in_background=true +- Maximum concurrent agents: {max_concurrency} +- Track each subtask with task_create tool +- When all agents finish, collect and merge results +- Do not stop prematurely + +Status: {iteration}/{max_iterations} iterations, {completed}/{total} done + +``` + +**Gemini variant:** +``` + +ULTRAWORK MODE. Maximum parallelism. + +1. Split task into {max_concurrency} parallel pieces +2. Spawn sub-agents (background=true) +3. Collect results +4. Merge and verify + +Progress: {completed}/{total} + +``` + +### 8.2 Deep-Interview — Per-Model Prompts + +**Claude:** +``` + +**DEEP INTERVIEW MODE ENABLED!** + +Before implementing ANYTHING, you MUST gather requirements through structured questioning. + +## Rules: +1. Analyze the user's request for ambiguities and missing details +2. Ask clarifying questions (max 5 per round) +3. Categorize questions: scope, constraints, design preferences, edge cases +4. After each round, re-evaluate ambiguity score (0-100) +5. Continue until ambiguity score ≤ 20 OR max {max_rounds} rounds reached +6. Summarize confirmed requirements before proceeding + +## Question Categories: +- **Scope**: What exactly is included/excluded? +- **Constraints**: Performance, security, compatibility requirements? +- **Design**: Architecture preferences, patterns to follow? +- **Edge Cases**: What should happen in error scenarios? +- **Testing**: How will we verify this works? + +## Current State: +- Round: {round}/{max_rounds} +- Ambiguity score: {ambiguity_score}/100 +- Questions asked: {questions_count} +- Confirmed requirements: {confirmed_count} + +Output when ambiguity ≤ 20 and requirements are confirmed. + +``` + +### 8.3 TDD — Per-Model Prompts + +**Claude:** +``` + +**TDD MODE ENABLED!** + +You MUST follow Test-Driven Development strictly. + +## Rules: +1. Write the test FIRST — before any implementation code +2. Run the test — it MUST fail (red) +3. Write the MINIMUM implementation to make the test pass +4. Run the test — it MUST pass (green) +5. Refactor if needed (keep tests green) +6. Repeat for next test case + +## Current State: +- Phase: {phase} (WriteTest | VerifyFail | Implement | VerifyPass | Refactor) +- Tests written: {tests_written} +- Tests passed: {tests_passed} +- Current test: {current_test} + +## Commands: +- Run tests: `cargo test` (or project-specific test command) +- Run single test: `cargo test {test_name}` + +Output when all planned tests pass. + +``` + +### 8.4 Ralplan — Per-Model Prompts + +**Claude:** +``` + +**RALPLAN MODE ENABLED!** + +You MUST create a consensus-approved plan before implementing ANYTHING. + +## Rules: +1. Draft a detailed implementation plan +2. Self-review the plan for gaps, risks, and missing steps +3. Identify potential failure modes and mitigation strategies +4. Revise the plan until it is comprehensive and solid +5. Present the final plan for user approval +6. ONLY implement after the plan is approved + +## Plan Structure: +- **Goal**: What we're building and why +- **Steps**: Ordered list of implementation steps +- **Dependencies**: What each step depends on +- **Risks**: Potential issues and mitigations +- **Verification**: How we'll verify each step works + +## Current State: +- Revision: {revision}/{max_revisions} +- Previous feedback: {feedback_summary} +- Status: {status} (Drafting | Reviewing | Revising | Approved) + +## Adversarial Self-Review: +After drafting, ask yourself: +- What could go wrong? +- What am I missing? +- Are there simpler approaches? +- What are the edge cases? + +Output when the plan is solid and ready for implementation. + +``` + +### 8.5 Ultragoal — Per-Model Prompts + +**Claude:** +``` + +**ULTRAGOAL MODE ENABLED!** + +You are working toward a durable goal with token budget enforcement. + +## Goal: +{goal_description} + +## Success Criteria: +{criteria_list} + +## Rules: +1. Work systematically toward each success criterion +2. Track progress after each significant step +3. Do NOT exceed the token budget +4. If blocked, document the blocker and try alternative approaches +5. Update criteria status as you progress + +## Current State: +- Progress: {completed}/{total} criteria met +- Token budget: {remaining}/{total} remaining +- Status: {status} (Active | Complete | Blocked) + +{progress_details} + +Output when ALL success criteria are met. +Output if unable to proceed. + +``` + +### 8.6 Ultraqa — Per-Model Prompts + +**Claude:** +``` + +**ULTRAQA MODE ENABLED!** + +Continuous QA cycling until all tests pass. + +## Rules: +1. Implement the requested change +2. Run the full test suite +3. If ANY test fails: + a. Analyze the failure + b. Fix the issue + c. Re-run tests +4. If all tests pass: + a. Run additional checks (clippy, format, lint) + b. Fix any warnings +5. Repeat until clean + +## Current State: +- Cycle: {cycle}/{max_cycles} +- Last run: {passed} passed, {failed} failed +- Status: {status} (Implementing | Testing | Fixing | Passed) + +{failure_details} + +## Commands: +- Run tests: `cargo test` +- Run clippy: `cargo clippy` +- Run format: `cargo fmt` + +Output when ALL tests pass AND checks are clean. +Output if max cycles reached without passing. + +``` + +### 8.7 Security Review — Per-Model Prompts + +**Claude:** +``` + +**SECURITY REVIEW MODE ENABLED!** + +Perform a thorough security audit of the code changes. + +## Checklist: +1. **OWASP Top 10**: + - [ ] Injection (SQL, Command, LDAP) + - [ ] Broken Authentication + - [ ] Sensitive Data Exposure + - [ ] XML External Entities (XXE) + - [ ] Broken Access Control + - [ ] Security Misconfiguration + - [ ] Cross-Site Scripting (XSS) + - [ ] Insecure Deserialization + - [ ] Using Components with Known Vulnerabilities + - [ ] Insufficient Logging & Monitoring + +2. **Secrets Detection**: + - [ ] Hardcoded passwords/API keys + - [ ] Credentials in config files + - [ ] Secrets in logs/error messages + +3. **Input Validation**: + - [ ] All user inputs sanitized + - [ ] Buffer overflow protection + - [ ] Path traversal prevention + +4. **Authentication/Authorization**: + - [ ] Proper session management + - [ ] Token validation + - [ ] Permission checks + +## Output Format: +For each finding: +- **Severity**: Critical | High | Medium | Low | Info +- **Category**: (from checklist above) +- **File:Line**: Location +- **Description**: What's wrong +- **Fix**: How to fix it + +Output when audit is finished. + +``` + +### 8.8 Code Review — Per-Model Prompts + +**Claude:** +``` + +**CODE REVIEW MODE ENABLED!** + +Review the recent code changes thoroughly. + +## Review Dimensions: +1. **Correctness**: Logic errors, edge cases, off-by-one +2. **Readability**: Clear naming, appropriate comments, structure +3. **Performance**: Unnecessary allocations, O(n²) patterns, missing caching +4. **Maintainability**: DRY, separation of concerns, testability +5. **Error Handling**: Proper error propagation, no swallowed errors +6. **Safety**: Unsafe blocks justified, no UB, proper lifetimes + +## Output Format: +For each finding: +- **Severity**: Critical | Warning | Nit +- **Category**: (from dimensions above) +- **File:Line**: Location +- **Comment**: What's wrong and how to fix it + +## Rules: +- Be specific — cite exact lines +- Be constructive — suggest fixes, not just problems +- Be thorough — check every changed file +- Prioritize critical issues over nits + +Output when all files are reviewed. + +``` + +### 8.9 Ultrathink — Per-Model Prompts + +**Claude:** +``` + +**ULTRATHINK MODE ENABLED!** + +Think deeply and thoroughly before acting. + +## Rules: +1. Think step-by-step through the problem +2. Consider ALL edge cases before implementing +3. Evaluate multiple approaches and their trade-offs +4. Verify your reasoning at each step +5. Consider security, performance, and maintainability implications +6. Do NOT rush — take the time to think it through properly + +## Thinking Framework: +1. **Understand**: What exactly is being asked? +2. **Explore**: What are the possible approaches? +3. **Evaluate**: Which approach is best and why? +4. **Plan**: What are the exact steps? +5. **Verify**: Does this plan handle all cases? + +Think silently, then act with confidence. + +``` + +### 8.10 Deepsearch — Per-Model Prompts + +**Claude:** +``` + +**DEEPSEARCH MODE ENABLED!** + +Perform a thorough codebase search before answering. + +## Search Strategy: +1. **Text search**: Use `grep` for exact string matches +2. **Pattern search**: Use `glob` for file patterns +3. **Symbol search**: Use `lsp` for definitions and references +4. **Structure search**: Use `ls` and `read` to understand directory layout + +## Rules: +1. Search from multiple angles — don't rely on a single grep +2. Follow references — if you find something, trace its usage +3. Check related files — imports, tests, docs +4. Build a mental map of where things live +5. Report findings with exact file:line references + +## Search Target: +{search_query} + +Output when thorough search is done. +Report: files found, key locations, relevant code snippets. + +``` + +### 8.11 Analyze — Per-Model Prompts + +**Claude:** +``` + +**ANALYZE MODE ENABLED!** + +Perform deep, structured analysis. + +## Analysis Framework: +1. **Gather**: Collect all relevant information +2. **Structure**: Organize findings into categories +3. **Analyze**: Identify patterns, issues, and insights +4. **Synthesize**: Draw conclusions from the analysis +5. **Recommend**: Suggest actionable next steps + +## Output Structure: +- **Summary**: One-paragraph overview +- **Findings**: Detailed list with evidence +- **Patterns**: Recurring themes or issues +- **Recommendations**: Prioritized action items +- **Appendix**: Supporting data/evidence + +## Analysis Subject: +{analysis_subject} + +Be thorough, evidence-based, and actionable. + +``` + +### 8.12 Wiki — Per-Model Prompts + +**Claude:** +``` + +**WIKI MODE ENABLED!** + +Look up documentation and provide clear answers. + +## Search Order: +1. Local docs (README, docs/, CHANGELOG, inline comments) +2. Source code (actual implementation is truth) +3. Web search (if local info insufficient) + +## Rules: +1. Always cite sources (file paths, URLs) +2. Distinguish between documented behavior and observed behavior +3. Note version-specific information +4. If docs are outdated, flag it + +## Query: +{wiki_query} + +Provide a clear, cited answer. + +``` + +### 8.13 AI Slop Cleaner — Per-Model Prompts + +**Claude:** +``` + +**AI SLOP CLEANER MODE ENABLED!** + +Detect and fix AI-generated low-quality code patterns. + +## Slop Patterns to Detect: +1. **Obvious comments**: `// increment counter` above `counter += 1` +2. **Over-engineering**: Unnecessary abstractions for simple problems +3. **Verbose names**: `calculateTheTotalSumOfAllItems()` → `total()` +4. **Redundant checks**: `if x != null && x != undefined` (in Rust: `if let Some(x)`) +5. **Copy-paste patterns**: Same logic repeated in multiple places +6. **Excessive error handling**: `.unwrap()` replaced with 10-line match blocks for infallible ops +7. **Boilerplate**: Auto-generated-looking code with no real logic +8. **Defensive programming**: Checking things that can't fail + +## Rules: +1. Maintain functionality — don't break working code +2. Improve clarity — shorter, clearer, more idiomatic +3. Use Rust idioms — `if let`, `?`, iterators, `impl` +4. Remove noise — comments that explain the obvious +5. Simplify — fewer lines, same behavior + +## Files to Clean: +{file_list} + +Output when all files are cleaned. +Report: patterns found, fixes applied, lines saved. + +``` + +### 8.14 Cancel — No Prompt Injection + +Cancel does NOT inject a prompt. It: +1. Clears all active mode states in `.jcode/state/modes.toml` +2. Cancels running background tasks for the session +3. Shows toast: "All modes cancelled" +4. Returns to normal mode + +--- + +## Part 9: Workflow State Machines + +### 9.1 Ultrawork State Machine + +``` + ┌─────────────┐ + │ Initial │ + └──────┬──────┘ + │ detect keyword + ▼ + ┌─────────────┐ + │ Analyzing │ ← decompose task into subtasks + └──────┬──────┘ + │ subtasks ready + ▼ + ┌─────────────┐ + ┌────→│ Spawning │ ← spawn sub-agents (up to max_concurrency) + │ └──────┬──────┘ + │ │ agents spawned + │ ▼ + │ ┌─────────────┐ + │ │ Running │ ← agents working in parallel + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │Completed│ │ Failed │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ │ retry? + │ │ ├──────→ Spawning (retry) + │ │ │ + │ │ └──────→ Failed (max retries) + │ │ + │ ▼ + │ ┌─────────────┐ + │ │ All Done? │ + │ └──────┬──────┘ + │ │ yes + │ ▼ + │ ┌─────────────┐ + └─│ Aggregating │ ← merge results + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ Complete │ + └─────────────┘ +``` + +**Transitions:** +- `Initial → Analyzing`: keyword detected, task decomposed +- `Analyzing → Spawning`: subtasks identified +- `Spawning → Running`: agents spawned +- `Running → Completed`: agent finishes successfully +- `Running → Failed`: agent fails +- `Failed → Spawning`: retry (if under max retries) +- `Completed → Aggregating`: all agents done +- `Aggregating → Complete`: results merged + +**State data:** +```rust +struct UltraworkState { + subtasks: Vec, + active_agents: Vec, + completed_count: usize, + failed_count: usize, + retry_count: usize, + max_retries: usize, // default 3 + max_concurrency: usize, // default 4 + iteration: usize, + max_iterations: usize, // default 10 +} +``` + +### 9.2 Deep-Interview State Machine + +``` + ┌─────────────┐ + │ Initial │ + └──────┬──────┘ + │ detect keyword + ▼ + ┌─────────────┐ + │ Analyzing │ ← identify ambiguities + └──────┬──────┘ + │ ambiguities found + ▼ + ┌─────────────┐ + ┌────→│ Questioning │ ← ask clarifying questions + │ └──────┬──────┘ + │ │ answers received + │ ▼ + │ ┌─────────────┐ + │ │ Scoring │ ← calculate ambiguity score + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ ≤ 20 │ │ > 20 │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ │ max rounds? + │ │ ├──────→ Questioning (next round) + │ │ │ + │ │ └──────→ Proceeding (force continue) + │ │ + │ ▼ + │ ┌─────────────┐ + │ │ Summarizing │ ← summarize requirements + │ └──────┬──────┘ + │ │ + │ ▼ + │ ┌─────────────┐ + └─│ Complete │ + └─────────────┘ +``` + +**State data:** +```rust +struct DeepInterviewState { + round: usize, + max_rounds: usize, // default 3 + questions: Vec, + answers: Vec, + ambiguity_score: usize, // 0-100 + threshold: usize, // default 20 + requirements: Option, +} +``` + +### 9.3 TDD State Machine + +``` + ┌─────────────┐ + │ Initial │ + └──────┬──────┘ + │ detect keyword + ▼ + ┌─────────────┐ + ┌────→│ WriteTest │ ← write test case + │ └──────┬──────┘ + │ │ test written + │ ▼ + │ ┌─────────────┐ + │ │ VerifyFail │ ← run test, expect FAIL + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ Failed │ │ Passed │ ← unexpected! + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ └──→ Fix test (should fail) + │ ▼ + │ ┌─────────────┐ + │ │ Implement │ ← write minimal code + │ └──────┬──────┘ + │ │ code written + │ ▼ + │ ┌─────────────┐ + │ │ VerifyPass │ ← run test, expect PASS + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ Passed │ │ Failed │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ └──→ Implement (fix) + │ ▼ + │ ┌─────────────┐ + │ │ Refactor? │ + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ Yes │ │ No │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ ▼ │ + │ ┌──────────┐ │ + │ │ Refactor │ │ + │ └────┬─────┘ │ + │ │ │ + └──────┘ ▼ + ┌─────────────┐ + │ Complete │ + └─────────────┘ +``` + +**State data:** +```rust +struct TddState { + phase: TddPhase, + tests: Vec, + current_test: Option, + cycle: usize, + max_cycles: usize, // default 20 +} + +enum TddPhase { + WriteTest, + VerifyFail, + Implement, + VerifyPass, + Refactor, + Complete, +} +``` + +### 9.4 Ralplan State Machine + +``` + ┌─────────────┐ + │ Initial │ + └──────┬──────┘ + │ detect keyword + ▼ + ┌─────────────┐ + ┌────→│ Drafting │ ← write plan + │ └──────┬──────┘ + │ │ draft ready + │ ▼ + │ ┌─────────────┐ + │ │ Reviewing │ ← self-review for gaps + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ Solid │ │ Gaps │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ ▼ + │ │ ┌─────────────┐ + │ │ │ Revising │ ← fix gaps + │ │ └──────┬──────┘ + │ │ │ + │ └──────────┘ (loop back to Reviewing) + │ + ▼ + ┌─────────────┐ + │ Presenting │ ← show plan to user + └──────┬──────┘ + │ + ┌─────┴─────┐ + │ │ + ▼ ▼ + ┌────────┐ ┌─────────┐ + │Approve │ │ Reject │ + └────┬───┘ └────┬────┘ + │ │ + ▼ ▼ + ┌─────────┐ ┌──────────┐ + │Complete │ │ Revise │──→ Drafting + └─────────┘ └──────────┘ +``` + +**State data:** +```rust +struct RalplanState { + plan: String, + revision: usize, + max_revisions: usize, // default 5 + reviews: Vec, + gaps: Vec, + status: RalplanStatus, +} + +enum RalplanStatus { + Drafting, + Reviewing, + Revising, + Presenting, + Approved, + Rejected, +} +``` + +### 9.5 Ultraqa State Machine + +``` + ┌─────────────┐ + │ Initial │ + └──────┬──────┘ + │ detect keyword + ▼ + ┌─────────────┐ + ┌────→│ Implementing│ ← write/change code + │ └──────┬──────┘ + │ │ code ready + │ ▼ + │ ┌─────────────┐ + │ │ Testing │ ← run cargo test + │ └──────┬──────┘ + │ │ + │ ┌─────┴─────┐ + │ │ │ + │ ▼ ▼ + │ ┌────────┐ ┌─────────┐ + │ │ Pass │ │ Fail │ + │ └────┬───┘ └────┬────┘ + │ │ │ + │ │ ▼ + │ │ ┌─────────────┐ + │ │ │ Fixing │ ← analyze + fix failures + │ │ └──────┬──────┘ + │ │ │ + │ └──────────┘ (loop back to Testing) + │ + ▼ + ┌─────────────┐ + │ Checks │ ← clippy, format, lint + └──────┬──────┘ + │ + ┌─────┴─────┐ + │ │ + ▼ ▼ + ┌────────┐ ┌─────────┐ + │ Clean │ │ Warning │ + └────┬───┘ └────┬────┘ + │ │ + │ ▼ + │ ┌──────────┐ + │ │ Fix │──→ Checks + │ └──────────┘ + ▼ + ┌─────────────┐ + │ Complete │ + └─────────────┘ +``` + +--- + +## Part 10: Integration with Existing jcode Tools + +### 10.1 Tool Usage Per Workflow + +| Workflow | Tools Used | How | +|----------|-----------|-----| +| **ultrawork** | `subagent`, `task_create`, `task_update`, `task_list` | Spawn parallel agents, track subtasks | +| **ultragoal** | `initiative`, `todo` | Create goal, track progress | +| **ultraqa** | `bash` (cargo test), `edit`, `read` | Run tests, fix code | +| **ralplan** | `read`, `write`, `glob`, `grep` | Analyze codebase, draft plan | +| **deep-interview** | (prompt-only) | Ask questions, no tool calls | +| **tdd** | `write`, `bash` (cargo test), `edit` | Write test, run, implement | +| **code-review** | `subagent`, `read`, `diff`, `grep` | Spawn reviewer, read changes | +| **security-review** | `subagent`, `read`, `grep`, `glob` | Spawn security agent, scan code | +| **ultrathink** | (prompt-only) | Think deeply, no tool calls | +| **deepsearch** | `grep`, `glob`, `lsp`, `read`, `agentgrep` | Multi-strategy search | +| **analyze** | `read`, `grep`, `glob`, `lsp` | Gather context, analyze | +| **wiki** | `read`, `websearch`, `webfetch` | Local + web docs | +| **ai-slop-cleaner** | `read`, `edit`, `grep` | Find + fix patterns | +| **cancel** | (state-only) | Clear state, cancel tasks | + +### 10.2 Sub-Agent Prompt Templates + +**Ultrawork sub-agent prompt:** +``` +You are a sub-agent in ULTRAWORK mode. Your task: +{subtask_description} + +Rules: +- Work independently on this subtask ONLY +- Do NOT spawn sub-agents yourself +- Report completion with +- If stuck, report +- Use available tools: read, write, edit, bash, grep, glob +``` + +**Code-review sub-agent prompt:** +``` +You are a code reviewer. Review these changes: +{diff_content} + +Check for: correctness, readability, performance, error handling. +Format: severity | file:line | comment +``` + +**Security-review sub-agent prompt:** +``` +You are a security auditor. Audit this code: +{file_list} + +Check OWASP Top 10, secrets, input validation, auth patterns. +Format: severity | category | file:line | description | fix +``` + +--- + +## Part 11: Error Handling Per Workflow + +| Workflow | Error Scenario | Handling | +|----------|---------------|----------| +| **ultrawork** | Sub-agent crashes | Retry up to 3 times, then mark failed | +| **ultrawork** | All agents fail | Report failure, suggest simplification | +| **ultragoal** | Budget exhausted | Stop, report progress, ask user | +| **ultraqa** | Max cycles reached | Report remaining failures, ask user | +| **ralplan** | Max revisions reached | Present best plan, ask user | +| **deep-interview** | Max rounds reached | Proceed with current info, note gaps | +| **tdd** | Can't make test pass | Report stuck point, ask user | +| **code-review** | Sub-agent timeout | Skip file, note in report | +| **security-review** | Sub-agent timeout | Skip file, note in report | +| **deepsearch** | No results found | Expand search, try alternatives | +| **analyze** | Insufficient context | Ask user for more info | +| **wiki** | No docs found | Suggest alternatives | +| **ai-slop-cleaner** | Can't simplify without breaking | Skip pattern, note in report | + +--- + +## Part 12: Edge Cases + +### 12.1 Multiple Keywords in One Message + +``` +User: "run ultrawork and tdd for this feature" +``` +**Resolution:** Both activate. Ultrawork (priority 10) > TDD (priority 7). Ultrawork orchestrates, TDD rules apply to each sub-agent. + +### 12.2 Keyword in Code Block + +``` +User: "Here's an example: ```ultrawork mode```" +``` +**Resolution:** Sanitizer strips code blocks before matching. No activation. + +### 12.3 Keyword in URL + +``` +User: "Check https://example.com/ultrawork-docs" +``` +**Resolution:** Sanitizer strips URLs before matching. No activation. + +### 12.4 Informational Query + +``` +User: "What is ultrawork mode?" +``` +**Resolution:** Intent classifier detects question pattern. No activation. + +### 12.5 Diagnostic Query + +``` +User: "ultrawork keeps failing, what's wrong?" +``` +**Resolution:** Intent classifier detects diagnostic pattern. No activation. + +### 12.6 Cancel During Active Workflow + +``` +User: (ultrawork running) → "canceljcode" +``` +**Resolution:** Cancel clears all state, cancels background tasks, shows toast. + +### 12.7 Small Task with Heavy Keyword + +``` +User: "quick: fix typo with ultrawork" +``` +**Resolution:** Escape hatch "quick:" forces small task. Ultrawork suppressed (heavy). Agent fixes typo normally. + +### 12.8 Stale Mode State + +``` +Mode active for > 2 hours without activity +``` +**Resolution:** Treat as inactive. Don't inject prompt. Log warning. + +### 12.9 Session Restart with Active Modes + +``` +Mode persisted in .jcode/state/modes.toml, session restarted +``` +**Resolution:** Load state on startup. If stale (>2h), deactivate. If fresh, continue. + +### 12.10 Conflicting Modes + +``` +User: "run ultrawork" then "run deep-interview" +``` +**Resolution:** Both can coexist. Deep-interview asks questions first, then ultrawork executes. + +--- + +## Part 13: Full File List + +### New Files (crates/jcode-keywords/) + +| File | Lines (est.) | Purpose | +|------|-------------|---------| +| `Cargo.toml` | 30 | Crate config | +| `src/lib.rs` | 100 | Public API, re-exports | +| `src/registry.rs` | 400 | 50+ keyword entries with full data | +| `src/detector.rs` | 300 | Main detection engine | +| `src/sanitizer.rs` | 250 | 8-stage sanitization pipeline | +| `src/intent.rs` | 200 | Informational/activation/diagnostic | +| `src/task_size.rs` | 150 | Small/medium/large classification | +| `src/conflict.rs` | 100 | Priority resolution | +| `src/state.rs` | 300 | TOML persistence, state ops | +| `src/prompt_builder.rs` | 500 | Per-mode prompt injection (14 modes × per-model) | +| `src/visual.rs` | 50 | Visual effect types | +| `src/workflow/mod.rs` | 150 | WorkflowHandler trait + dispatch | +| `src/workflow/ultrawork.rs` | 400 | Parallel execution workflow | +| `src/workflow/ultragoal.rs` | 300 | Goal tracking workflow | +| `src/workflow/ultraqa.rs` | 300 | QA cycling workflow | +| `src/workflow/ralplan.rs` | 350 | Consensus planning workflow | +| `src/workflow/deep_interview.rs` | 300 | Requirements gathering workflow | +| `src/workflow/tdd.rs` | 300 | Test-driven dev workflow | +| `src/workflow/code_review.rs` | 250 | Code review workflow | +| `src/workflow/security_review.rs` | 250 | Security review workflow | +| `src/workflow/ultrathink.rs` | 100 | Extended thinking workflow | +| `src/workflow/deepsearch.rs` | 200 | Codebase search workflow | +| `src/workflow/analyze.rs` | 150 | Deep analysis workflow | +| `src/workflow/wiki.rs` | 100 | Doc lookup workflow | +| `src/workflow/ai_slop_cleaner.rs` | 200 | Slop cleanup workflow | +| `src/workflow/cancel.rs` | 80 | Cancel workflow | +| `src/tests.rs` | 500 | Comprehensive tests | +| **Total** | **~5,700** | | + +### Modified Files + +| File | Change | Lines (est.) | +|------|--------|-------------| +| `Cargo.toml` | Add workspace member | 2 | +| `crates/jcode-app-core/src/agent/prompting.rs` | Keyword detection + injection | 80 | +| `crates/jcode-app-core/src/agent/turn_loops.rs` | Pass message to detector | 20 | +| `crates/jcode-tui/src/tui/app/input.rs` | Rainbow + shimmer | 150 | +| `crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs` | Cancel command | 30 | +| `crates/jcode-tui/src/tui/info_widget.rs` | Mode indicator | 50 | +| `crates/jcode-config-types/src/lib.rs` | KeywordsConfig | 30 | +| `crates/jcode-base/src/config.rs` | Load config | 20 | +| **Total** | | **~380** | + +### Grand Total: ~6,080 lines of new/modified code diff --git a/src/lib.rs b/src/lib.rs index 6039fdfe3..54941491b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,9 +50,10 @@ pub async fn run() -> Result<()> { pub fn early_exit_on_help_or_version() { use clap::Parser; match cli::args::Args::try_parse() { - Ok(_) => {} // normal invocation — caller continues + Ok(_) => {} // normal invocation — caller continues Err(e) => match e.kind() { - clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { + clap::error::ErrorKind::DisplayHelp + | clap::error::ErrorKind::DisplayVersion => { let _ = e.print(); std::process::exit(0); }