Skip to main content

punch_runtime/
tools.rs

1//! Built-in tool definitions (JSON schemas for the LLM).
2//!
3//! This module defines the tool schemas that get sent to the LLM so it knows
4//! what tools are available. The actual execution logic lives in `tool_executor`.
5//!
6//! ## Dynamic Tool Selection
7//!
8//! The [`ToolSelector`] provides context-aware tool loading. Instead of sending
9//! all 80+ tools on every LLM call, it loads a core set (~16 tools) and
10//! activates additional tool groups when conversation context mentions relevant
11//! keywords. Once a group is activated in a bout, it stays active (monotonic
12//! growth) to maximize prompt cache hits.
13
14use std::collections::BTreeSet;
15
16use punch_types::{Capability, Message, ToolCategory, ToolDefinition};
17use tracing::info;
18
19/// Return all built-in tool definitions that match the given capabilities.
20///
21/// Only tools the fighter is allowed to use (based on granted capabilities) are
22/// included. This prevents the LLM from seeing tools it can't invoke.
23pub fn tools_for_capabilities(capabilities: &[Capability]) -> Vec<ToolDefinition> {
24    let mut tools = Vec::new();
25
26    for cap in capabilities {
27        match cap {
28            Capability::FileRead(_) => {
29                push_unique(&mut tools, file_read());
30                push_unique(&mut tools, file_list());
31                push_unique(&mut tools, file_search());
32                push_unique(&mut tools, file_info());
33            }
34            Capability::FileWrite(_) => {
35                push_unique(&mut tools, file_write());
36                push_unique(&mut tools, patch_apply());
37            }
38            Capability::ShellExec(_) => {
39                push_unique(&mut tools, shell_exec());
40                push_unique(&mut tools, process_list());
41                push_unique(&mut tools, process_kill());
42                push_unique(&mut tools, env_get());
43                push_unique(&mut tools, env_list());
44            }
45            Capability::Network(_) => {
46                push_unique(&mut tools, web_fetch());
47                push_unique(&mut tools, web_search());
48                push_unique(&mut tools, http_request());
49                push_unique(&mut tools, http_post());
50            }
51            Capability::Memory => {
52                push_unique(&mut tools, memory_store());
53                push_unique(&mut tools, memory_recall());
54            }
55            Capability::KnowledgeGraph => {
56                push_unique(&mut tools, knowledge_add_entity());
57                push_unique(&mut tools, knowledge_add_relation());
58                push_unique(&mut tools, knowledge_query());
59            }
60            Capability::AgentSpawn => {
61                push_unique(&mut tools, agent_spawn());
62            }
63            Capability::AgentMessage => {
64                push_unique(&mut tools, agent_message());
65                push_unique(&mut tools, agent_list());
66            }
67            Capability::BrowserControl => {
68                push_unique(&mut tools, browser_navigate());
69                push_unique(&mut tools, browser_screenshot());
70                push_unique(&mut tools, browser_click());
71                push_unique(&mut tools, browser_type());
72                push_unique(&mut tools, browser_content());
73            }
74            Capability::SourceControl => {
75                push_unique(&mut tools, git_status());
76                push_unique(&mut tools, git_diff());
77                push_unique(&mut tools, git_log());
78                push_unique(&mut tools, git_commit());
79                push_unique(&mut tools, git_branch());
80            }
81            Capability::Container => {
82                push_unique(&mut tools, docker_ps());
83                push_unique(&mut tools, docker_run());
84                push_unique(&mut tools, docker_build());
85                push_unique(&mut tools, docker_logs());
86            }
87            Capability::DataManipulation => {
88                push_unique(&mut tools, json_query());
89                push_unique(&mut tools, json_transform());
90                push_unique(&mut tools, yaml_parse());
91                push_unique(&mut tools, regex_match());
92                push_unique(&mut tools, regex_replace());
93                push_unique(&mut tools, text_diff());
94                push_unique(&mut tools, text_count());
95            }
96            Capability::Schedule => {
97                push_unique(&mut tools, schedule_task());
98                push_unique(&mut tools, schedule_list());
99                push_unique(&mut tools, schedule_cancel());
100            }
101            Capability::CodeAnalysis => {
102                push_unique(&mut tools, code_search());
103                push_unique(&mut tools, code_symbols());
104            }
105            Capability::Archive => {
106                push_unique(&mut tools, archive_create());
107                push_unique(&mut tools, archive_extract());
108                push_unique(&mut tools, archive_list());
109            }
110            Capability::Template => {
111                push_unique(&mut tools, template_render());
112            }
113            Capability::Crypto => {
114                push_unique(&mut tools, hash_compute());
115                push_unique(&mut tools, hash_verify());
116            }
117            Capability::A2ADelegate => {
118                push_unique(&mut tools, a2a_delegate());
119            }
120            Capability::PluginInvoke => {
121                push_unique(&mut tools, wasm_invoke());
122            }
123            Capability::ChannelNotify => {
124                push_unique(&mut tools, channel_notify());
125            }
126            Capability::SelfConfig => {
127                push_unique(&mut tools, heartbeat_add());
128                push_unique(&mut tools, heartbeat_list());
129                push_unique(&mut tools, heartbeat_remove());
130                push_unique(&mut tools, creed_view());
131                push_unique(&mut tools, skill_list());
132                push_unique(&mut tools, skill_recommend());
133            }
134            Capability::SystemAutomation => {
135                push_unique(&mut tools, sys_screenshot());
136            }
137            Capability::UiAutomation(_) => {
138                push_unique(&mut tools, ui_screenshot());
139                push_unique(&mut tools, ui_find_elements());
140                push_unique(&mut tools, ui_click());
141                push_unique(&mut tools, ui_type_text());
142                push_unique(&mut tools, ui_list_windows());
143                push_unique(&mut tools, ui_read_attribute());
144            }
145            Capability::AppIntegration(_) => {
146                push_unique(&mut tools, app_ocr());
147            }
148            _ => {}
149        }
150    }
151
152    tools
153}
154
155/// Return ALL built-in tool definitions (for unrestricted fighters).
156pub fn all_tools() -> Vec<ToolDefinition> {
157    vec![
158        file_read(),
159        file_write(),
160        file_list(),
161        patch_apply(),
162        shell_exec(),
163        web_fetch(),
164        web_search(),
165        memory_store(),
166        memory_recall(),
167        knowledge_add_entity(),
168        knowledge_add_relation(),
169        knowledge_query(),
170        agent_spawn(),
171        agent_message(),
172        agent_list(),
173        browser_navigate(),
174        browser_screenshot(),
175        browser_click(),
176        browser_type(),
177        browser_content(),
178        // Git / Source Control
179        git_status(),
180        git_diff(),
181        git_log(),
182        git_commit(),
183        git_branch(),
184        // Container
185        docker_ps(),
186        docker_run(),
187        docker_build(),
188        docker_logs(),
189        // HTTP
190        http_request(),
191        http_post(),
192        // Data manipulation
193        json_query(),
194        json_transform(),
195        yaml_parse(),
196        regex_match(),
197        regex_replace(),
198        // Process
199        process_list(),
200        process_kill(),
201        // Schedule
202        schedule_task(),
203        schedule_list(),
204        schedule_cancel(),
205        // Code analysis
206        code_search(),
207        code_symbols(),
208        // Archive
209        archive_create(),
210        archive_extract(),
211        archive_list(),
212        // Template
213        template_render(),
214        // Crypto / Hash
215        hash_compute(),
216        hash_verify(),
217        // Environment
218        env_get(),
219        env_list(),
220        // Text
221        text_diff(),
222        text_count(),
223        // File (extended)
224        file_search(),
225        file_info(),
226        // A2A delegation
227        a2a_delegate(),
228        // WASM Plugin
229        wasm_invoke(),
230        // Channel notification
231        channel_notify(),
232        // Self-configuration
233        heartbeat_add(),
234        heartbeat_list(),
235        heartbeat_remove(),
236        creed_view(),
237        skill_list(),
238        skill_recommend(),
239        // Desktop automation
240        sys_screenshot(),
241        ui_screenshot(),
242        app_ocr(),
243        ui_find_elements(),
244        ui_click(),
245        ui_type_text(),
246        ui_list_windows(),
247        ui_read_attribute(),
248    ]
249}
250
251fn push_unique(tools: &mut Vec<ToolDefinition>, tool: ToolDefinition) {
252    if !tools.iter().any(|t| t.name == tool.name) {
253        tools.push(tool);
254    }
255}
256
257// ---------------------------------------------------------------------------
258// Dynamic tool selection
259// ---------------------------------------------------------------------------
260
261/// Groups of contextual tools that activate when conversation context is relevant.
262///
263/// Each group maps to a set of related tools and a list of activation keywords.
264/// Groups are ordered deterministically (via BTreeSet) to keep the tool list
265/// stable across turns for prompt cache efficiency.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
267pub enum ToolGroup {
268    SourceControl,
269    Container,
270    DataManipulation,
271    Schedule,
272    BrowserControl,
273    AgentCoordination,
274    Archive,
275    Template,
276    Crypto,
277    UiAutomation,
278    AppIntegration,
279    PluginInvoke,
280    A2ADelegate,
281    KnowledgeGraph,
282    CodeAnalysis,
283    ProcessManagement,
284    HttpAdvanced,
285}
286
287impl ToolGroup {
288    /// All tool groups.
289    const ALL: &[ToolGroup] = &[
290        ToolGroup::SourceControl,
291        ToolGroup::Container,
292        ToolGroup::DataManipulation,
293        ToolGroup::Schedule,
294        ToolGroup::BrowserControl,
295        ToolGroup::AgentCoordination,
296        ToolGroup::Archive,
297        ToolGroup::Template,
298        ToolGroup::Crypto,
299        ToolGroup::UiAutomation,
300        ToolGroup::AppIntegration,
301        ToolGroup::PluginInvoke,
302        ToolGroup::A2ADelegate,
303        ToolGroup::KnowledgeGraph,
304        ToolGroup::CodeAnalysis,
305        ToolGroup::ProcessManagement,
306        ToolGroup::HttpAdvanced,
307    ];
308
309    /// Keywords that trigger this tool group. Matched case-insensitively
310    /// against conversation messages.
311    fn keywords(&self) -> &'static [&'static str] {
312        match self {
313            ToolGroup::SourceControl => &[
314                "git ",
315                "git commit",
316                "git branch",
317                "git diff",
318                "git merge",
319                "git rebase",
320                "git push",
321                "git pull",
322                "git clone",
323                "git stash",
324                "git checkout",
325                "git log",
326                "commit changes",
327                "commit my",
328                "repo",
329                "repository",
330            ],
331            ToolGroup::Container => &[
332                "docker",
333                "container",
334                "dockerfile",
335                "docker image",
336                "compose",
337                "pod",
338                "kubernetes",
339                "k8s",
340                "helm",
341            ],
342            ToolGroup::DataManipulation => &[
343                "json",
344                "yaml",
345                "toml",
346                "regex",
347                "parse json",
348                "parse yaml",
349                "transform json",
350                "csv",
351                "xml",
352                "jq",
353            ],
354            ToolGroup::Schedule => &[
355                "schedule",
356                "cron",
357                "timer",
358                "periodic",
359                "interval",
360                "recurring",
361                "every hour",
362                "every day",
363            ],
364            ToolGroup::BrowserControl => &[
365                "browser",
366                "webpage",
367                "click button",
368                "click element",
369                "click link",
370                "navigate to",
371                "selenium",
372                "puppeteer",
373                "scrape",
374                "open website",
375                "web page",
376            ],
377            ToolGroup::AgentCoordination => &[
378                "spawn fighter",
379                "spawn agent",
380                "spawn worker",
381                "new fighter",
382                "troop",
383                "multi-agent",
384                "delegate to",
385                "coordinate agents",
386            ],
387            ToolGroup::Archive => &[
388                "archive",
389                "zip file",
390                "tar file",
391                "gzip",
392                "extract archive",
393                "compress file",
394                "unzip",
395                "decompress",
396            ],
397            ToolGroup::Template => &[
398                "template",
399                "render template",
400                "handlebars",
401                "mustache",
402                "jinja",
403            ],
404            ToolGroup::Crypto => &[
405                "sha256",
406                "sha512",
407                "crypto",
408                "md5",
409                "hmac",
410                "checksum",
411                "digest",
412                "encrypt",
413                "decrypt",
414                "hash file",
415                "hash password",
416            ],
417            ToolGroup::UiAutomation => &[
418                "ui automation",
419                "accessibility",
420                "ui element",
421                "click button",
422                "menu bar",
423                "window list",
424                "applescript",
425                "osascript",
426            ],
427            ToolGroup::AppIntegration => &[
428                "ocr",
429                "text recognition",
430                "screen read",
431                "extract text from image",
432            ],
433            ToolGroup::PluginInvoke => &["plugin", "wasm", "extension", "webassembly"],
434            ToolGroup::A2ADelegate => &["a2a", "remote agent", "delegate task", "external agent"],
435            ToolGroup::KnowledgeGraph => &[
436                "knowledge graph",
437                "add entity",
438                "add relation",
439                "knowledge base",
440                "ontology",
441                "triple store",
442            ],
443            ToolGroup::CodeAnalysis => &[
444                "code symbols",
445                "code search",
446                "find definition",
447                "find references",
448                "ast",
449                "code analysis",
450                "go to definition",
451            ],
452            ToolGroup::ProcessManagement => &[
453                "list processes",
454                "kill process",
455                "pid",
456                "send signal",
457                "background process",
458                "process list",
459            ],
460            ToolGroup::HttpAdvanced => &[
461                "http post",
462                "api call",
463                "rest api",
464                "api endpoint",
465                "webhook",
466                "curl",
467                "http request",
468                "http put",
469                "http delete",
470            ],
471        }
472    }
473
474    /// Check whether the fighter's capabilities include the one required for this group.
475    fn is_permitted(&self, capabilities: &[Capability]) -> bool {
476        capabilities.iter().any(|c| match self {
477            ToolGroup::SourceControl => matches!(c, Capability::SourceControl),
478            ToolGroup::Container => matches!(c, Capability::Container),
479            ToolGroup::DataManipulation => matches!(c, Capability::DataManipulation),
480            ToolGroup::Schedule => matches!(c, Capability::Schedule),
481            ToolGroup::BrowserControl => matches!(c, Capability::BrowserControl),
482            ToolGroup::AgentCoordination => {
483                matches!(c, Capability::AgentSpawn | Capability::AgentMessage)
484            }
485            ToolGroup::Archive => matches!(c, Capability::Archive),
486            ToolGroup::Template => matches!(c, Capability::Template),
487            ToolGroup::Crypto => matches!(c, Capability::Crypto),
488            ToolGroup::UiAutomation => matches!(c, Capability::UiAutomation(_)),
489            ToolGroup::AppIntegration => matches!(c, Capability::AppIntegration(_)),
490            ToolGroup::PluginInvoke => matches!(c, Capability::PluginInvoke),
491            ToolGroup::A2ADelegate => matches!(c, Capability::A2ADelegate),
492            ToolGroup::KnowledgeGraph => matches!(c, Capability::KnowledgeGraph),
493            ToolGroup::CodeAnalysis => matches!(c, Capability::CodeAnalysis),
494            ToolGroup::ProcessManagement => matches!(c, Capability::ShellExec(_)),
495            ToolGroup::HttpAdvanced => matches!(c, Capability::Network(_)),
496        })
497    }
498
499    /// Return the tool definitions for this group.
500    fn tools(&self) -> Vec<ToolDefinition> {
501        match self {
502            ToolGroup::SourceControl => {
503                vec![
504                    git_status(),
505                    git_diff(),
506                    git_log(),
507                    git_commit(),
508                    git_branch(),
509                ]
510            }
511            ToolGroup::Container => {
512                vec![docker_ps(), docker_run(), docker_build(), docker_logs()]
513            }
514            ToolGroup::DataManipulation => vec![
515                json_query(),
516                json_transform(),
517                yaml_parse(),
518                regex_match(),
519                regex_replace(),
520                text_diff(),
521                text_count(),
522            ],
523            ToolGroup::Schedule => {
524                vec![schedule_task(), schedule_list(), schedule_cancel()]
525            }
526            ToolGroup::BrowserControl => vec![
527                browser_navigate(),
528                browser_screenshot(),
529                browser_click(),
530                browser_type(),
531                browser_content(),
532            ],
533            ToolGroup::AgentCoordination => {
534                vec![agent_spawn(), agent_message(), agent_list()]
535            }
536            ToolGroup::Archive => {
537                vec![archive_create(), archive_extract(), archive_list()]
538            }
539            ToolGroup::Template => vec![template_render()],
540            ToolGroup::Crypto => vec![hash_compute(), hash_verify()],
541            ToolGroup::UiAutomation => vec![
542                ui_screenshot(),
543                ui_find_elements(),
544                ui_click(),
545                ui_type_text(),
546                ui_list_windows(),
547                ui_read_attribute(),
548            ],
549            ToolGroup::AppIntegration => vec![app_ocr()],
550            ToolGroup::PluginInvoke => vec![wasm_invoke()],
551            ToolGroup::A2ADelegate => vec![a2a_delegate()],
552            ToolGroup::KnowledgeGraph => vec![
553                knowledge_add_entity(),
554                knowledge_add_relation(),
555                knowledge_query(),
556            ],
557            ToolGroup::CodeAnalysis => vec![code_search(), code_symbols()],
558            ToolGroup::ProcessManagement => vec![process_list(), process_kill()],
559            ToolGroup::HttpAdvanced => vec![http_request(), http_post()],
560        }
561    }
562}
563
564/// Dynamic tool selector that loads tools based on conversation context.
565///
566/// Core tools (~18) are always available. Contextual tool groups activate
567/// when relevant keywords appear in recent messages and stay active for the
568/// remainder of the bout (monotonic growth) to maximize prompt cache hits.
569///
570/// The tool executor can still run ANY tool regardless of what the LLM sees —
571/// capabilities are checked at execution time, not selection time. The model
572/// never loses capability: `shell_exec` is always in the core set and can do
573/// anything the contextual tools can via command-line equivalents.
574pub struct ToolSelector {
575    /// Fighter's granted capabilities.
576    capabilities: Vec<Capability>,
577    /// Tool groups activated so far in this bout (only grows, never shrinks).
578    active_groups: BTreeSet<ToolGroup>,
579    /// Hash of the last tool list returned, for change detection.
580    last_tool_hash: u64,
581}
582
583impl ToolSelector {
584    /// Create a new selector for a fighter with the given capabilities.
585    ///
586    /// Groups whose capabilities are granted but have no representation in the
587    /// core tool set are auto-activated on construction. This prevents a
588    /// capability regression where, e.g., a fighter with `SourceControl` wouldn't
589    /// get git tools on turn 1 unless the user explicitly said "git".
590    pub fn new(capabilities: &[Capability]) -> Self {
591        let mut active_groups = BTreeSet::new();
592
593        // Auto-activate groups that are permitted AND have no core-tool overlap.
594        // These are "capability-only" groups whose tools would be invisible without
595        // keyword triggers, creating a silent regression for restricted fighters.
596        for group in ToolGroup::ALL {
597            if group.is_permitted(capabilities) && Self::group_needs_auto_activate(group) {
598                active_groups.insert(*group);
599            }
600        }
601
602        if !active_groups.is_empty() {
603            info!(
604                auto_activated = ?active_groups,
605                "tool selector: auto-activated groups for granted capabilities"
606            );
607        }
608
609        Self {
610            capabilities: capabilities.to_vec(),
611            active_groups,
612            last_tool_hash: 0,
613        }
614    }
615
616    /// Returns true if a group's tools have zero overlap with core tools,
617    /// meaning the group would be invisible without keyword activation.
618    fn group_needs_auto_activate(group: &ToolGroup) -> bool {
619        matches!(
620            group,
621            ToolGroup::SourceControl
622                | ToolGroup::Container
623                | ToolGroup::DataManipulation
624                | ToolGroup::Schedule
625                | ToolGroup::BrowserControl
626                | ToolGroup::AgentCoordination
627                | ToolGroup::ProcessManagement
628        )
629    }
630
631    /// Select tools based on conversation context.
632    ///
633    /// Returns `(tools, tools_changed)` where `tools_changed` is true if the
634    /// tool list differs from the previous call (useful for cache invalidation
635    /// decisions).
636    ///
637    /// Scans the last 4 messages (current + 3 prior) for keywords. Once a
638    /// group activates, it stays active for the rest of the bout.
639    pub fn select_tools(&mut self, messages: &[Message]) -> (Vec<ToolDefinition>, bool) {
640        let scan_text = Self::build_scan_text(messages);
641
642        let mut newly_activated = Vec::new();
643        for group in ToolGroup::ALL {
644            if self.active_groups.contains(group) {
645                continue;
646            }
647            if !group.is_permitted(&self.capabilities) {
648                continue;
649            }
650            if Self::keywords_match(&scan_text, group.keywords()) {
651                self.active_groups.insert(*group);
652                newly_activated.push(*group);
653            }
654        }
655
656        if !newly_activated.is_empty() {
657            info!(
658                groups = ?newly_activated,
659                total_active = self.active_groups.len(),
660                "tool selector: activated new groups"
661            );
662        }
663
664        let mut tools = self.core_tools();
665        for group in &self.active_groups {
666            for tool in group.tools() {
667                push_unique(&mut tools, tool);
668            }
669        }
670
671        let hash = Self::compute_hash(&tools);
672        let changed = hash != self.last_tool_hash;
673        self.last_tool_hash = hash;
674
675        (tools, changed)
676    }
677
678    /// Return the number of currently active contextual tool groups.
679    pub fn active_group_count(&self) -> usize {
680        self.active_groups.len()
681    }
682
683    /// Core tools that are always loaded (when capability permits).
684    fn core_tools(&self) -> Vec<ToolDefinition> {
685        let mut tools = Vec::with_capacity(20);
686
687        for cap in &self.capabilities {
688            match cap {
689                Capability::FileRead(_) => {
690                    push_unique(&mut tools, file_read());
691                    push_unique(&mut tools, file_list());
692                    push_unique(&mut tools, file_search());
693                    push_unique(&mut tools, file_info());
694                }
695                Capability::FileWrite(_) => {
696                    push_unique(&mut tools, file_write());
697                    push_unique(&mut tools, patch_apply());
698                }
699                Capability::ShellExec(_) => {
700                    push_unique(&mut tools, shell_exec());
701                    push_unique(&mut tools, env_get());
702                    push_unique(&mut tools, env_list());
703                }
704                Capability::Network(_) => {
705                    push_unique(&mut tools, web_fetch());
706                    push_unique(&mut tools, web_search());
707                }
708                Capability::Memory => {
709                    push_unique(&mut tools, memory_store());
710                    push_unique(&mut tools, memory_recall());
711                }
712                Capability::SystemAutomation => {
713                    push_unique(&mut tools, sys_screenshot());
714                }
715                Capability::ChannelNotify => {
716                    push_unique(&mut tools, channel_notify());
717                }
718                Capability::SelfConfig => {
719                    push_unique(&mut tools, heartbeat_add());
720                    push_unique(&mut tools, heartbeat_list());
721                    push_unique(&mut tools, heartbeat_remove());
722                    push_unique(&mut tools, creed_view());
723                    push_unique(&mut tools, skill_list());
724                    push_unique(&mut tools, skill_recommend());
725                }
726                _ => {}
727            }
728        }
729
730        tools
731    }
732
733    /// Build the keyword scan text from recent messages.
734    ///
735    /// Scans message content, tool call names/arguments, and tool result content
736    /// to catch context from tool usage (e.g. a git_status call signals SourceControl).
737    fn build_scan_text(messages: &[Message]) -> String {
738        let window_size = 4.min(messages.len());
739        let start = messages.len().saturating_sub(window_size);
740        let mut text = String::with_capacity(4000);
741        for msg in &messages[start..] {
742            text.push_str(&msg.content);
743            text.push(' ');
744            for tc in &msg.tool_calls {
745                text.push_str(&tc.name);
746                text.push(' ');
747                // Include a compact form of arguments (tool names are most useful).
748                if let Some(obj) = tc.input.as_object() {
749                    for val in obj.values() {
750                        if let Some(s) = val.as_str() {
751                            // Only include short string values to avoid bloating scan text.
752                            if s.len() <= 200 {
753                                text.push_str(s);
754                                text.push(' ');
755                            }
756                        }
757                    }
758                }
759            }
760            for tr in &msg.tool_results {
761                // Include first ~200 chars of tool result content for keyword detection.
762                // Use floor_char_boundary to avoid panicking on multi-byte UTF-8.
763                let end = if tr.content.len() > 200 {
764                    tr.content.floor_char_boundary(200)
765                } else {
766                    tr.content.len()
767                };
768                text.push_str(&tr.content[..end]);
769                text.push(' ');
770            }
771        }
772        text.to_lowercase()
773    }
774
775    /// Check if any keyword appears in the scan text.
776    fn keywords_match(scan_text: &str, keywords: &[&str]) -> bool {
777        keywords.iter().any(|kw| scan_text.contains(kw))
778    }
779
780    /// Compute a stable hash of the tool list for change detection.
781    ///
782    /// Uses a simple FNV-1a-inspired hash that is stable across Rust versions
783    /// (unlike `DefaultHasher` which uses SipHash with randomized keys).
784    fn compute_hash(tools: &[ToolDefinition]) -> u64 {
785        let mut hash: u64 = 0xcbf2_9ce4_8422_2325; // FNV offset basis
786        for tool in tools {
787            for byte in tool.name.as_bytes() {
788                hash ^= u64::from(*byte);
789                hash = hash.wrapping_mul(0x0100_0000_01b3); // FNV prime
790            }
791            // Separator to distinguish "ab"+"cd" from "abc"+"d".
792            hash ^= 0xff;
793            hash = hash.wrapping_mul(0x0100_0000_01b3);
794        }
795        hash
796    }
797}
798
799// ---------------------------------------------------------------------------
800// Tool definitions
801// ---------------------------------------------------------------------------
802
803fn file_read() -> ToolDefinition {
804    ToolDefinition {
805        name: "file_read".into(),
806        description: "Read a file by path (absolute or relative). Supports text, code, config, logs, CSV, JSON, etc.".into(),
807        input_schema: serde_json::json!({
808            "type": "object",
809            "properties": {
810                "path": {
811                    "type": "string",
812                    "description": "The file path to read (relative to working directory or absolute)."
813                }
814            },
815            "required": ["path"]
816        }),
817        category: ToolCategory::FileSystem,
818    }
819}
820
821fn file_write() -> ToolDefinition {
822    ToolDefinition {
823        name: "file_write".into(),
824        description: "Write or create a file. Creates parent directories if needed.".into(),
825        input_schema: serde_json::json!({
826            "type": "object",
827            "properties": {
828                "path": {
829                    "type": "string",
830                    "description": "The file path to write to."
831                },
832                "content": {
833                    "type": "string",
834                    "description": "The content to write to the file."
835                }
836            },
837            "required": ["path", "content"]
838        }),
839        category: ToolCategory::FileSystem,
840    }
841}
842
843fn file_list() -> ToolDefinition {
844    ToolDefinition {
845        name: "file_list".into(),
846        description: "List files and directories in a folder.".into(),
847        input_schema: serde_json::json!({
848            "type": "object",
849            "properties": {
850                "path": {
851                    "type": "string",
852                    "description": "The directory path to list (defaults to working directory)."
853                }
854            }
855        }),
856        category: ToolCategory::FileSystem,
857    }
858}
859
860fn shell_exec() -> ToolDefinition {
861    ToolDefinition {
862        name: "shell_exec".into(),
863        description: "Execute a shell command and return stdout, stderr, and exit code. Universal fallback for any task doable from a terminal.".into(),
864        input_schema: serde_json::json!({
865            "type": "object",
866            "properties": {
867                "command": {
868                    "type": "string",
869                    "description": "The shell command to execute."
870                }
871            },
872            "required": ["command"]
873        }),
874        category: ToolCategory::Shell,
875    }
876}
877
878fn web_fetch() -> ToolDefinition {
879    ToolDefinition {
880        name: "web_fetch".into(),
881        description: "Fetch the content of a URL via HTTP GET.".into(),
882        input_schema: serde_json::json!({
883            "type": "object",
884            "properties": {
885                "url": {
886                    "type": "string",
887                    "description": "The URL to fetch."
888                }
889            },
890            "required": ["url"]
891        }),
892        category: ToolCategory::Web,
893    }
894}
895
896fn web_search() -> ToolDefinition {
897    ToolDefinition {
898        name: "web_search".into(),
899        description: "Search the web and return top results with titles and URLs.".into(),
900        input_schema: serde_json::json!({
901            "type": "object",
902            "properties": {
903                "query": {
904                    "type": "string",
905                    "description": "The search query."
906                }
907            },
908            "required": ["query"]
909        }),
910        category: ToolCategory::Web,
911    }
912}
913
914fn memory_store() -> ToolDefinition {
915    ToolDefinition {
916        name: "memory_store".into(),
917        description: "Store a key-value pair in persistent memory. Use to remember facts, preferences, or context across conversations.".into(),
918        input_schema: serde_json::json!({
919            "type": "object",
920            "properties": {
921                "key": {
922                    "type": "string",
923                    "description": "A short descriptive key for the memory."
924                },
925                "value": {
926                    "type": "string",
927                    "description": "The value to remember."
928                },
929                "confidence": {
930                    "type": "number",
931                    "description": "Confidence level from 0.0 to 1.0 (default: 0.9)."
932                }
933            },
934            "required": ["key", "value"]
935        }),
936        category: ToolCategory::Memory,
937    }
938}
939
940fn memory_recall() -> ToolDefinition {
941    ToolDefinition {
942        name: "memory_recall".into(),
943        description: "Search persistent memory for previously stored information.".into(),
944        input_schema: serde_json::json!({
945            "type": "object",
946            "properties": {
947                "query": {
948                    "type": "string",
949                    "description": "Search query to find relevant memories."
950                },
951                "limit": {
952                    "type": "integer",
953                    "description": "Maximum number of results (default: 10)."
954                }
955            },
956            "required": ["query"]
957        }),
958        category: ToolCategory::Memory,
959    }
960}
961
962fn knowledge_add_entity() -> ToolDefinition {
963    ToolDefinition {
964        name: "knowledge_add_entity".into(),
965        description: "Add an entity to your knowledge graph.".into(),
966        input_schema: serde_json::json!({
967            "type": "object",
968            "properties": {
969                "name": {
970                    "type": "string",
971                    "description": "Name of the entity."
972                },
973                "entity_type": {
974                    "type": "string",
975                    "description": "Type of entity (e.g. 'person', 'company', 'concept')."
976                },
977                "properties": {
978                    "type": "object",
979                    "description": "Additional properties as key-value pairs."
980                }
981            },
982            "required": ["name", "entity_type"]
983        }),
984        category: ToolCategory::Knowledge,
985    }
986}
987
988fn knowledge_add_relation() -> ToolDefinition {
989    ToolDefinition {
990        name: "knowledge_add_relation".into(),
991        description: "Add a relation between two entities in your knowledge graph.".into(),
992        input_schema: serde_json::json!({
993            "type": "object",
994            "properties": {
995                "from": {
996                    "type": "string",
997                    "description": "Source entity name."
998                },
999                "relation": {
1000                    "type": "string",
1001                    "description": "The relation type (e.g. 'works_at', 'depends_on')."
1002                },
1003                "to": {
1004                    "type": "string",
1005                    "description": "Target entity name."
1006                },
1007                "properties": {
1008                    "type": "object",
1009                    "description": "Additional properties."
1010                }
1011            },
1012            "required": ["from", "relation", "to"]
1013        }),
1014        category: ToolCategory::Knowledge,
1015    }
1016}
1017
1018fn knowledge_query() -> ToolDefinition {
1019    ToolDefinition {
1020        name: "knowledge_query".into(),
1021        description: "Search your knowledge graph for entities and their relations.".into(),
1022        input_schema: serde_json::json!({
1023            "type": "object",
1024            "properties": {
1025                "query": {
1026                    "type": "string",
1027                    "description": "Search query to find entities."
1028                }
1029            },
1030            "required": ["query"]
1031        }),
1032        category: ToolCategory::Knowledge,
1033    }
1034}
1035
1036// ---------------------------------------------------------------------------
1037// Agent coordination tools
1038// ---------------------------------------------------------------------------
1039
1040fn agent_spawn() -> ToolDefinition {
1041    ToolDefinition {
1042        name: "agent_spawn".into(),
1043        description: "Spawn a new fighter (AI agent) and return its ID.".into(),
1044        input_schema: serde_json::json!({
1045            "type": "object",
1046            "properties": {
1047                "name": {
1048                    "type": "string",
1049                    "description": "A human-readable name for the new fighter."
1050                },
1051                "system_prompt": {
1052                    "type": "string",
1053                    "description": "The system prompt that shapes the new fighter's behavior and specialization."
1054                },
1055                "description": {
1056                    "type": "string",
1057                    "description": "A short description of the fighter's purpose (optional)."
1058                },
1059                "capabilities": {
1060                    "type": "array",
1061                    "description": "Capabilities to grant the new fighter (optional). Each item is a capability object.",
1062                    "items": {
1063                        "type": "object"
1064                    }
1065                }
1066            },
1067            "required": ["name", "system_prompt"]
1068        }),
1069        category: ToolCategory::Agent,
1070    }
1071}
1072
1073fn agent_message() -> ToolDefinition {
1074    ToolDefinition {
1075        name: "agent_message".into(),
1076        description: "Send a message to another fighter by ID or name and get its response.".into(),
1077        input_schema: serde_json::json!({
1078            "type": "object",
1079            "properties": {
1080                "fighter_id": {
1081                    "type": "string",
1082                    "description": "The UUID of the target fighter (provide either this or 'name')."
1083                },
1084                "name": {
1085                    "type": "string",
1086                    "description": "The name of the target fighter (provide either this or 'fighter_id')."
1087                },
1088                "message": {
1089                    "type": "string",
1090                    "description": "The message to send to the target fighter."
1091                }
1092            },
1093            "required": ["message"]
1094        }),
1095        category: ToolCategory::Agent,
1096    }
1097}
1098
1099fn agent_list() -> ToolDefinition {
1100    ToolDefinition {
1101        name: "agent_list".into(),
1102        description: "List all active fighters (AI agents) with their IDs, names, and status."
1103            .into(),
1104        input_schema: serde_json::json!({
1105            "type": "object",
1106            "properties": {}
1107        }),
1108        category: ToolCategory::Agent,
1109    }
1110}
1111
1112// ---------------------------------------------------------------------------
1113// Patch tools — combo move corrections
1114// ---------------------------------------------------------------------------
1115
1116fn patch_apply() -> ToolDefinition {
1117    ToolDefinition {
1118        name: "patch_apply".into(),
1119        description: "Apply a unified diff patch to a file.".into(),
1120        input_schema: serde_json::json!({
1121            "type": "object",
1122            "properties": {
1123                "path": {
1124                    "type": "string",
1125                    "description": "The file path to patch (relative to working directory or absolute)."
1126                },
1127                "diff": {
1128                    "type": "string",
1129                    "description": "The unified diff text to apply to the file."
1130                }
1131            },
1132            "required": ["path", "diff"]
1133        }),
1134        category: ToolCategory::FileSystem,
1135    }
1136}
1137
1138// ---------------------------------------------------------------------------
1139// Browser automation tools — ring-side scouting moves
1140// ---------------------------------------------------------------------------
1141
1142fn browser_navigate() -> ToolDefinition {
1143    ToolDefinition {
1144        name: "browser_navigate".into(),
1145        description: "Navigate the browser to a URL. Opens the page and waits for it to load."
1146            .into(),
1147        input_schema: serde_json::json!({
1148            "type": "object",
1149            "properties": {
1150                "url": {
1151                    "type": "string",
1152                    "description": "The URL to navigate to."
1153                }
1154            },
1155            "required": ["url"]
1156        }),
1157        category: ToolCategory::Browser,
1158    }
1159}
1160
1161fn browser_screenshot() -> ToolDefinition {
1162    ToolDefinition {
1163        name: "browser_screenshot".into(),
1164        description: "Take a screenshot of the current page. Returns a base64-encoded PNG image."
1165            .into(),
1166        input_schema: serde_json::json!({
1167            "type": "object",
1168            "properties": {
1169                "full_page": {
1170                    "type": "boolean",
1171                    "description": "Capture the full scrollable page (true) or just the viewport (false). Default: false."
1172                }
1173            }
1174        }),
1175        category: ToolCategory::Browser,
1176    }
1177}
1178
1179fn browser_click() -> ToolDefinition {
1180    ToolDefinition {
1181        name: "browser_click".into(),
1182        description: "Click an element on the page matching the given CSS selector.".into(),
1183        input_schema: serde_json::json!({
1184            "type": "object",
1185            "properties": {
1186                "selector": {
1187                    "type": "string",
1188                    "description": "CSS selector of the element to click."
1189                }
1190            },
1191            "required": ["selector"]
1192        }),
1193        category: ToolCategory::Browser,
1194    }
1195}
1196
1197fn browser_type() -> ToolDefinition {
1198    ToolDefinition {
1199        name: "browser_type".into(),
1200        description: "Type text into an input element matching the given CSS selector.".into(),
1201        input_schema: serde_json::json!({
1202            "type": "object",
1203            "properties": {
1204                "selector": {
1205                    "type": "string",
1206                    "description": "CSS selector of the input element."
1207                },
1208                "text": {
1209                    "type": "string",
1210                    "description": "The text to type into the element."
1211                }
1212            },
1213            "required": ["selector", "text"]
1214        }),
1215        category: ToolCategory::Browser,
1216    }
1217}
1218
1219fn browser_content() -> ToolDefinition {
1220    ToolDefinition {
1221        name: "browser_content".into(),
1222        description: "Get the text content of the page or a specific element by CSS selector."
1223            .into(),
1224        input_schema: serde_json::json!({
1225            "type": "object",
1226            "properties": {
1227                "selector": {
1228                    "type": "string",
1229                    "description": "Optional CSS selector. If omitted, returns the full page text content."
1230                }
1231            }
1232        }),
1233        category: ToolCategory::Browser,
1234    }
1235}
1236
1237// ---------------------------------------------------------------------------
1238// Git / Source Control tools
1239// ---------------------------------------------------------------------------
1240
1241fn git_status() -> ToolDefinition {
1242    ToolDefinition {
1243        name: "git_status".into(),
1244        description: "Run `git status --porcelain` in the working directory to show changed files."
1245            .into(),
1246        input_schema: serde_json::json!({
1247            "type": "object",
1248            "properties": {}
1249        }),
1250        category: ToolCategory::SourceControl,
1251    }
1252}
1253
1254fn git_diff() -> ToolDefinition {
1255    ToolDefinition {
1256        name: "git_diff".into(),
1257        description:
1258            "Run `git diff` to show unstaged changes. Use `staged: true` to see staged changes."
1259                .into(),
1260        input_schema: serde_json::json!({
1261            "type": "object",
1262            "properties": {
1263                "staged": {
1264                    "type": "boolean",
1265                    "description": "If true, show staged changes (--staged). Default: false."
1266                },
1267                "path": {
1268                    "type": "string",
1269                    "description": "Optional file path to restrict the diff to."
1270                }
1271            }
1272        }),
1273        category: ToolCategory::SourceControl,
1274    }
1275}
1276
1277fn git_log() -> ToolDefinition {
1278    ToolDefinition {
1279        name: "git_log".into(),
1280        description: "Show recent git commits with `git log --oneline`.".into(),
1281        input_schema: serde_json::json!({
1282            "type": "object",
1283            "properties": {
1284                "count": {
1285                    "type": "integer",
1286                    "description": "Number of commits to show (default: 10)."
1287                }
1288            }
1289        }),
1290        category: ToolCategory::SourceControl,
1291    }
1292}
1293
1294fn git_commit() -> ToolDefinition {
1295    ToolDefinition {
1296        name: "git_commit".into(),
1297        description: "Stage files and create a git commit with the given message.".into(),
1298        input_schema: serde_json::json!({
1299            "type": "object",
1300            "properties": {
1301                "message": {
1302                    "type": "string",
1303                    "description": "The commit message."
1304                },
1305                "files": {
1306                    "type": "array",
1307                    "items": { "type": "string" },
1308                    "description": "Files to stage before committing. If empty, commits all staged changes."
1309                }
1310            },
1311            "required": ["message"]
1312        }),
1313        category: ToolCategory::SourceControl,
1314    }
1315}
1316
1317fn git_branch() -> ToolDefinition {
1318    ToolDefinition {
1319        name: "git_branch".into(),
1320        description: "List, create, or switch git branches.".into(),
1321        input_schema: serde_json::json!({
1322            "type": "object",
1323            "properties": {
1324                "action": {
1325                    "type": "string",
1326                    "enum": ["list", "create", "switch"],
1327                    "description": "Action to perform: list, create, or switch. Default: list."
1328                },
1329                "name": {
1330                    "type": "string",
1331                    "description": "Branch name (required for create and switch)."
1332                }
1333            }
1334        }),
1335        category: ToolCategory::SourceControl,
1336    }
1337}
1338
1339// ---------------------------------------------------------------------------
1340// Container tools
1341// ---------------------------------------------------------------------------
1342
1343fn docker_ps() -> ToolDefinition {
1344    ToolDefinition {
1345        name: "docker_ps".into(),
1346        description: "List running Docker containers.".into(),
1347        input_schema: serde_json::json!({
1348            "type": "object",
1349            "properties": {
1350                "all": {
1351                    "type": "boolean",
1352                    "description": "Show all containers, not just running ones. Default: false."
1353                }
1354            }
1355        }),
1356        category: ToolCategory::Container,
1357    }
1358}
1359
1360fn docker_run() -> ToolDefinition {
1361    ToolDefinition {
1362        name: "docker_run".into(),
1363        description: "Run a Docker container from an image.".into(),
1364        input_schema: serde_json::json!({
1365            "type": "object",
1366            "properties": {
1367                "image": {
1368                    "type": "string",
1369                    "description": "The Docker image to run."
1370                },
1371                "command": {
1372                    "type": "string",
1373                    "description": "Optional command to run inside the container."
1374                },
1375                "env": {
1376                    "type": "object",
1377                    "description": "Environment variables as key-value pairs."
1378                },
1379                "ports": {
1380                    "type": "array",
1381                    "items": { "type": "string" },
1382                    "description": "Port mappings (e.g. '8080:80')."
1383                },
1384                "detach": {
1385                    "type": "boolean",
1386                    "description": "Run in detached mode. Default: false."
1387                },
1388                "name": {
1389                    "type": "string",
1390                    "description": "Optional container name."
1391                }
1392            },
1393            "required": ["image"]
1394        }),
1395        category: ToolCategory::Container,
1396    }
1397}
1398
1399fn docker_build() -> ToolDefinition {
1400    ToolDefinition {
1401        name: "docker_build".into(),
1402        description: "Build a Docker image from a Dockerfile.".into(),
1403        input_schema: serde_json::json!({
1404            "type": "object",
1405            "properties": {
1406                "path": {
1407                    "type": "string",
1408                    "description": "Path to the build context directory (default: '.')."
1409                },
1410                "tag": {
1411                    "type": "string",
1412                    "description": "Tag for the built image (e.g. 'myapp:latest')."
1413                },
1414                "dockerfile": {
1415                    "type": "string",
1416                    "description": "Path to the Dockerfile (default: 'Dockerfile')."
1417                }
1418            }
1419        }),
1420        category: ToolCategory::Container,
1421    }
1422}
1423
1424fn docker_logs() -> ToolDefinition {
1425    ToolDefinition {
1426        name: "docker_logs".into(),
1427        description: "Get logs from a Docker container.".into(),
1428        input_schema: serde_json::json!({
1429            "type": "object",
1430            "properties": {
1431                "container": {
1432                    "type": "string",
1433                    "description": "Container ID or name."
1434                },
1435                "tail": {
1436                    "type": "integer",
1437                    "description": "Number of lines to show from the end (default: 100)."
1438                }
1439            },
1440            "required": ["container"]
1441        }),
1442        category: ToolCategory::Container,
1443    }
1444}
1445
1446// ---------------------------------------------------------------------------
1447// HTTP tools
1448// ---------------------------------------------------------------------------
1449
1450fn http_request() -> ToolDefinition {
1451    ToolDefinition {
1452        name: "http_request".into(),
1453        description: "Send a full HTTP request with custom method, headers, body, and timeout."
1454            .into(),
1455        input_schema: serde_json::json!({
1456            "type": "object",
1457            "properties": {
1458                "url": {
1459                    "type": "string",
1460                    "description": "The URL to send the request to."
1461                },
1462                "method": {
1463                    "type": "string",
1464                    "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"],
1465                    "description": "HTTP method. Default: GET."
1466                },
1467                "headers": {
1468                    "type": "object",
1469                    "description": "Request headers as key-value pairs."
1470                },
1471                "body": {
1472                    "type": "string",
1473                    "description": "Request body."
1474                },
1475                "timeout_secs": {
1476                    "type": "integer",
1477                    "description": "Request timeout in seconds (default: 30)."
1478                }
1479            },
1480            "required": ["url"]
1481        }),
1482        category: ToolCategory::Web,
1483    }
1484}
1485
1486fn http_post() -> ToolDefinition {
1487    ToolDefinition {
1488        name: "http_post".into(),
1489        description: "Shorthand for an HTTP POST request with a JSON body.".into(),
1490        input_schema: serde_json::json!({
1491            "type": "object",
1492            "properties": {
1493                "url": {
1494                    "type": "string",
1495                    "description": "The URL to POST to."
1496                },
1497                "json": {
1498                    "type": "object",
1499                    "description": "JSON body to send."
1500                },
1501                "headers": {
1502                    "type": "object",
1503                    "description": "Additional headers."
1504                }
1505            },
1506            "required": ["url", "json"]
1507        }),
1508        category: ToolCategory::Web,
1509    }
1510}
1511
1512// ---------------------------------------------------------------------------
1513// Data manipulation tools
1514// ---------------------------------------------------------------------------
1515
1516fn json_query() -> ToolDefinition {
1517    ToolDefinition {
1518        name: "json_query".into(),
1519        description: "Query a JSON value using a dot-separated path (e.g. 'users.0.name'). Array indices are numeric.".into(),
1520        input_schema: serde_json::json!({
1521            "type": "object",
1522            "properties": {
1523                "data": {
1524                    "description": "The JSON data to query (object, array, or string to parse)."
1525                },
1526                "path": {
1527                    "type": "string",
1528                    "description": "Dot-separated path to query (e.g. 'a.b.0.c')."
1529                }
1530            },
1531            "required": ["data", "path"]
1532        }),
1533        category: ToolCategory::Data,
1534    }
1535}
1536
1537fn json_transform() -> ToolDefinition {
1538    ToolDefinition {
1539        name: "json_transform".into(),
1540        description: "Transform JSON: extract keys, rename keys, or filter arrays.".into(),
1541        input_schema: serde_json::json!({
1542            "type": "object",
1543            "properties": {
1544                "data": {
1545                    "description": "The JSON data to transform."
1546                },
1547                "extract": {
1548                    "type": "array",
1549                    "items": { "type": "string" },
1550                    "description": "List of keys to extract from each object."
1551                },
1552                "rename": {
1553                    "type": "object",
1554                    "description": "Key rename mapping (old_name -> new_name)."
1555                },
1556                "filter_key": {
1557                    "type": "string",
1558                    "description": "Key to filter array items by."
1559                },
1560                "filter_value": {
1561                    "type": "string",
1562                    "description": "Value the filter_key must match."
1563                }
1564            },
1565            "required": ["data"]
1566        }),
1567        category: ToolCategory::Data,
1568    }
1569}
1570
1571fn yaml_parse() -> ToolDefinition {
1572    ToolDefinition {
1573        name: "yaml_parse".into(),
1574        description: "Parse a YAML string and return it as JSON.".into(),
1575        input_schema: serde_json::json!({
1576            "type": "object",
1577            "properties": {
1578                "content": {
1579                    "type": "string",
1580                    "description": "The YAML string to parse."
1581                }
1582            },
1583            "required": ["content"]
1584        }),
1585        category: ToolCategory::Data,
1586    }
1587}
1588
1589fn regex_match() -> ToolDefinition {
1590    ToolDefinition {
1591        name: "regex_match".into(),
1592        description: "Match a regex pattern against text and return all captures.".into(),
1593        input_schema: serde_json::json!({
1594            "type": "object",
1595            "properties": {
1596                "pattern": {
1597                    "type": "string",
1598                    "description": "The regex pattern."
1599                },
1600                "text": {
1601                    "type": "string",
1602                    "description": "The text to match against."
1603                },
1604                "global": {
1605                    "type": "boolean",
1606                    "description": "Find all matches (true) or just the first (false). Default: false."
1607                }
1608            },
1609            "required": ["pattern", "text"]
1610        }),
1611        category: ToolCategory::Data,
1612    }
1613}
1614
1615fn regex_replace() -> ToolDefinition {
1616    ToolDefinition {
1617        name: "regex_replace".into(),
1618        description: "Find and replace text using a regex pattern with capture group support."
1619            .into(),
1620        input_schema: serde_json::json!({
1621            "type": "object",
1622            "properties": {
1623                "pattern": {
1624                    "type": "string",
1625                    "description": "The regex pattern to find."
1626                },
1627                "replacement": {
1628                    "type": "string",
1629                    "description": "The replacement string (supports $1, $2, etc. for captures)."
1630                },
1631                "text": {
1632                    "type": "string",
1633                    "description": "The text to perform replacement on."
1634                }
1635            },
1636            "required": ["pattern", "replacement", "text"]
1637        }),
1638        category: ToolCategory::Data,
1639    }
1640}
1641
1642// ---------------------------------------------------------------------------
1643// Process tools
1644// ---------------------------------------------------------------------------
1645
1646fn process_list() -> ToolDefinition {
1647    ToolDefinition {
1648        name: "process_list".into(),
1649        description: "List running processes with PID, name, and CPU/memory usage.".into(),
1650        input_schema: serde_json::json!({
1651            "type": "object",
1652            "properties": {
1653                "filter": {
1654                    "type": "string",
1655                    "description": "Optional filter string to match process names."
1656                }
1657            }
1658        }),
1659        category: ToolCategory::Shell,
1660    }
1661}
1662
1663fn process_kill() -> ToolDefinition {
1664    ToolDefinition {
1665        name: "process_kill".into(),
1666        description: "Kill a process by PID.".into(),
1667        input_schema: serde_json::json!({
1668            "type": "object",
1669            "properties": {
1670                "pid": {
1671                    "type": "integer",
1672                    "description": "The process ID to kill."
1673                },
1674                "signal": {
1675                    "type": "string",
1676                    "description": "Signal to send (e.g. 'TERM', 'KILL'). Default: 'TERM'."
1677                }
1678            },
1679            "required": ["pid"]
1680        }),
1681        category: ToolCategory::Shell,
1682    }
1683}
1684
1685// ---------------------------------------------------------------------------
1686// Schedule tools
1687// ---------------------------------------------------------------------------
1688
1689fn schedule_task() -> ToolDefinition {
1690    ToolDefinition {
1691        name: "schedule_task".into(),
1692        description: "Schedule a one-shot or recurring task. Returns a task ID.".into(),
1693        input_schema: serde_json::json!({
1694            "type": "object",
1695            "properties": {
1696                "name": {
1697                    "type": "string",
1698                    "description": "Human-readable name for the task."
1699                },
1700                "command": {
1701                    "type": "string",
1702                    "description": "Shell command to execute when the task fires."
1703                },
1704                "delay_secs": {
1705                    "type": "integer",
1706                    "description": "Delay in seconds before first execution."
1707                },
1708                "interval_secs": {
1709                    "type": "integer",
1710                    "description": "Interval in seconds for recurring execution. If omitted, the task runs once."
1711                }
1712            },
1713            "required": ["name", "command", "delay_secs"]
1714        }),
1715        category: ToolCategory::Schedule,
1716    }
1717}
1718
1719fn schedule_list() -> ToolDefinition {
1720    ToolDefinition {
1721        name: "schedule_list".into(),
1722        description: "List all scheduled tasks with their IDs, names, and status.".into(),
1723        input_schema: serde_json::json!({
1724            "type": "object",
1725            "properties": {}
1726        }),
1727        category: ToolCategory::Schedule,
1728    }
1729}
1730
1731fn schedule_cancel() -> ToolDefinition {
1732    ToolDefinition {
1733        name: "schedule_cancel".into(),
1734        description: "Cancel a scheduled task by its ID.".into(),
1735        input_schema: serde_json::json!({
1736            "type": "object",
1737            "properties": {
1738                "task_id": {
1739                    "type": "string",
1740                    "description": "The UUID of the task to cancel."
1741                }
1742            },
1743            "required": ["task_id"]
1744        }),
1745        category: ToolCategory::Schedule,
1746    }
1747}
1748
1749// ---------------------------------------------------------------------------
1750// Code analysis tools
1751// ---------------------------------------------------------------------------
1752
1753fn code_search() -> ToolDefinition {
1754    ToolDefinition {
1755        name: "code_search".into(),
1756        description: "Search for a regex pattern in files recursively. Returns matching lines with paths and line numbers.".into(),
1757        input_schema: serde_json::json!({
1758            "type": "object",
1759            "properties": {
1760                "pattern": {
1761                    "type": "string",
1762                    "description": "The regex pattern to search for."
1763                },
1764                "path": {
1765                    "type": "string",
1766                    "description": "Root directory to search in (default: working directory)."
1767                },
1768                "file_pattern": {
1769                    "type": "string",
1770                    "description": "Glob pattern to filter files (e.g. '*.rs', '*.py')."
1771                },
1772                "max_results": {
1773                    "type": "integer",
1774                    "description": "Maximum number of matches to return (default: 50)."
1775                }
1776            },
1777            "required": ["pattern"]
1778        }),
1779        category: ToolCategory::CodeAnalysis,
1780    }
1781}
1782
1783fn code_symbols() -> ToolDefinition {
1784    ToolDefinition {
1785        name: "code_symbols".into(),
1786        description: "Extract function, struct, class, and method definitions from a source file."
1787            .into(),
1788        input_schema: serde_json::json!({
1789            "type": "object",
1790            "properties": {
1791                "path": {
1792                    "type": "string",
1793                    "description": "Path to the source file to analyze."
1794                }
1795            },
1796            "required": ["path"]
1797        }),
1798        category: ToolCategory::CodeAnalysis,
1799    }
1800}
1801
1802// ---------------------------------------------------------------------------
1803// Archive tools
1804// ---------------------------------------------------------------------------
1805
1806fn archive_create() -> ToolDefinition {
1807    ToolDefinition {
1808        name: "archive_create".into(),
1809        description: "Create a tar.gz archive from a list of file or directory paths.".into(),
1810        input_schema: serde_json::json!({
1811            "type": "object",
1812            "properties": {
1813                "output_path": {
1814                    "type": "string",
1815                    "description": "Path for the output .tar.gz archive file."
1816                },
1817                "paths": {
1818                    "type": "array",
1819                    "items": { "type": "string" },
1820                    "description": "List of file or directory paths to include in the archive."
1821                }
1822            },
1823            "required": ["output_path", "paths"]
1824        }),
1825        category: ToolCategory::Archive,
1826    }
1827}
1828
1829fn archive_extract() -> ToolDefinition {
1830    ToolDefinition {
1831        name: "archive_extract".into(),
1832        description: "Extract a tar.gz archive to a destination directory.".into(),
1833        input_schema: serde_json::json!({
1834            "type": "object",
1835            "properties": {
1836                "archive_path": {
1837                    "type": "string",
1838                    "description": "Path to the .tar.gz archive to extract."
1839                },
1840                "destination": {
1841                    "type": "string",
1842                    "description": "Directory to extract the archive into."
1843                }
1844            },
1845            "required": ["archive_path", "destination"]
1846        }),
1847        category: ToolCategory::Archive,
1848    }
1849}
1850
1851fn archive_list() -> ToolDefinition {
1852    ToolDefinition {
1853        name: "archive_list".into(),
1854        description: "List the contents of a tar.gz archive without extracting.".into(),
1855        input_schema: serde_json::json!({
1856            "type": "object",
1857            "properties": {
1858                "archive_path": {
1859                    "type": "string",
1860                    "description": "Path to the .tar.gz archive to list."
1861                }
1862            },
1863            "required": ["archive_path"]
1864        }),
1865        category: ToolCategory::Archive,
1866    }
1867}
1868
1869// ---------------------------------------------------------------------------
1870// Template tools
1871// ---------------------------------------------------------------------------
1872
1873fn template_render() -> ToolDefinition {
1874    ToolDefinition {
1875        name: "template_render".into(),
1876        description: "Render a Handlebars-style template by substituting {{variable}} placeholders with provided values.".into(),
1877        input_schema: serde_json::json!({
1878            "type": "object",
1879            "properties": {
1880                "template": {
1881                    "type": "string",
1882                    "description": "The template string containing {{variable}} placeholders."
1883                },
1884                "variables": {
1885                    "type": "object",
1886                    "description": "Key-value pairs mapping variable names to their values."
1887                }
1888            },
1889            "required": ["template", "variables"]
1890        }),
1891        category: ToolCategory::Template,
1892    }
1893}
1894
1895// ---------------------------------------------------------------------------
1896// Crypto / Hash tools
1897// ---------------------------------------------------------------------------
1898
1899fn hash_compute() -> ToolDefinition {
1900    ToolDefinition {
1901        name: "hash_compute".into(),
1902        description: "Compute a cryptographic hash (SHA-256, SHA-512, or MD5) of a string or file."
1903            .into(),
1904        input_schema: serde_json::json!({
1905            "type": "object",
1906            "properties": {
1907                "algorithm": {
1908                    "type": "string",
1909                    "enum": ["sha256", "sha512", "md5"],
1910                    "description": "Hash algorithm to use. Default: sha256."
1911                },
1912                "input": {
1913                    "type": "string",
1914                    "description": "The string to hash (provide either this or 'file')."
1915                },
1916                "file": {
1917                    "type": "string",
1918                    "description": "Path to a file to hash (provide either this or 'input')."
1919                }
1920            }
1921        }),
1922        category: ToolCategory::Crypto,
1923    }
1924}
1925
1926fn hash_verify() -> ToolDefinition {
1927    ToolDefinition {
1928        name: "hash_verify".into(),
1929        description: "Verify that a hash matches an expected value.".into(),
1930        input_schema: serde_json::json!({
1931            "type": "object",
1932            "properties": {
1933                "algorithm": {
1934                    "type": "string",
1935                    "enum": ["sha256", "sha512", "md5"],
1936                    "description": "Hash algorithm to use. Default: sha256."
1937                },
1938                "input": {
1939                    "type": "string",
1940                    "description": "The string to hash (provide either this or 'file')."
1941                },
1942                "file": {
1943                    "type": "string",
1944                    "description": "Path to a file to hash (provide either this or 'input')."
1945                },
1946                "expected": {
1947                    "type": "string",
1948                    "description": "The expected hex-encoded hash value to compare against."
1949                }
1950            },
1951            "required": ["expected"]
1952        }),
1953        category: ToolCategory::Crypto,
1954    }
1955}
1956
1957// ---------------------------------------------------------------------------
1958// Environment tools
1959// ---------------------------------------------------------------------------
1960
1961fn env_get() -> ToolDefinition {
1962    ToolDefinition {
1963        name: "env_get".into(),
1964        description: "Get the value of an environment variable.".into(),
1965        input_schema: serde_json::json!({
1966            "type": "object",
1967            "properties": {
1968                "name": {
1969                    "type": "string",
1970                    "description": "The environment variable name."
1971                }
1972            },
1973            "required": ["name"]
1974        }),
1975        category: ToolCategory::Shell,
1976    }
1977}
1978
1979fn env_list() -> ToolDefinition {
1980    ToolDefinition {
1981        name: "env_list".into(),
1982        description: "List all environment variables, with optional prefix filter.".into(),
1983        input_schema: serde_json::json!({
1984            "type": "object",
1985            "properties": {
1986                "prefix": {
1987                    "type": "string",
1988                    "description": "Optional prefix to filter environment variable names by."
1989                }
1990            }
1991        }),
1992        category: ToolCategory::Shell,
1993    }
1994}
1995
1996// ---------------------------------------------------------------------------
1997// Text tools
1998// ---------------------------------------------------------------------------
1999
2000fn text_diff() -> ToolDefinition {
2001    ToolDefinition {
2002        name: "text_diff".into(),
2003        description: "Compute a unified diff between two text strings.".into(),
2004        input_schema: serde_json::json!({
2005            "type": "object",
2006            "properties": {
2007                "old_text": {
2008                    "type": "string",
2009                    "description": "The original text."
2010                },
2011                "new_text": {
2012                    "type": "string",
2013                    "description": "The modified text."
2014                },
2015                "label": {
2016                    "type": "string",
2017                    "description": "Optional label for the diff output (default: 'a' / 'b')."
2018                }
2019            },
2020            "required": ["old_text", "new_text"]
2021        }),
2022        category: ToolCategory::Data,
2023    }
2024}
2025
2026fn text_count() -> ToolDefinition {
2027    ToolDefinition {
2028        name: "text_count".into(),
2029        description: "Count lines, words, and characters in text.".into(),
2030        input_schema: serde_json::json!({
2031            "type": "object",
2032            "properties": {
2033                "text": {
2034                    "type": "string",
2035                    "description": "The text to count."
2036                }
2037            },
2038            "required": ["text"]
2039        }),
2040        category: ToolCategory::Data,
2041    }
2042}
2043
2044// ---------------------------------------------------------------------------
2045// File tools (extended)
2046// ---------------------------------------------------------------------------
2047
2048fn file_search() -> ToolDefinition {
2049    ToolDefinition {
2050        name: "file_search".into(),
2051        description: "Search for files by glob pattern recursively.".into(),
2052        input_schema: serde_json::json!({
2053            "type": "object",
2054            "properties": {
2055                "pattern": {
2056                    "type": "string",
2057                    "description": "Glob pattern to match file names (e.g. '*.rs', 'Cargo.*')."
2058                },
2059                "path": {
2060                    "type": "string",
2061                    "description": "Root directory to search in (default: working directory)."
2062                },
2063                "max_results": {
2064                    "type": "integer",
2065                    "description": "Maximum number of results to return (default: 100)."
2066                }
2067            },
2068            "required": ["pattern"]
2069        }),
2070        category: ToolCategory::FileSystem,
2071    }
2072}
2073
2074fn file_info() -> ToolDefinition {
2075    ToolDefinition {
2076        name: "file_info".into(),
2077        description: "Get file metadata: size, modified time, permissions, and type.".into(),
2078        input_schema: serde_json::json!({
2079            "type": "object",
2080            "properties": {
2081                "path": {
2082                    "type": "string",
2083                    "description": "Path to the file or directory to inspect."
2084                }
2085            },
2086            "required": ["path"]
2087        }),
2088        category: ToolCategory::FileSystem,
2089    }
2090}
2091
2092fn a2a_delegate() -> ToolDefinition {
2093    ToolDefinition {
2094        name: "a2a_delegate".into(),
2095        description: "Delegate a task to a remote A2A agent and return the result.".into(),
2096        input_schema: serde_json::json!({
2097            "type": "object",
2098            "properties": {
2099                "agent_url": {
2100                    "type": "string",
2101                    "description": "Base URL of the remote A2A agent (e.g. 'https://agent.example.com')."
2102                },
2103                "prompt": {
2104                    "type": "string",
2105                    "description": "The task description / prompt to send to the remote agent."
2106                },
2107                "context": {
2108                    "type": "object",
2109                    "description": "Optional additional context as key-value pairs."
2110                },
2111                "timeout_secs": {
2112                    "type": "integer",
2113                    "description": "Maximum time to wait for the task to complete (default: 60)."
2114                }
2115            },
2116            "required": ["agent_url", "prompt"]
2117        }),
2118        category: ToolCategory::Agent,
2119    }
2120}
2121
2122fn wasm_invoke() -> ToolDefinition {
2123    ToolDefinition {
2124        name: "wasm_invoke".into(),
2125        description: "Invoke a function on a loaded WASM plugin and return the result.".into(),
2126        input_schema: serde_json::json!({
2127            "type": "object",
2128            "properties": {
2129                "plugin": {
2130                    "type": "string",
2131                    "description": "Name of the loaded WASM plugin to invoke."
2132                },
2133                "function": {
2134                    "type": "string",
2135                    "description": "Name of the exported function to call within the plugin."
2136                },
2137                "input": {
2138                    "type": "object",
2139                    "description": "Input arguments to pass to the plugin function (optional)."
2140                }
2141            },
2142            "required": ["plugin", "function"]
2143        }),
2144        category: ToolCategory::Plugin,
2145    }
2146}
2147
2148fn channel_notify() -> ToolDefinition {
2149    ToolDefinition {
2150        name: "channel_notify".into(),
2151        description: "Send a message to an external channel (Telegram, Slack, Discord, etc.)."
2152            .into(),
2153        input_schema: serde_json::json!({
2154            "type": "object",
2155            "properties": {
2156                "channel": {
2157                    "type": "string",
2158                    "description": "The channel adapter name (e.g., \"telegram\", \"discord\", \"slack\")."
2159                },
2160                "chat_id": {
2161                    "type": "string",
2162                    "description": "The channel/conversation ID to send the message to."
2163                },
2164                "message": {
2165                    "type": "string",
2166                    "description": "The text message to send. Keep it concise and actionable."
2167                }
2168            },
2169            "required": ["channel", "chat_id", "message"]
2170        }),
2171        category: ToolCategory::Channel,
2172    }
2173}
2174
2175// ---------------------------------------------------------------------------
2176// Self-Configuration Tools
2177// ---------------------------------------------------------------------------
2178
2179fn heartbeat_add() -> ToolDefinition {
2180    ToolDefinition {
2181        name: "heartbeat_add".into(),
2182        description: "Add a recurring heartbeat task to your creed. Cadences: every_bout, on_wake, hourly, daily, weekly, cron.".into(),
2183        input_schema: serde_json::json!({
2184            "type": "object",
2185            "properties": {
2186                "task": {
2187                    "type": "string",
2188                    "description": "What to do when the heartbeat fires (e.g., \"Check email for important messages and notify user\")."
2189                },
2190                "cadence": {
2191                    "type": "string",
2192                    "description": "How often: every_bout, on_wake, hourly, daily, weekly, 'every 30m', 'every 2h', or cron like '*/10 * * * *'."
2193                }
2194            },
2195            "required": ["task", "cadence"]
2196        }),
2197        category: ToolCategory::Agent,
2198    }
2199}
2200
2201fn heartbeat_list() -> ToolDefinition {
2202    ToolDefinition {
2203        name: "heartbeat_list".into(),
2204        description: "List all heartbeat tasks in your creed with cadence and execution counts."
2205            .into(),
2206        input_schema: serde_json::json!({
2207            "type": "object",
2208            "properties": {},
2209            "required": []
2210        }),
2211        category: ToolCategory::Agent,
2212    }
2213}
2214
2215fn heartbeat_remove() -> ToolDefinition {
2216    ToolDefinition {
2217        name: "heartbeat_remove".into(),
2218        description: "Remove a heartbeat task from your creed by its 0-based index.".into(),
2219        input_schema: serde_json::json!({
2220            "type": "object",
2221            "properties": {
2222                "index": {
2223                    "type": "integer",
2224                    "description": "The 0-based index of the heartbeat task to remove."
2225                }
2226            },
2227            "required": ["index"]
2228        }),
2229        category: ToolCategory::Agent,
2230    }
2231}
2232
2233fn creed_view() -> ToolDefinition {
2234    ToolDefinition {
2235        name: "creed_view".into(),
2236        description: "View your current creed: identity, traits, directives, behaviors, relationships, and stats.".into(),
2237        input_schema: serde_json::json!({
2238            "type": "object",
2239            "properties": {},
2240            "required": []
2241        }),
2242        category: ToolCategory::Agent,
2243    }
2244}
2245
2246fn skill_list() -> ToolDefinition {
2247    ToolDefinition {
2248        name: "skill_list".into(),
2249        description: "List available skill packs: productivity, developer, research, files.".into(),
2250        input_schema: serde_json::json!({
2251            "type": "object",
2252            "properties": {},
2253            "required": []
2254        }),
2255        category: ToolCategory::Agent,
2256    }
2257}
2258
2259fn skill_recommend() -> ToolDefinition {
2260    ToolDefinition {
2261        name: "skill_recommend".into(),
2262        description: "Recommend a skill pack when the user needs capabilities you don't have (calendar, email, GitHub). Shows install command.".into(),
2263        input_schema: serde_json::json!({
2264            "type": "object",
2265            "properties": {
2266                "pack_name": {
2267                    "type": "string",
2268                    "description": "The skill pack name to recommend (e.g., \"productivity\", \"developer\", \"research\", \"files\")."
2269                }
2270            },
2271            "required": ["pack_name"]
2272        }),
2273        category: ToolCategory::Agent,
2274    }
2275}
2276
2277// ---------------------------------------------------------------------------
2278// Desktop automation tool definitions
2279// ---------------------------------------------------------------------------
2280
2281fn sys_screenshot() -> ToolDefinition {
2282    ToolDefinition {
2283        name: "sys_screenshot".into(),
2284        description:
2285            "Capture a screenshot of the full screen or a specific window. Returns base64 PNG."
2286                .into(),
2287        input_schema: serde_json::json!({
2288            "type": "object",
2289            "properties": {
2290                "window": {
2291                    "type": "string",
2292                    "description": "Optional window title to capture. If omitted, captures the full screen."
2293                }
2294            }
2295        }),
2296        category: ToolCategory::SystemAutomation,
2297    }
2298}
2299
2300fn ui_screenshot() -> ToolDefinition {
2301    ToolDefinition {
2302        name: "ui_screenshot".into(),
2303        description: "Capture a screenshot of a specific UI region by element ID or bounds.".into(),
2304        input_schema: serde_json::json!({
2305            "type": "object",
2306            "properties": {
2307                "element_id": {
2308                    "type": "string",
2309                    "description": "Element ID from ui_find_elements (e.g. \"Safari:3\"). Captures the region of that element."
2310                },
2311                "bounds": {
2312                    "type": "object",
2313                    "description": "Explicit bounds to capture: {x, y, width, height} in pixels.",
2314                    "properties": {
2315                        "x": {"type": "integer"},
2316                        "y": {"type": "integer"},
2317                        "width": {"type": "integer"},
2318                        "height": {"type": "integer"}
2319                    },
2320                    "required": ["x", "y", "width", "height"]
2321                }
2322            }
2323        }),
2324        category: ToolCategory::UiAutomation,
2325    }
2326}
2327
2328fn app_ocr() -> ToolDefinition {
2329    ToolDefinition {
2330        name: "app_ocr".into(),
2331        description: "Extract text from an app window using OCR. Returns plain text. Prefer over sys_screenshot for text extraction.".into(),
2332        input_schema: serde_json::json!({
2333            "type": "object",
2334            "properties": {
2335                "app": {
2336                    "type": "string",
2337                    "description": "Name of the application to OCR (e.g. \"Messages\", \"Safari\")."
2338                }
2339            },
2340            "required": ["app"]
2341        }),
2342        category: ToolCategory::AppIntegration,
2343    }
2344}
2345
2346fn ui_find_elements() -> ToolDefinition {
2347    ToolDefinition {
2348        name: "ui_find_elements".into(),
2349        description: "Query the accessibility tree of an app to find UI elements. Returns element IDs for ui_click/ui_type_text. Re-query after state changes — IDs are ephemeral.".into(),
2350        input_schema: serde_json::json!({
2351            "type": "object",
2352            "properties": {
2353                "app": {
2354                    "type": "string",
2355                    "description": "Name of the application to query (e.g. \"Messages\", \"Safari\")."
2356                },
2357                "role": {
2358                    "type": "string",
2359                    "description": "Optional: filter by accessibility role (e.g. \"button\", \"text field\", \"row\", \"menu item\")."
2360                },
2361                "label": {
2362                    "type": "string",
2363                    "description": "Optional: filter by accessibility label (substring match)."
2364                },
2365                "value": {
2366                    "type": "string",
2367                    "description": "Optional: filter by current value (substring match)."
2368                }
2369            },
2370            "required": ["app"]
2371        }),
2372        category: ToolCategory::UiAutomation,
2373    }
2374}
2375
2376fn ui_click() -> ToolDefinition {
2377    ToolDefinition {
2378        name: "ui_click".into(),
2379        description: "Click a UI element by its element ID from ui_find_elements.".into(),
2380        input_schema: serde_json::json!({
2381            "type": "object",
2382            "properties": {
2383                "element_id": {
2384                    "type": "string",
2385                    "description": "Element ID from ui_find_elements (e.g. \"Messages:0\")."
2386                }
2387            },
2388            "required": ["element_id"]
2389        }),
2390        category: ToolCategory::UiAutomation,
2391    }
2392}
2393
2394fn ui_type_text() -> ToolDefinition {
2395    ToolDefinition {
2396        name: "ui_type_text".into(),
2397        description: "Type text into a UI element by its element ID from ui_find_elements.".into(),
2398        input_schema: serde_json::json!({
2399            "type": "object",
2400            "properties": {
2401                "element_id": {
2402                    "type": "string",
2403                    "description": "Element ID of the text field (e.g. \"Messages:2\")."
2404                },
2405                "text": {
2406                    "type": "string",
2407                    "description": "The text to type into the element."
2408                }
2409            },
2410            "required": ["element_id", "text"]
2411        }),
2412        category: ToolCategory::UiAutomation,
2413    }
2414}
2415
2416fn ui_list_windows() -> ToolDefinition {
2417    ToolDefinition {
2418        name: "ui_list_windows".into(),
2419        description: "List all visible windows with their titles and owning apps.".into(),
2420        input_schema: serde_json::json!({
2421            "type": "object",
2422            "properties": {}
2423        }),
2424        category: ToolCategory::UiAutomation,
2425    }
2426}
2427
2428fn ui_read_attribute() -> ToolDefinition {
2429    ToolDefinition {
2430        name: "ui_read_attribute".into(),
2431        description:
2432            "Read an accessibility attribute (value, enabled, focused, etc.) from a UI element."
2433                .into(),
2434        input_schema: serde_json::json!({
2435            "type": "object",
2436            "properties": {
2437                "element_id": {
2438                    "type": "string",
2439                    "description": "Element ID from ui_find_elements (e.g. \"Safari:3\")."
2440                },
2441                "attribute": {
2442                    "type": "string",
2443                    "description": "The attribute to read. Allowed: value, name, role, role description, title, description, enabled, focused, position, size, selected, help, subrole, identifier, minimum value, maximum value, orientation, placeholder value."
2444                }
2445            },
2446            "required": ["element_id", "attribute"]
2447        }),
2448        category: ToolCategory::UiAutomation,
2449    }
2450}
2451
2452// ---------------------------------------------------------------------------
2453// Tests
2454// ---------------------------------------------------------------------------
2455
2456#[cfg(test)]
2457mod tests {
2458    use super::*;
2459
2460    #[test]
2461    fn test_browser_tool_definitions_correct() {
2462        let nav = browser_navigate();
2463        assert_eq!(nav.name, "browser_navigate");
2464        assert_eq!(nav.category, ToolCategory::Browser);
2465        assert!(
2466            nav.input_schema["required"]
2467                .as_array()
2468                .expect("required should be array")
2469                .iter()
2470                .any(|v| v == "url")
2471        );
2472
2473        let ss = browser_screenshot();
2474        assert_eq!(ss.name, "browser_screenshot");
2475        assert_eq!(ss.category, ToolCategory::Browser);
2476
2477        let click = browser_click();
2478        assert_eq!(click.name, "browser_click");
2479        assert!(
2480            click.input_schema["required"]
2481                .as_array()
2482                .expect("required should be array")
2483                .iter()
2484                .any(|v| v == "selector")
2485        );
2486
2487        let typ = browser_type();
2488        assert_eq!(typ.name, "browser_type");
2489        let required = typ.input_schema["required"]
2490            .as_array()
2491            .expect("required should be array");
2492        assert!(required.iter().any(|v| v == "selector"));
2493        assert!(required.iter().any(|v| v == "text"));
2494
2495        let content = browser_content();
2496        assert_eq!(content.name, "browser_content");
2497        assert_eq!(content.category, ToolCategory::Browser);
2498    }
2499
2500    #[test]
2501    fn test_browser_tools_require_browser_control_capability() {
2502        let caps = vec![Capability::BrowserControl];
2503        let tools = tools_for_capabilities(&caps);
2504
2505        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2506        assert!(
2507            names.contains(&"browser_navigate"),
2508            "missing browser_navigate"
2509        );
2510        assert!(
2511            names.contains(&"browser_screenshot"),
2512            "missing browser_screenshot"
2513        );
2514        assert!(names.contains(&"browser_click"), "missing browser_click");
2515        assert!(names.contains(&"browser_type"), "missing browser_type");
2516        assert!(
2517            names.contains(&"browser_content"),
2518            "missing browser_content"
2519        );
2520    }
2521
2522    #[test]
2523    fn test_browser_tools_absent_without_capability() {
2524        let caps = vec![Capability::Memory];
2525        let tools = tools_for_capabilities(&caps);
2526
2527        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2528        assert!(!names.iter().any(|n| n.starts_with("browser_")));
2529    }
2530
2531    #[test]
2532    fn test_all_tools_includes_browser() {
2533        let tools = all_tools();
2534        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2535        assert!(names.contains(&"browser_navigate"));
2536        assert!(names.contains(&"browser_screenshot"));
2537        assert!(names.contains(&"browser_click"));
2538        assert!(names.contains(&"browser_type"));
2539        assert!(names.contains(&"browser_content"));
2540    }
2541
2542    // -----------------------------------------------------------------------
2543    // Tool definition correctness tests
2544    // -----------------------------------------------------------------------
2545
2546    #[test]
2547    fn test_file_read_definition() {
2548        let t = file_read();
2549        assert_eq!(t.name, "file_read");
2550        assert_eq!(t.category, ToolCategory::FileSystem);
2551        assert_eq!(t.input_schema["type"], "object");
2552        let required = t.input_schema["required"].as_array().unwrap();
2553        assert!(required.iter().any(|v| v == "path"));
2554    }
2555
2556    #[test]
2557    fn test_file_write_definition() {
2558        let t = file_write();
2559        assert_eq!(t.name, "file_write");
2560        assert_eq!(t.category, ToolCategory::FileSystem);
2561        let required = t.input_schema["required"].as_array().unwrap();
2562        assert!(required.iter().any(|v| v == "path"));
2563        assert!(required.iter().any(|v| v == "content"));
2564    }
2565
2566    #[test]
2567    fn test_file_list_definition() {
2568        let t = file_list();
2569        assert_eq!(t.name, "file_list");
2570        assert_eq!(t.category, ToolCategory::FileSystem);
2571        assert_eq!(t.input_schema["type"], "object");
2572    }
2573
2574    #[test]
2575    fn test_file_search_definition() {
2576        let t = file_search();
2577        assert_eq!(t.name, "file_search");
2578        assert_eq!(t.category, ToolCategory::FileSystem);
2579        let required = t.input_schema["required"].as_array().unwrap();
2580        assert!(required.iter().any(|v| v == "pattern"));
2581    }
2582
2583    #[test]
2584    fn test_file_info_definition() {
2585        let t = file_info();
2586        assert_eq!(t.name, "file_info");
2587        assert_eq!(t.category, ToolCategory::FileSystem);
2588        let required = t.input_schema["required"].as_array().unwrap();
2589        assert!(required.iter().any(|v| v == "path"));
2590    }
2591
2592    #[test]
2593    fn test_patch_apply_definition() {
2594        let t = patch_apply();
2595        assert_eq!(t.name, "patch_apply");
2596        assert_eq!(t.category, ToolCategory::FileSystem);
2597        let required = t.input_schema["required"].as_array().unwrap();
2598        assert!(required.iter().any(|v| v == "path"));
2599        assert!(required.iter().any(|v| v == "diff"));
2600    }
2601
2602    #[test]
2603    fn test_shell_exec_definition() {
2604        let t = shell_exec();
2605        assert_eq!(t.name, "shell_exec");
2606        assert_eq!(t.category, ToolCategory::Shell);
2607        let required = t.input_schema["required"].as_array().unwrap();
2608        assert!(required.iter().any(|v| v == "command"));
2609    }
2610
2611    #[test]
2612    fn test_web_fetch_definition() {
2613        let t = web_fetch();
2614        assert_eq!(t.name, "web_fetch");
2615        assert_eq!(t.category, ToolCategory::Web);
2616        let required = t.input_schema["required"].as_array().unwrap();
2617        assert!(required.iter().any(|v| v == "url"));
2618    }
2619
2620    #[test]
2621    fn test_web_search_definition() {
2622        let t = web_search();
2623        assert_eq!(t.name, "web_search");
2624        assert_eq!(t.category, ToolCategory::Web);
2625        let required = t.input_schema["required"].as_array().unwrap();
2626        assert!(required.iter().any(|v| v == "query"));
2627    }
2628
2629    #[test]
2630    fn test_memory_store_definition() {
2631        let t = memory_store();
2632        assert_eq!(t.name, "memory_store");
2633        assert_eq!(t.category, ToolCategory::Memory);
2634        let required = t.input_schema["required"].as_array().unwrap();
2635        assert!(required.iter().any(|v| v == "key"));
2636        assert!(required.iter().any(|v| v == "value"));
2637    }
2638
2639    #[test]
2640    fn test_memory_recall_definition() {
2641        let t = memory_recall();
2642        assert_eq!(t.name, "memory_recall");
2643        assert_eq!(t.category, ToolCategory::Memory);
2644        let required = t.input_schema["required"].as_array().unwrap();
2645        assert!(required.iter().any(|v| v == "query"));
2646    }
2647
2648    #[test]
2649    fn test_knowledge_tools_definitions() {
2650        let ae = knowledge_add_entity();
2651        assert_eq!(ae.name, "knowledge_add_entity");
2652        assert_eq!(ae.category, ToolCategory::Knowledge);
2653        let required = ae.input_schema["required"].as_array().unwrap();
2654        assert!(required.iter().any(|v| v == "name"));
2655        assert!(required.iter().any(|v| v == "entity_type"));
2656
2657        let ar = knowledge_add_relation();
2658        assert_eq!(ar.name, "knowledge_add_relation");
2659        let required = ar.input_schema["required"].as_array().unwrap();
2660        assert!(required.iter().any(|v| v == "from"));
2661        assert!(required.iter().any(|v| v == "relation"));
2662        assert!(required.iter().any(|v| v == "to"));
2663
2664        let kq = knowledge_query();
2665        assert_eq!(kq.name, "knowledge_query");
2666        let required = kq.input_schema["required"].as_array().unwrap();
2667        assert!(required.iter().any(|v| v == "query"));
2668    }
2669
2670    #[test]
2671    fn test_agent_tools_definitions() {
2672        let spawn = agent_spawn();
2673        assert_eq!(spawn.name, "agent_spawn");
2674        assert_eq!(spawn.category, ToolCategory::Agent);
2675        let required = spawn.input_schema["required"].as_array().unwrap();
2676        assert!(required.iter().any(|v| v == "name"));
2677        assert!(required.iter().any(|v| v == "system_prompt"));
2678
2679        let msg = agent_message();
2680        assert_eq!(msg.name, "agent_message");
2681        assert_eq!(msg.category, ToolCategory::Agent);
2682        let required = msg.input_schema["required"].as_array().unwrap();
2683        assert!(required.iter().any(|v| v == "message"));
2684
2685        let list = agent_list();
2686        assert_eq!(list.name, "agent_list");
2687        assert_eq!(list.category, ToolCategory::Agent);
2688    }
2689
2690    #[test]
2691    fn test_git_tools_definitions() {
2692        let status = git_status();
2693        assert_eq!(status.name, "git_status");
2694        assert_eq!(status.category, ToolCategory::SourceControl);
2695
2696        let diff = git_diff();
2697        assert_eq!(diff.name, "git_diff");
2698
2699        let log = git_log();
2700        assert_eq!(log.name, "git_log");
2701
2702        let commit = git_commit();
2703        assert_eq!(commit.name, "git_commit");
2704        let required = commit.input_schema["required"].as_array().unwrap();
2705        assert!(required.iter().any(|v| v == "message"));
2706
2707        let branch = git_branch();
2708        assert_eq!(branch.name, "git_branch");
2709    }
2710
2711    #[test]
2712    fn test_docker_tools_definitions() {
2713        let ps = docker_ps();
2714        assert_eq!(ps.name, "docker_ps");
2715        assert_eq!(ps.category, ToolCategory::Container);
2716
2717        let run = docker_run();
2718        assert_eq!(run.name, "docker_run");
2719        let required = run.input_schema["required"].as_array().unwrap();
2720        assert!(required.iter().any(|v| v == "image"));
2721
2722        let build = docker_build();
2723        assert_eq!(build.name, "docker_build");
2724
2725        let logs = docker_logs();
2726        assert_eq!(logs.name, "docker_logs");
2727        let required = logs.input_schema["required"].as_array().unwrap();
2728        assert!(required.iter().any(|v| v == "container"));
2729    }
2730
2731    #[test]
2732    fn test_http_tools_definitions() {
2733        let req = http_request();
2734        assert_eq!(req.name, "http_request");
2735        assert_eq!(req.category, ToolCategory::Web);
2736        let required = req.input_schema["required"].as_array().unwrap();
2737        assert!(required.iter().any(|v| v == "url"));
2738
2739        let post = http_post();
2740        assert_eq!(post.name, "http_post");
2741        let required = post.input_schema["required"].as_array().unwrap();
2742        assert!(required.iter().any(|v| v == "url"));
2743        assert!(required.iter().any(|v| v == "json"));
2744    }
2745
2746    #[test]
2747    fn test_data_tools_definitions() {
2748        let jq = json_query();
2749        assert_eq!(jq.name, "json_query");
2750        assert_eq!(jq.category, ToolCategory::Data);
2751        let required = jq.input_schema["required"].as_array().unwrap();
2752        assert!(required.iter().any(|v| v == "data"));
2753        assert!(required.iter().any(|v| v == "path"));
2754
2755        let jt = json_transform();
2756        assert_eq!(jt.name, "json_transform");
2757        let required = jt.input_schema["required"].as_array().unwrap();
2758        assert!(required.iter().any(|v| v == "data"));
2759
2760        let yp = yaml_parse();
2761        assert_eq!(yp.name, "yaml_parse");
2762        let required = yp.input_schema["required"].as_array().unwrap();
2763        assert!(required.iter().any(|v| v == "content"));
2764
2765        let rm = regex_match();
2766        assert_eq!(rm.name, "regex_match");
2767        let required = rm.input_schema["required"].as_array().unwrap();
2768        assert!(required.iter().any(|v| v == "pattern"));
2769        assert!(required.iter().any(|v| v == "text"));
2770
2771        let rr = regex_replace();
2772        assert_eq!(rr.name, "regex_replace");
2773        let required = rr.input_schema["required"].as_array().unwrap();
2774        assert!(required.iter().any(|v| v == "pattern"));
2775        assert!(required.iter().any(|v| v == "replacement"));
2776        assert!(required.iter().any(|v| v == "text"));
2777    }
2778
2779    #[test]
2780    fn test_process_tools_definitions() {
2781        let pl = process_list();
2782        assert_eq!(pl.name, "process_list");
2783        assert_eq!(pl.category, ToolCategory::Shell);
2784
2785        let pk = process_kill();
2786        assert_eq!(pk.name, "process_kill");
2787        let required = pk.input_schema["required"].as_array().unwrap();
2788        assert!(required.iter().any(|v| v == "pid"));
2789    }
2790
2791    #[test]
2792    fn test_schedule_tools_definitions() {
2793        let st = schedule_task();
2794        assert_eq!(st.name, "schedule_task");
2795        assert_eq!(st.category, ToolCategory::Schedule);
2796        let required = st.input_schema["required"].as_array().unwrap();
2797        assert!(required.iter().any(|v| v == "name"));
2798        assert!(required.iter().any(|v| v == "command"));
2799        assert!(required.iter().any(|v| v == "delay_secs"));
2800
2801        let sl = schedule_list();
2802        assert_eq!(sl.name, "schedule_list");
2803
2804        let sc = schedule_cancel();
2805        assert_eq!(sc.name, "schedule_cancel");
2806        let required = sc.input_schema["required"].as_array().unwrap();
2807        assert!(required.iter().any(|v| v == "task_id"));
2808    }
2809
2810    #[test]
2811    fn test_code_analysis_tools_definitions() {
2812        let cs = code_search();
2813        assert_eq!(cs.name, "code_search");
2814        assert_eq!(cs.category, ToolCategory::CodeAnalysis);
2815        let required = cs.input_schema["required"].as_array().unwrap();
2816        assert!(required.iter().any(|v| v == "pattern"));
2817
2818        let sym = code_symbols();
2819        assert_eq!(sym.name, "code_symbols");
2820        let required = sym.input_schema["required"].as_array().unwrap();
2821        assert!(required.iter().any(|v| v == "path"));
2822    }
2823
2824    #[test]
2825    fn test_archive_tools_definitions() {
2826        let ac = archive_create();
2827        assert_eq!(ac.name, "archive_create");
2828        assert_eq!(ac.category, ToolCategory::Archive);
2829        let required = ac.input_schema["required"].as_array().unwrap();
2830        assert!(required.iter().any(|v| v == "output_path"));
2831        assert!(required.iter().any(|v| v == "paths"));
2832
2833        let ae = archive_extract();
2834        assert_eq!(ae.name, "archive_extract");
2835        let required = ae.input_schema["required"].as_array().unwrap();
2836        assert!(required.iter().any(|v| v == "archive_path"));
2837        assert!(required.iter().any(|v| v == "destination"));
2838
2839        let al = archive_list();
2840        assert_eq!(al.name, "archive_list");
2841        let required = al.input_schema["required"].as_array().unwrap();
2842        assert!(required.iter().any(|v| v == "archive_path"));
2843    }
2844
2845    #[test]
2846    fn test_template_render_definition() {
2847        let t = template_render();
2848        assert_eq!(t.name, "template_render");
2849        assert_eq!(t.category, ToolCategory::Template);
2850        let required = t.input_schema["required"].as_array().unwrap();
2851        assert!(required.iter().any(|v| v == "template"));
2852        assert!(required.iter().any(|v| v == "variables"));
2853    }
2854
2855    #[test]
2856    fn test_crypto_tools_definitions() {
2857        let hc = hash_compute();
2858        assert_eq!(hc.name, "hash_compute");
2859        assert_eq!(hc.category, ToolCategory::Crypto);
2860
2861        let hv = hash_verify();
2862        assert_eq!(hv.name, "hash_verify");
2863        let required = hv.input_schema["required"].as_array().unwrap();
2864        assert!(required.iter().any(|v| v == "expected"));
2865    }
2866
2867    #[test]
2868    fn test_env_tools_definitions() {
2869        let eg = env_get();
2870        assert_eq!(eg.name, "env_get");
2871        assert_eq!(eg.category, ToolCategory::Shell);
2872        let required = eg.input_schema["required"].as_array().unwrap();
2873        assert!(required.iter().any(|v| v == "name"));
2874
2875        let el = env_list();
2876        assert_eq!(el.name, "env_list");
2877    }
2878
2879    #[test]
2880    fn test_text_tools_definitions() {
2881        let td = text_diff();
2882        assert_eq!(td.name, "text_diff");
2883        assert_eq!(td.category, ToolCategory::Data);
2884        let required = td.input_schema["required"].as_array().unwrap();
2885        assert!(required.iter().any(|v| v == "old_text"));
2886        assert!(required.iter().any(|v| v == "new_text"));
2887
2888        let tc = text_count();
2889        assert_eq!(tc.name, "text_count");
2890        let required = tc.input_schema["required"].as_array().unwrap();
2891        assert!(required.iter().any(|v| v == "text"));
2892    }
2893
2894    // -----------------------------------------------------------------------
2895    // tools_for_capabilities tests per capability variant
2896    // -----------------------------------------------------------------------
2897
2898    #[test]
2899    fn test_tools_for_file_read_capability() {
2900        let caps = vec![Capability::FileRead("**".into())];
2901        let tools = tools_for_capabilities(&caps);
2902        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2903        assert!(names.contains(&"file_read"));
2904        assert!(names.contains(&"file_list"));
2905        assert!(names.contains(&"file_search"));
2906        assert!(names.contains(&"file_info"));
2907        assert!(!names.contains(&"file_write"));
2908    }
2909
2910    #[test]
2911    fn test_tools_for_file_write_capability() {
2912        let caps = vec![Capability::FileWrite("**".into())];
2913        let tools = tools_for_capabilities(&caps);
2914        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2915        assert!(names.contains(&"file_write"));
2916        assert!(names.contains(&"patch_apply"));
2917        assert!(!names.contains(&"file_read"));
2918    }
2919
2920    #[test]
2921    fn test_tools_for_shell_exec_capability() {
2922        let caps = vec![Capability::ShellExec("*".into())];
2923        let tools = tools_for_capabilities(&caps);
2924        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2925        assert!(names.contains(&"shell_exec"));
2926        assert!(names.contains(&"process_list"));
2927        assert!(names.contains(&"process_kill"));
2928        assert!(names.contains(&"env_get"));
2929        assert!(names.contains(&"env_list"));
2930    }
2931
2932    #[test]
2933    fn test_tools_for_network_capability() {
2934        let caps = vec![Capability::Network("*".into())];
2935        let tools = tools_for_capabilities(&caps);
2936        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2937        assert!(names.contains(&"web_fetch"));
2938        assert!(names.contains(&"web_search"));
2939        assert!(names.contains(&"http_request"));
2940        assert!(names.contains(&"http_post"));
2941    }
2942
2943    #[test]
2944    fn test_tools_for_memory_capability() {
2945        let caps = vec![Capability::Memory];
2946        let tools = tools_for_capabilities(&caps);
2947        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2948        assert!(names.contains(&"memory_store"));
2949        assert!(names.contains(&"memory_recall"));
2950        assert_eq!(names.len(), 2);
2951    }
2952
2953    #[test]
2954    fn test_tools_for_knowledge_graph_capability() {
2955        let caps = vec![Capability::KnowledgeGraph];
2956        let tools = tools_for_capabilities(&caps);
2957        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2958        assert!(names.contains(&"knowledge_add_entity"));
2959        assert!(names.contains(&"knowledge_add_relation"));
2960        assert!(names.contains(&"knowledge_query"));
2961        assert_eq!(names.len(), 3);
2962    }
2963
2964    #[test]
2965    fn test_tools_for_agent_spawn_capability() {
2966        let caps = vec![Capability::AgentSpawn];
2967        let tools = tools_for_capabilities(&caps);
2968        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2969        assert!(names.contains(&"agent_spawn"));
2970        assert_eq!(names.len(), 1);
2971    }
2972
2973    #[test]
2974    fn test_tools_for_agent_message_capability() {
2975        let caps = vec![Capability::AgentMessage];
2976        let tools = tools_for_capabilities(&caps);
2977        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2978        assert!(names.contains(&"agent_message"));
2979        assert!(names.contains(&"agent_list"));
2980        assert_eq!(names.len(), 2);
2981    }
2982
2983    #[test]
2984    fn test_tools_for_source_control_capability() {
2985        let caps = vec![Capability::SourceControl];
2986        let tools = tools_for_capabilities(&caps);
2987        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2988        assert!(names.contains(&"git_status"));
2989        assert!(names.contains(&"git_diff"));
2990        assert!(names.contains(&"git_log"));
2991        assert!(names.contains(&"git_commit"));
2992        assert!(names.contains(&"git_branch"));
2993        assert_eq!(names.len(), 5);
2994    }
2995
2996    #[test]
2997    fn test_tools_for_container_capability() {
2998        let caps = vec![Capability::Container];
2999        let tools = tools_for_capabilities(&caps);
3000        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3001        assert!(names.contains(&"docker_ps"));
3002        assert!(names.contains(&"docker_run"));
3003        assert!(names.contains(&"docker_build"));
3004        assert!(names.contains(&"docker_logs"));
3005        assert_eq!(names.len(), 4);
3006    }
3007
3008    #[test]
3009    fn test_tools_for_data_manipulation_capability() {
3010        let caps = vec![Capability::DataManipulation];
3011        let tools = tools_for_capabilities(&caps);
3012        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3013        assert!(names.contains(&"json_query"));
3014        assert!(names.contains(&"json_transform"));
3015        assert!(names.contains(&"yaml_parse"));
3016        assert!(names.contains(&"regex_match"));
3017        assert!(names.contains(&"regex_replace"));
3018        assert!(names.contains(&"text_diff"));
3019        assert!(names.contains(&"text_count"));
3020        assert_eq!(names.len(), 7);
3021    }
3022
3023    #[test]
3024    fn test_tools_for_schedule_capability() {
3025        let caps = vec![Capability::Schedule];
3026        let tools = tools_for_capabilities(&caps);
3027        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3028        assert!(names.contains(&"schedule_task"));
3029        assert!(names.contains(&"schedule_list"));
3030        assert!(names.contains(&"schedule_cancel"));
3031        assert_eq!(names.len(), 3);
3032    }
3033
3034    #[test]
3035    fn test_tools_for_code_analysis_capability() {
3036        let caps = vec![Capability::CodeAnalysis];
3037        let tools = tools_for_capabilities(&caps);
3038        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3039        assert!(names.contains(&"code_search"));
3040        assert!(names.contains(&"code_symbols"));
3041        assert_eq!(names.len(), 2);
3042    }
3043
3044    #[test]
3045    fn test_tools_for_archive_capability() {
3046        let caps = vec![Capability::Archive];
3047        let tools = tools_for_capabilities(&caps);
3048        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3049        assert!(names.contains(&"archive_create"));
3050        assert!(names.contains(&"archive_extract"));
3051        assert!(names.contains(&"archive_list"));
3052        assert_eq!(names.len(), 3);
3053    }
3054
3055    #[test]
3056    fn test_tools_for_template_capability() {
3057        let caps = vec![Capability::Template];
3058        let tools = tools_for_capabilities(&caps);
3059        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3060        assert!(names.contains(&"template_render"));
3061        assert_eq!(names.len(), 1);
3062    }
3063
3064    #[test]
3065    fn test_tools_for_crypto_capability() {
3066        let caps = vec![Capability::Crypto];
3067        let tools = tools_for_capabilities(&caps);
3068        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3069        assert!(names.contains(&"hash_compute"));
3070        assert!(names.contains(&"hash_verify"));
3071        assert_eq!(names.len(), 2);
3072    }
3073
3074    #[test]
3075    fn test_tools_for_empty_capabilities() {
3076        let caps: Vec<Capability> = vec![];
3077        let tools = tools_for_capabilities(&caps);
3078        assert!(tools.is_empty());
3079    }
3080
3081    #[test]
3082    fn test_tools_for_event_publish_returns_empty() {
3083        // EventPublish is in the _ => {} catch-all
3084        let caps = vec![Capability::EventPublish];
3085        let tools = tools_for_capabilities(&caps);
3086        assert!(tools.is_empty());
3087    }
3088
3089    // -----------------------------------------------------------------------
3090    // push_unique dedup tests
3091    // -----------------------------------------------------------------------
3092
3093    #[test]
3094    fn test_push_unique_dedup() {
3095        let mut tools = Vec::new();
3096        push_unique(&mut tools, file_read());
3097        push_unique(&mut tools, file_read());
3098        push_unique(&mut tools, file_read());
3099        assert_eq!(tools.len(), 1);
3100    }
3101
3102    #[test]
3103    fn test_push_unique_different_tools() {
3104        let mut tools = Vec::new();
3105        push_unique(&mut tools, file_read());
3106        push_unique(&mut tools, file_write());
3107        push_unique(&mut tools, shell_exec());
3108        assert_eq!(tools.len(), 3);
3109    }
3110
3111    #[test]
3112    fn test_tools_for_multiple_capabilities_dedup() {
3113        // FileRead + FileRead should not double-add tools
3114        let caps = vec![
3115            Capability::FileRead("src/**".into()),
3116            Capability::FileRead("tests/**".into()),
3117        ];
3118        let tools = tools_for_capabilities(&caps);
3119        let file_read_count = tools.iter().filter(|t| t.name == "file_read").count();
3120        assert_eq!(file_read_count, 1);
3121    }
3122
3123    // -----------------------------------------------------------------------
3124    // all_tools tests
3125    // -----------------------------------------------------------------------
3126
3127    #[test]
3128    fn test_all_tools_count() {
3129        let tools = all_tools();
3130        // Count the items in the vec literal in all_tools()
3131        assert!(
3132            tools.len() >= 50,
3133            "expected at least 50 tools, got {}",
3134            tools.len()
3135        );
3136    }
3137
3138    #[test]
3139    fn test_all_tools_unique_names() {
3140        let tools = all_tools();
3141        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3142        let original_len = names.len();
3143        names.sort();
3144        names.dedup();
3145        assert_eq!(names.len(), original_len, "all_tools has duplicate names");
3146    }
3147
3148    #[test]
3149    fn test_all_tools_valid_schemas() {
3150        let tools = all_tools();
3151        for tool in &tools {
3152            assert_eq!(
3153                tool.input_schema["type"], "object",
3154                "tool {} has non-object schema",
3155                tool.name
3156            );
3157            assert!(!tool.name.is_empty(), "tool has empty name");
3158            assert!(
3159                !tool.description.is_empty(),
3160                "tool {} has empty description",
3161                tool.name
3162            );
3163        }
3164    }
3165
3166    #[test]
3167    fn test_a2a_delegate_tool_definition() {
3168        let tool = a2a_delegate();
3169        assert_eq!(tool.name, "a2a_delegate");
3170        assert_eq!(tool.category, ToolCategory::Agent);
3171        let required = tool.input_schema["required"]
3172            .as_array()
3173            .expect("required should be array");
3174        assert!(required.iter().any(|v| v == "agent_url"));
3175        assert!(required.iter().any(|v| v == "prompt"));
3176    }
3177
3178    #[test]
3179    fn test_tools_for_a2a_delegate_capability() {
3180        let caps = vec![Capability::A2ADelegate];
3181        let tools = tools_for_capabilities(&caps);
3182        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3183        assert!(names.contains(&"a2a_delegate"));
3184        assert_eq!(names.len(), 1);
3185    }
3186
3187    #[test]
3188    fn test_all_tools_includes_a2a_delegate() {
3189        let tools = all_tools();
3190        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3191        assert!(names.contains(&"a2a_delegate"));
3192    }
3193
3194    #[test]
3195    fn test_wasm_invoke_tool_definition() {
3196        let tool = wasm_invoke();
3197        assert_eq!(tool.name, "wasm_invoke");
3198        assert_eq!(tool.category, ToolCategory::Plugin);
3199        let required = tool.input_schema["required"]
3200            .as_array()
3201            .expect("required should be array");
3202        assert!(required.iter().any(|v| v == "plugin"));
3203        assert!(required.iter().any(|v| v == "function"));
3204    }
3205
3206    #[test]
3207    fn test_tools_for_plugin_invoke_capability() {
3208        let caps = vec![Capability::PluginInvoke];
3209        let tools = tools_for_capabilities(&caps);
3210        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3211        assert!(names.contains(&"wasm_invoke"));
3212        assert_eq!(names.len(), 1);
3213    }
3214
3215    #[test]
3216    fn test_all_tools_includes_wasm_invoke() {
3217        let tools = all_tools();
3218        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3219        assert!(names.contains(&"wasm_invoke"));
3220    }
3221
3222    // -----------------------------------------------------------------------
3223    // Self-configuration tool definition tests
3224    // -----------------------------------------------------------------------
3225
3226    #[test]
3227    fn test_self_config_tools_registered() {
3228        let caps = vec![Capability::SelfConfig];
3229        let tools = tools_for_capabilities(&caps);
3230        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3231        assert!(names.contains(&"heartbeat_add"), "missing heartbeat_add");
3232        assert!(names.contains(&"heartbeat_list"), "missing heartbeat_list");
3233        assert!(
3234            names.contains(&"heartbeat_remove"),
3235            "missing heartbeat_remove"
3236        );
3237        assert!(names.contains(&"creed_view"), "missing creed_view");
3238        assert!(names.contains(&"skill_list"), "missing skill_list");
3239        assert!(
3240            names.contains(&"skill_recommend"),
3241            "missing skill_recommend"
3242        );
3243        assert_eq!(names.len(), 6);
3244    }
3245
3246    #[test]
3247    fn test_self_config_tools_absent_without_capability() {
3248        let caps = vec![Capability::Memory];
3249        let tools = tools_for_capabilities(&caps);
3250        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3251        assert!(!names.contains(&"heartbeat_add"));
3252        assert!(!names.contains(&"creed_view"));
3253        assert!(!names.contains(&"skill_list"));
3254    }
3255
3256    #[test]
3257    fn test_heartbeat_add_definition() {
3258        let t = heartbeat_add();
3259        assert_eq!(t.name, "heartbeat_add");
3260        assert_eq!(t.category, ToolCategory::Agent);
3261        let required = t.input_schema["required"].as_array().unwrap();
3262        assert!(required.iter().any(|v| v == "task"));
3263        assert!(required.iter().any(|v| v == "cadence"));
3264    }
3265
3266    #[test]
3267    fn test_heartbeat_remove_definition() {
3268        let t = heartbeat_remove();
3269        assert_eq!(t.name, "heartbeat_remove");
3270        let required = t.input_schema["required"].as_array().unwrap();
3271        assert!(required.iter().any(|v| v == "index"));
3272    }
3273
3274    #[test]
3275    fn test_skill_recommend_definition() {
3276        let t = skill_recommend();
3277        assert_eq!(t.name, "skill_recommend");
3278        let required = t.input_schema["required"].as_array().unwrap();
3279        assert!(required.iter().any(|v| v == "pack_name"));
3280    }
3281
3282    #[test]
3283    fn test_all_tools_includes_self_config() {
3284        let tools = all_tools();
3285        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3286        assert!(names.contains(&"heartbeat_add"));
3287        assert!(names.contains(&"heartbeat_list"));
3288        assert!(names.contains(&"heartbeat_remove"));
3289        assert!(names.contains(&"creed_view"));
3290        assert!(names.contains(&"skill_list"));
3291        assert!(names.contains(&"skill_recommend"));
3292    }
3293
3294    // --- Desktop automation tool definition tests ---
3295
3296    #[test]
3297    fn test_sys_screenshot_definition() {
3298        let t = sys_screenshot();
3299        assert_eq!(t.name, "sys_screenshot");
3300        assert_eq!(t.category, ToolCategory::SystemAutomation);
3301        // window is optional (not in required)
3302        assert!(t.input_schema.get("required").is_none());
3303    }
3304
3305    #[test]
3306    fn test_ui_screenshot_definition() {
3307        let t = ui_screenshot();
3308        assert_eq!(t.name, "ui_screenshot");
3309        assert_eq!(t.category, ToolCategory::UiAutomation);
3310    }
3311
3312    #[test]
3313    fn test_app_ocr_definition() {
3314        let t = app_ocr();
3315        assert_eq!(t.name, "app_ocr");
3316        assert_eq!(t.category, ToolCategory::AppIntegration);
3317        let required = t.input_schema["required"].as_array().unwrap();
3318        assert!(required.iter().any(|v| v == "app"));
3319    }
3320
3321    #[test]
3322    fn test_ui_find_elements_definition() {
3323        let t = ui_find_elements();
3324        assert_eq!(t.name, "ui_find_elements");
3325        assert_eq!(t.category, ToolCategory::UiAutomation);
3326        let required = t.input_schema["required"].as_array().unwrap();
3327        assert!(required.iter().any(|v| v == "app"));
3328    }
3329
3330    #[test]
3331    fn test_ui_click_definition() {
3332        let t = ui_click();
3333        assert_eq!(t.name, "ui_click");
3334        assert_eq!(t.category, ToolCategory::UiAutomation);
3335        let required = t.input_schema["required"].as_array().unwrap();
3336        assert!(required.iter().any(|v| v == "element_id"));
3337    }
3338
3339    #[test]
3340    fn test_ui_type_text_definition() {
3341        let t = ui_type_text();
3342        assert_eq!(t.name, "ui_type_text");
3343        assert_eq!(t.category, ToolCategory::UiAutomation);
3344        let required = t.input_schema["required"].as_array().unwrap();
3345        assert!(required.iter().any(|v| v == "element_id"));
3346        assert!(required.iter().any(|v| v == "text"));
3347    }
3348
3349    #[test]
3350    fn test_ui_list_windows_definition() {
3351        let t = ui_list_windows();
3352        assert_eq!(t.name, "ui_list_windows");
3353        assert_eq!(t.category, ToolCategory::UiAutomation);
3354    }
3355
3356    #[test]
3357    fn test_ui_read_attribute_definition() {
3358        let t = ui_read_attribute();
3359        assert_eq!(t.name, "ui_read_attribute");
3360        assert_eq!(t.category, ToolCategory::UiAutomation);
3361        let required = t.input_schema["required"].as_array().unwrap();
3362        assert!(required.iter().any(|v| v == "element_id"));
3363        assert!(required.iter().any(|v| v == "attribute"));
3364    }
3365
3366    #[test]
3367    fn test_all_tools_includes_automation() {
3368        let tools = all_tools();
3369        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3370        assert!(names.contains(&"sys_screenshot"));
3371        assert!(names.contains(&"ui_screenshot"));
3372        assert!(names.contains(&"app_ocr"));
3373        assert!(names.contains(&"ui_find_elements"));
3374        assert!(names.contains(&"ui_click"));
3375        assert!(names.contains(&"ui_type_text"));
3376        assert!(names.contains(&"ui_list_windows"));
3377        assert!(names.contains(&"ui_read_attribute"));
3378    }
3379
3380    #[test]
3381    fn test_tools_for_system_automation_capability() {
3382        let tools = tools_for_capabilities(&[Capability::SystemAutomation]);
3383        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3384        assert!(names.contains(&"sys_screenshot"));
3385        assert!(!names.contains(&"ui_click")); // not included without UiAutomation
3386    }
3387
3388    #[test]
3389    fn test_tools_for_ui_automation_capability() {
3390        let tools = tools_for_capabilities(&[Capability::UiAutomation("*".to_string())]);
3391        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3392        assert!(names.contains(&"ui_find_elements"));
3393        assert!(names.contains(&"ui_click"));
3394        assert!(names.contains(&"ui_type_text"));
3395        assert!(names.contains(&"ui_list_windows"));
3396        assert!(names.contains(&"ui_read_attribute"));
3397        assert!(names.contains(&"ui_screenshot"));
3398        assert!(!names.contains(&"sys_screenshot")); // not included without SystemAutomation
3399        assert!(!names.contains(&"app_ocr")); // not included without AppIntegration
3400    }
3401
3402    #[test]
3403    fn test_tools_for_app_integration_capability() {
3404        let tools = tools_for_capabilities(&[Capability::AppIntegration("*".to_string())]);
3405        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3406        assert!(names.contains(&"app_ocr"));
3407        assert!(!names.contains(&"ui_click")); // not included without UiAutomation
3408    }
3409
3410    #[test]
3411    fn test_automation_tool_no_duplicates() {
3412        // Granting all automation capabilities should not produce duplicate tools.
3413        let tools = tools_for_capabilities(&[
3414            Capability::SystemAutomation,
3415            Capability::UiAutomation("*".to_string()),
3416            Capability::AppIntegration("*".to_string()),
3417        ]);
3418        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3419        let before = names.len();
3420        names.sort();
3421        names.dedup();
3422        assert_eq!(before, names.len(), "duplicate tools found");
3423    }
3424
3425    // -----------------------------------------------------------------------
3426    // ToolSelector tests
3427    // -----------------------------------------------------------------------
3428
3429    use super::{ToolGroup, ToolSelector};
3430    use punch_types::{Message, Role};
3431
3432    fn full_caps() -> Vec<Capability> {
3433        Capability::full_access()
3434    }
3435
3436    fn msg(role: Role, text: &str) -> Message {
3437        Message::new(role, text)
3438    }
3439
3440    #[test]
3441    fn test_selector_core_tools_always_present() {
3442        let mut sel = ToolSelector::new(&full_caps());
3443        let messages = vec![msg(Role::User, "hello, how are you?")];
3444        let (tools, _) = sel.select_tools(&messages);
3445
3446        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3447        // Core tools must be present.
3448        assert!(names.contains(&"file_read"), "file_read missing");
3449        assert!(names.contains(&"shell_exec"), "shell_exec missing");
3450        assert!(names.contains(&"web_fetch"), "web_fetch missing");
3451        assert!(names.contains(&"memory_store"), "memory_store missing");
3452        // Auto-activated groups (granted capability, no core overlap) should also be present.
3453        assert!(
3454            names.contains(&"git_status"),
3455            "git tools should auto-activate for SourceControl capability"
3456        );
3457        assert!(
3458            names.contains(&"docker_ps"),
3459            "docker tools should auto-activate for Container capability"
3460        );
3461    }
3462
3463    #[test]
3464    fn test_selector_no_auto_activate_without_capability() {
3465        // A fighter with only FileRead + ShellExec should NOT get git or docker tools.
3466        let caps = vec![
3467            Capability::FileRead("**".to_string()),
3468            Capability::ShellExec("*".to_string()),
3469        ];
3470        let mut sel = ToolSelector::new(&caps);
3471        let messages = vec![msg(Role::User, "hello")];
3472        let (tools, _) = sel.select_tools(&messages);
3473
3474        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3475        assert!(
3476            !names.contains(&"git_status"),
3477            "git should not auto-activate without SourceControl"
3478        );
3479        assert!(
3480            !names.contains(&"docker_ps"),
3481            "docker should not auto-activate without Container"
3482        );
3483    }
3484
3485    #[test]
3486    fn test_selector_keyword_activates_group() {
3487        let mut sel = ToolSelector::new(&full_caps());
3488        let messages = vec![msg(Role::User, "please commit my changes to git")];
3489        let (tools, _) = sel.select_tools(&messages);
3490
3491        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3492        assert!(
3493            names.contains(&"git_status"),
3494            "git_status should be activated"
3495        );
3496        assert!(
3497            names.contains(&"git_commit"),
3498            "git_commit should be activated"
3499        );
3500        assert!(names.contains(&"git_diff"), "git_diff should be activated");
3501    }
3502
3503    #[test]
3504    fn test_selector_multiple_groups_activate() {
3505        let mut sel = ToolSelector::new(&full_caps());
3506        let messages = vec![msg(
3507            Role::User,
3508            "build the docker image and then commit it to git",
3509        )];
3510        let (tools, _) = sel.select_tools(&messages);
3511
3512        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3513        assert!(names.contains(&"git_status"), "git should activate");
3514        assert!(names.contains(&"docker_build"), "docker should activate");
3515    }
3516
3517    #[test]
3518    fn test_selector_capability_gating() {
3519        // No capabilities granted — no contextual tools should activate.
3520        let mut sel = ToolSelector::new(&[]);
3521        let messages = vec![msg(Role::User, "git commit and docker build")];
3522        let (tools, _) = sel.select_tools(&messages);
3523
3524        assert!(tools.is_empty(), "no tools without capabilities");
3525    }
3526
3527    #[test]
3528    fn test_selector_monotonic_growth() {
3529        let mut sel = ToolSelector::new(&full_caps());
3530
3531        // Turn 1: activate git tools.
3532        let msgs1 = vec![msg(Role::User, "commit my changes")];
3533        let (tools1, _) = sel.select_tools(&msgs1);
3534        let count1 = tools1.len();
3535
3536        // Turn 2: no git keywords, but git tools should persist.
3537        let msgs2 = vec![
3538            msg(Role::User, "commit my changes"),
3539            msg(Role::Assistant, "done"),
3540            msg(Role::User, "now tell me a joke"),
3541        ];
3542        let (tools2, _) = sel.select_tools(&msgs2);
3543
3544        let names2: Vec<&str> = tools2.iter().map(|t| t.name.as_str()).collect();
3545        assert!(
3546            names2.contains(&"git_status"),
3547            "git tools should persist across turns"
3548        );
3549        assert!(tools2.len() >= count1, "tool count should not decrease");
3550    }
3551
3552    #[test]
3553    fn test_selector_tools_changed_detection() {
3554        // Use Archive + Crypto (not auto-activated) to test change detection.
3555        let caps = vec![Capability::Archive, Capability::Crypto];
3556        let mut sel = ToolSelector::new(&caps);
3557
3558        let msgs1 = vec![msg(Role::User, "hello")];
3559        let (_, changed1) = sel.select_tools(&msgs1);
3560        // First call always reports changed (hash was 0).
3561        assert!(changed1, "first call should report changed");
3562
3563        // Same message again — no new groups, no change.
3564        let (_, changed2) = sel.select_tools(&msgs1);
3565        assert!(!changed2, "no new groups means no change");
3566
3567        // New keyword activates a group — should report changed.
3568        let msgs3 = vec![msg(Role::User, "compute sha256 checksum")];
3569        let (_, changed3) = sel.select_tools(&msgs3);
3570        assert!(changed3, "new group activation should report changed");
3571    }
3572
3573    #[test]
3574    fn test_selector_case_insensitive() {
3575        let mut sel = ToolSelector::new(&full_caps());
3576        let messages = vec![msg(Role::User, "GIT COMMIT DOCKER BUILD")];
3577        let (tools, _) = sel.select_tools(&messages);
3578
3579        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3580        assert!(names.contains(&"git_status"), "case insensitive match");
3581        assert!(names.contains(&"docker_ps"), "case insensitive match");
3582    }
3583
3584    #[test]
3585    fn test_selector_scan_window_limited() {
3586        // Use Crypto capability (not auto-activated) to test window limits.
3587        let caps = vec![Capability::Crypto];
3588        let mut sel = ToolSelector::new(&caps);
3589        // Old message mentions crypto keyword, but it's outside the 4-message window.
3590        let messages = vec![
3591            msg(Role::User, "compute sha256 checksum"), // 5th from end — outside window
3592            msg(Role::Assistant, "done"),               // 4th
3593            msg(Role::User, "ok"),                      // 3rd
3594            msg(Role::Assistant, "ok"),                 // 2nd
3595            msg(Role::User, "tell me a joke"),          // 1st (current)
3596        ];
3597        let (tools, _) = sel.select_tools(&messages);
3598
3599        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3600        assert!(
3601            !names.contains(&"hash_compute"),
3602            "old messages outside window should not activate groups"
3603        );
3604    }
3605
3606    #[test]
3607    fn test_selector_no_duplicates() {
3608        let mut sel = ToolSelector::new(&full_caps());
3609        // Use precise keywords that match the tightened patterns.
3610        let messages = vec![msg(
3611            Role::User,
3612            "git commit docker json unzip schedule cron browser sha256 plugin a2a knowledge graph code analysis",
3613        )];
3614        let (tools, _) = sel.select_tools(&messages);
3615
3616        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
3617        let before = names.len();
3618        names.sort();
3619        names.dedup();
3620        assert_eq!(before, names.len(), "duplicate tools found in selection");
3621    }
3622
3623    #[test]
3624    fn test_selector_core_count_reasonable() {
3625        // Use a fighter with only basic capabilities (no auto-activated groups).
3626        let caps = vec![
3627            Capability::FileRead("**".to_string()),
3628            Capability::FileWrite("**".to_string()),
3629            Capability::ShellExec("*".to_string()),
3630            Capability::Network("*".to_string()),
3631            Capability::Memory,
3632        ];
3633        let mut sel = ToolSelector::new(&caps);
3634        let messages = vec![msg(Role::User, "hello")];
3635        let (tools, _) = sel.select_tools(&messages);
3636
3637        // Core should be roughly 12-20 tools (not 80+).
3638        assert!(
3639            tools.len() < 25,
3640            "core tools should be under 25, got {}",
3641            tools.len()
3642        );
3643        assert!(
3644            tools.len() >= 10,
3645            "core tools should be at least 10, got {}",
3646            tools.len()
3647        );
3648    }
3649
3650    #[test]
3651    fn test_selector_full_caps_with_auto_activation() {
3652        // Full capabilities: auto-activated groups should be present from turn 1.
3653        let mut sel = ToolSelector::new(&full_caps());
3654        let messages = vec![msg(Role::User, "hello")];
3655        let (tools, _) = sel.select_tools(&messages);
3656
3657        // Should have core + auto-activated groups, but still well under the full 80+.
3658        assert!(
3659            tools.len() < 60,
3660            "full caps + auto-activation should be under 60, got {}",
3661            tools.len()
3662        );
3663        assert!(
3664            tools.len() >= 30,
3665            "full caps + auto-activation should be at least 30, got {}",
3666            tools.len()
3667        );
3668    }
3669
3670    #[test]
3671    fn test_tool_group_all_covers_every_variant() {
3672        // Ensure ALL array matches the enum variants.
3673        assert_eq!(
3674            ToolGroup::ALL.len(),
3675            17,
3676            "ToolGroup::ALL must match the number of variants"
3677        );
3678    }
3679
3680    #[test]
3681    fn test_tool_group_tools_not_empty() {
3682        for group in ToolGroup::ALL {
3683            let tools = group.tools();
3684            assert!(!tools.is_empty(), "ToolGroup::{:?} has no tools", group);
3685        }
3686    }
3687}