//! MCP policy resources and routing prompts that describe language support, tool-surface profiles,
//! and when agents should prefer Frigg tools over shell reads.
use rmcp::model::{
GetPromptResult, Prompt, PromptArgument, PromptMessage, ReadResourceResult, Resource,
ResourceContents, Role,
};
use serde::Serialize;
use serde_json::{Map, Value, json};
use crate::languages::{LanguageSupportCapability, SymbolLanguage};
use crate::mcp::tool_surface::{ToolSurfaceProfile, manifest_for_tool_surface_profile};
pub(crate) const SUPPORT_MATRIX_RESOURCE_URI: &str = "frigg://policy/support-matrix.json";
pub(crate) const TOOL_SURFACE_RESOURCE_URI: &str = "frigg://policy/tool-surface.json";
pub(crate) const SHELL_REPLACEMENT_MAP_RESOURCE_URI: &str =
"frigg://policy/shell-replacement-map.json";
pub(crate) const SHELL_GUIDANCE_RESOURCE_URI: &str = "frigg://guidance/shell-vs-frigg.md";
pub(crate) const ROUTING_GUIDE_PROMPT_NAME: &str = "frigg-routing-guide";
#[derive(Debug, Clone, Serialize)]
struct LanguageSupportEntry {
id: &'static str,
display_name: &'static str,
capabilities: Value,
search_outline: &'static str,
navigation: &'static str,
semantic_retrieval: &'static str,
capability_note: &'static str,
}
fn support_matrix_json() -> String {
let languages = SymbolLanguage::ALL
.into_iter()
.map(|language| LanguageSupportEntry {
id: support_matrix_language_id(language),
display_name: language.display_name(),
capabilities: language_capabilities_json(language),
search_outline: support_matrix_search_outline(language),
navigation: support_matrix_navigation(language),
semantic_retrieval: support_matrix_semantic_retrieval(language),
capability_note: support_matrix_capability_note(language),
})
.collect::<Vec<_>>();
serde_json::to_string_pretty(&json!({
"schema_id": "frigg.policy.support_matrix.v4",
"product": "frigg",
"product_boundary": "local-first deterministic code-evidence engine delivered through MCP",
"stable_core": [
"repository discovery and attach",
"safe file reads",
"text, symbol, and hybrid search",
"read-only navigation",
"evidence-backed auditing"
],
"optional_accelerators": [
"semantic retrieval",
"external SCIP ingestion",
"built-in watch mode"
],
"advanced_consumers": support_matrix_advanced_consumers(),
"language_support_notes": [
"Frigg currently supports the listed languages for source-backed search, outline, structural, and hybrid retrieval workflows.",
"Navigation stays read-only and may combine source heuristics, graph evidence, and optional external artifacts.",
"Semantic retrieval is optional acceleration only and never the grounding layer."
],
"capability_tiers": {
"core": "capability is part of FRIGG's stable read-only core contract for that language",
"optional_accelerator": "capability is an optional accelerator that only contributes when runtime configuration and repository state make it available",
"unsupported": "capability is not currently provided for that language in the runtime registry"
},
"languages": languages
}))
.expect("support matrix JSON should serialize")
}
fn shell_replacement_map_json() -> String {
serde_json::to_string_pretty(&json!({
"schema_id": "frigg.policy.shell_replacement_map.v1",
"product": "frigg",
"default_for": [
"source-code discovery",
"exact text search",
"symbol lookup",
"repository-relative source reads",
"code navigation"
],
"fallback_only_for": [
"git state and diffs",
"non-code files or workspace metadata",
"build/test output",
"generated or unindexed files",
"explicit live-disk verification",
"Frigg unavailable"
],
"replacements": [
{
"shell": "rg --files",
"tool": "list_files",
"params": ["path_regex", "glob", "language", "path_class", "include_hidden", "limit", "resume_from"]
},
{
"shell": "rg -n PATTERN",
"tool": "search_text",
"params": ["query", "pattern_type", "case_sensitive", "ignore_case", "word", "context_lines", "limit"]
},
{
"shell": "rg -n 'foo|bar'",
"tool": "search_text",
"params": ["query", "pattern_type=regex"]
},
{
"shell": "rg -n PATTERN path/",
"tool": "search_text",
"params": ["query", "path_regex"]
},
{
"shell": "rg -n -g GLOB PATTERN",
"tool": "search_text",
"params": ["query", "glob", "exclude_glob"]
},
{
"shell": "rg -l PATTERN",
"tool": "search_text",
"params": ["query", "files_with_matches=true"]
},
{
"shell": "rg -c PATTERN",
"tool": "search_text",
"params": ["query", "count_only=true"]
},
{
"shell": "identifier/API/type/class/function lookup",
"tool": "search_symbol",
"params": ["query", "path_regex", "path_class", "limit"]
},
{
"shell": "cat path",
"tool": "read_file",
"params": ["path"]
},
{
"shell": "sed -n '10,80p' path",
"tool": "read_file",
"params": ["path", "start_line", "end_line", "line_count"]
},
{
"shell": "follow definitions/references/calls",
"tool": "navigation tools",
"params": ["go_to_definition", "find_references", "find_implementations", "incoming_calls", "outgoing_calls"]
}
]
}))
.expect("shell replacement map JSON should serialize")
}
fn support_matrix_language_id(language: SymbolLanguage) -> &'static str {
match language {
SymbolLanguage::TypeScript => "typescript_tsx",
other => other.as_str(),
}
}
fn support_matrix_search_outline(language: SymbolLanguage) -> &'static str {
match language {
SymbolLanguage::Blade => "supported_template_surface",
_ => "supported_source_language",
}
}
fn support_matrix_navigation(language: SymbolLanguage) -> &'static str {
match language {
SymbolLanguage::Blade => "bounded_source_template_navigation",
_ => "read_only_source_graph_or_artifact_assisted",
}
}
fn support_matrix_semantic_retrieval(language: SymbolLanguage) -> &'static str {
if language.supports_semantic_chunking() {
"optional_when_enabled"
} else {
"unsupported"
}
}
fn support_matrix_capability_note(language: SymbolLanguage) -> &'static str {
match language {
SymbolLanguage::Blade => "template_metadata_livewire_flux",
_ => "general_source_support",
}
}
fn language_capabilities_json(language: SymbolLanguage) -> Value {
let mut capabilities = Map::new();
for capability in LanguageSupportCapability::ALL {
capabilities.insert(
capability.as_str().to_owned(),
Value::String(language.capability_tier(capability).as_str().to_owned()),
);
}
Value::Object(capabilities)
}
fn extended_only_tool_names() -> Vec<String> {
let core = manifest_for_tool_surface_profile(ToolSurfaceProfile::Core);
let extended = manifest_for_tool_surface_profile(ToolSurfaceProfile::Extended);
extended
.tool_names
.into_iter()
.filter(|tool_name| !core.tool_names.contains(tool_name))
.collect()
}
fn support_matrix_advanced_consumers() -> Vec<String> {
let mut consumers = extended_only_tool_names();
consumers.push("self_improvement_loop".to_owned());
consumers
}
fn tool_surface_json(active_profile: ToolSurfaceProfile) -> String {
let core = manifest_for_tool_surface_profile(ToolSurfaceProfile::Core);
let core_guidance = if cfg!(feature = "playbook") {
"The default runtime surface is extended. Set FRIGG_MCP_TOOL_SURFACE_PROFILE=core when you need the restricted stable subset without explore or playbook tools."
} else {
"The default runtime surface is extended. Set FRIGG_MCP_TOOL_SURFACE_PROFILE=core when you need the restricted stable subset without explore."
};
serde_json::to_string_pretty(&json!({
"schema_id": "frigg.policy.tool_surface.v1",
"default_profile": ToolSurfaceProfile::Extended.as_str(),
"active_profile": active_profile.as_str(),
"core_tools": core.tool_names,
"extended_only_tools": extended_only_tool_names(),
"guidance": [
"Use Frigg as the default for code discovery, file listing, navigation, exact code search, and bounded source reads.",
"Use workspace for compact workspace status or to adopt a target path/repository; repo-aware tools auto-adopt sensible defaults when possible.",
"Before shell rg/grep/find/fd/cat/sed for code exploration, use list_files, search_text, search_symbol, read_file, read_match, or navigation tools.",
"Use search_hybrid only for broad discovery-style repository questions; use search_text for rg-shaped literal or safe-regex code scans, including grouped alternation and path_regex narrowing; use search_symbol for known identifiers; use list_files for rg --files-shaped listing.",
"Use shell tools as the exception for git state and diffs, non-code files, build/test output, generated or unindexed files, explicit live-disk verification, and unavailable Frigg results.",
"Use Frigg when repository-aware evidence, symbols, navigation, provenance, or multi-repo context matter.",
"Read surfaces are text-first by default: read_file, read_match, and explore(operation=zoom). Request presentation_mode=json when a downstream consumer needs the structured compatibility payload.",
"Use include_follow_up_structural=true when you want replayable search_structural follow-ups from inspect_syntax_tree, search_structural, or anchored navigation and outline results.",
core_guidance
]
}))
.expect("tool surface JSON should serialize")
}
fn shell_vs_frigg_markdown(active_profile: ToolSurfaceProfile) -> String {
let explore_guidance = if active_profile == ToolSurfaceProfile::Extended {
"`explore` is available for bounded single-artifact follow-up after discovery. `explore(operation=zoom)` defaults to the same text-first read rendering as `read_file` and `read_match`, while `probe` and `refine` stay structured by default."
} else {
"`explore` is intentionally absent from the active `core` profile."
};
format!(
"# Shell vs Frigg\n\n\
Use Frigg as the default for code discovery, file listing, navigation, exact code search, and bounded source reads.\n\n\
- repository-aware file listing through `list_files` instead of `rg --files`, `find`, or `fd`\n\
- symbol, definition, reference, implementation, or call navigation\n\
- exact literal, safe-regex, grouped-alternation, or `rg`-shaped code searches with repository scoping and result handles\n\
- bounded source reads through `read_file`, `read_match`, or `explore(operation=zoom)`\n\
- mixed doc/runtime questions where lexical, graph, witness, and semantic channels may all matter\n\
- evidence-backed answers or replayable source references\n\
- attached multi-repo context instead of one current shell directory\n\n\
Use shell tools only as the exception for git state and diffs, non-code files, build/test output, generated or unindexed files, explicit live-disk verification, and unavailable Frigg results.\n\n\
Use shell `rg` for explicit live-disk verification, ripgrep-specific flags outside `search_text`, or generated/unindexed files.\n\n\
Shell replacement map:\n\
- `rg --files` -> `list_files`\n\
- `rg -n \"text\"` -> `search_text`\n\
- `rg -n \"foo|bar\"` -> `search_text` with regex mode\n\
- `rg -n \"text\" path/` -> `search_text` with `path_regex`\n\
- identifier/API/type/class/function lookup -> `search_symbol`\n\
- `cat path` -> `read_file`\n\
- `sed -n '10,80p' path` -> `read_file` with `start_line`, `end_line`, or `line_count`\n\
- follow definitions/references/calls -> navigation tools\n\n\
Use `search_hybrid` only for broad discovery-style repository questions when there is no stable string, symbol, or path anchor yet. Use `search_text` for `rg`-shaped literal or safe-regex scans, including grouped alternation, `path_regex` narrowing, context windows, per-file limits (`max_count_per_file`), and file-containment probes (`files_with_matches`). Frigg may execute those scans with its native scanner, its ripgrep accelerator, or a mixed path while preserving repository-scoped results and result handles. Use `search_symbol` for known identifiers.\n\n\
`read_file` and `read_match` default to text-first output. Ask for `presentation_mode=json` when a caller needs the structured compatibility payload with explicit `content`, and apply the same rule to `explore(operation=zoom)` in the extended profile.\n\n\
Structural follow-up suggestions are opt-in. Use `include_follow_up_structural=true` on `inspect_syntax_tree`, `search_structural`, or anchored navigation and outline tools when you want replayable `search_structural` follow-ups derived from the resolved AST focus.\n\n\
Semantic retrieval remains an optional accelerator, not the grounding layer.\n\n\
{explore_guidance}\n"
)
}
pub(crate) fn policy_resources() -> Vec<Resource> {
vec![
Resource::new(SUPPORT_MATRIX_RESOURCE_URI, "FRIGG Support Matrix")
.with_description("Machine-readable supported languages and capability notes.")
.with_mime_type("application/json"),
Resource::new(TOOL_SURFACE_RESOURCE_URI, "FRIGG Tool Surface Policy")
.with_description("Machine-readable core vs extended tool-surface policy.")
.with_mime_type("application/json"),
Resource::new(
SHELL_REPLACEMENT_MAP_RESOURCE_URI,
"FRIGG Shell Replacement Map",
)
.with_description("Machine-readable shell-to-Frigg replacement table.")
.with_mime_type("application/json"),
Resource::new(SHELL_GUIDANCE_RESOURCE_URI, "Shell vs Frigg Guidance")
.with_description(
"Guidance for when to use shell tools versus repo-aware Frigg surfaces.",
)
.with_mime_type("text/markdown"),
]
}
pub(crate) fn read_policy_resource(
uri: &str,
active_profile: ToolSurfaceProfile,
) -> Option<ReadResourceResult> {
let (content, mime_type) = match uri {
SUPPORT_MATRIX_RESOURCE_URI => (support_matrix_json(), "application/json"),
TOOL_SURFACE_RESOURCE_URI => (tool_surface_json(active_profile), "application/json"),
SHELL_REPLACEMENT_MAP_RESOURCE_URI => (shell_replacement_map_json(), "application/json"),
SHELL_GUIDANCE_RESOURCE_URI => (shell_vs_frigg_markdown(active_profile), "text/markdown"),
_ => return None,
};
Some(ReadResourceResult::new(vec![
ResourceContents::text(content, uri).with_mime_type(mime_type),
]))
}
pub(crate) fn guidance_prompts() -> Vec<Prompt> {
vec![Prompt::new(
ROUTING_GUIDE_PROMPT_NAME,
Some("Route a code question toward Frigg tools, shell exceptions, or extended follow-up."),
Some(vec![PromptArgument::new("task")
.with_description("Optional task or question to route.")
.with_required(false)]),
)
.with_title("FRIGG Routing Guide")]
}
pub(crate) fn read_guidance_prompt(
name: &str,
arguments: Option<&Map<String, Value>>,
active_profile: ToolSurfaceProfile,
) -> Option<GetPromptResult> {
if name != ROUTING_GUIDE_PROMPT_NAME {
return None;
}
let task = arguments
.and_then(|map| map.get("task"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let profile_note = if active_profile == ToolSurfaceProfile::Extended {
"Active profile: `extended`."
} else {
"Active profile: `core`."
};
let mut text = String::new();
if let Some(task) = task {
text.push_str("Task:\n");
text.push_str(task);
text.push_str("\n\n");
}
text.push_str(
"Routing policy:\n\
1. Prefer Frigg for code discovery, navigation, exact code search, and bounded source reads.\n\
2. Use `search_hybrid` only for broad discovery-style repository questions when there is no stable string, symbol, or path anchor yet; use `search_text` for `rg`-shaped literal or safe-regex scans, including grouped alternation and `path_regex`; use `search_symbol` for known identifiers.\n\
3. Use shell tools as the exception for non-code files, git/filesystem inspection, explicit live-disk verification, trivial one-offs, or ripgrep-specific flags outside `search_text`.\n\
4. Prefer Frigg core tools when repository-aware evidence, symbols, navigation, provenance, or multi-repo context matter.\n\
5. Treat semantic retrieval as optional acceleration only. When broad discovery is weak, pivot to lexical, graph, and source-witness evidence instead of diagnosing runtime state by default.\n\
6. Treat the current supported-language set as one public list: Rust, PHP, Blade, TypeScript / TSX, Python, Go, Kotlin / KTS, Java, Lua, Roc, and Nim. Describe differences in concrete capability terms, not first-class or baseline badges.\n\
7. `read_file` and `read_match` default to text-first output; request `presentation_mode=json` only when the caller truly needs the structured compatibility payload. In the extended profile, `explore(operation=zoom)` follows the same text-first default, while `probe` and `refine` stay structured.\n\
8. Use `include_follow_up_structural=true` when you want replayable `search_structural` follow-ups from `inspect_syntax_tree`, `search_structural`, or anchored navigation and outline results.\n\
9. Use `explore` only after discovery and only when the active profile includes it.\n\n",
);
text.push_str(profile_note);
Some(
GetPromptResult::new(vec![
PromptMessage::new_text(Role::Assistant, text),
PromptMessage::new_resource_link(
Role::Assistant,
Resource::new(SUPPORT_MATRIX_RESOURCE_URI, "FRIGG Support Matrix"),
),
PromptMessage::new_resource_link(
Role::Assistant,
Resource::new(TOOL_SURFACE_RESOURCE_URI, "FRIGG Tool Surface Policy"),
),
PromptMessage::new_resource_link(
Role::Assistant,
Resource::new(
SHELL_REPLACEMENT_MAP_RESOURCE_URI,
"FRIGG Shell Replacement Map",
),
),
PromptMessage::new_resource_link(
Role::Assistant,
Resource::new(SHELL_GUIDANCE_RESOURCE_URI, "Shell vs Frigg Guidance"),
),
])
.with_description("Guide shell-vs-FRIGG routing and link the relevant policy resources."),
)
}
#[cfg(test)]
mod tests {
use super::{
ROUTING_GUIDE_PROMPT_NAME, SHELL_GUIDANCE_RESOURCE_URI, SHELL_REPLACEMENT_MAP_RESOURCE_URI,
SUPPORT_MATRIX_RESOURCE_URI, TOOL_SURFACE_RESOURCE_URI, read_guidance_prompt,
read_policy_resource,
};
use crate::languages::{LanguageSupportCapability, SymbolLanguage};
use crate::mcp::tool_surface::ToolSurfaceProfile;
use rmcp::model::ResourceContents;
use serde_json::{Value, json};
fn resource_text(uri: &str, profile: ToolSurfaceProfile) -> String {
let result = read_policy_resource(uri, profile).expect("resource should exist");
let ResourceContents::TextResourceContents { text, .. } = &result.contents[0] else {
unreachable!("expected text resource contents");
};
text.clone()
}
#[test]
fn support_matrix_lists_supported_languages_without_rollout_tiers() {
let json = resource_text(SUPPORT_MATRIX_RESOURCE_URI, ToolSurfaceProfile::Core);
let parsed =
serde_json::from_str::<Value>(&json).expect("support matrix JSON should parse");
assert!(parsed.get("next_language_priority").is_none());
assert!(parsed.get("language_rollout_policy").is_none());
assert_eq!(parsed["schema_id"], json!("frigg.policy.support_matrix.v4"));
assert_eq!(
parsed["capability_tiers"]["core"].as_str(),
Some("capability is part of FRIGG's stable read-only core contract for that language")
);
assert_eq!(
parsed["capability_tiers"]["optional_accelerator"].as_str(),
Some(
"capability is an optional accelerator that only contributes when runtime configuration and repository state make it available"
)
);
for language_id in [
"rust",
"php",
"blade",
"typescript_tsx",
"python",
"go",
"kotlin",
"java",
"lua",
"roc",
"nim",
] {
assert!(
parsed["languages"]
.as_array()
.expect("languages should be an array")
.iter()
.any(|entry| entry["id"] == json!(language_id)),
"expected {language_id} to be listed as supported"
);
}
assert!(
parsed["languages"]
.as_array()
.expect("languages should be an array")
.iter()
.any(|entry| {
entry["id"] == json!("blade")
&& entry["capability_note"] == json!("template_metadata_livewire_flux")
})
);
assert_eq!(
parsed["languages"]
.as_array()
.expect("languages should be an array")
.iter()
.find(|entry| entry["id"] == json!("typescript_tsx"))
.and_then(|entry| entry.get("capabilities"))
.and_then(|value| value.get("semantic_chunking"))
.and_then(|value| value.as_str()),
Some("unsupported")
);
assert_eq!(
parsed["languages"]
.as_array()
.expect("languages should be an array")
.iter()
.find(|entry| entry["id"] == json!("rust"))
.and_then(|entry| entry.get("capabilities"))
.and_then(|value| value.get("precise_artifact_assist"))
.and_then(|value| value.as_str()),
Some("optional_accelerator")
);
}
#[test]
fn support_matrix_capabilities_match_language_registry() {
let json = resource_text(SUPPORT_MATRIX_RESOURCE_URI, ToolSurfaceProfile::Core);
let parsed =
serde_json::from_str::<Value>(&json).expect("support matrix JSON should parse");
let languages = parsed["languages"]
.as_array()
.expect("languages should be an array");
for language in SymbolLanguage::ALL {
let expected_id = if matches!(language, SymbolLanguage::TypeScript) {
"typescript_tsx"
} else {
language.as_str()
};
let entry = languages
.iter()
.find(|entry| entry["id"] == json!(expected_id))
.unwrap_or_else(|| unreachable!("expected {expected_id} to be listed"));
for capability in LanguageSupportCapability::ALL {
let expected = language.capability_tier(capability).as_str();
assert_eq!(
entry["capabilities"][capability.as_str()].as_str(),
Some(expected),
"expected {expected_id} capability {} to match the registry",
capability.as_str()
);
}
}
}
#[test]
fn support_matrix_advanced_consumers_follow_extended_tool_surface_manifest() {
let json = resource_text(SUPPORT_MATRIX_RESOURCE_URI, ToolSurfaceProfile::Core);
let parsed =
serde_json::from_str::<Value>(&json).expect("support matrix JSON should parse");
let advanced_consumers = parsed["advanced_consumers"]
.as_array()
.expect("advanced_consumers should be an array");
let core =
crate::mcp::tool_surface::manifest_for_tool_surface_profile(ToolSurfaceProfile::Core);
let extended = crate::mcp::tool_surface::manifest_for_tool_surface_profile(
ToolSurfaceProfile::Extended,
);
for tool_name in extended
.tool_names
.iter()
.filter(|tool_name| !core.tool_names.contains(tool_name))
{
assert!(
advanced_consumers
.iter()
.any(|entry| entry.as_str() == Some(tool_name.as_str())),
"expected advanced_consumers to include extended-only tool {tool_name}"
);
}
assert!(
!advanced_consumers
.iter()
.any(|entry| entry.as_str() == Some("search_text")),
"stable-core tools must not leak into advanced_consumers"
);
assert!(
advanced_consumers
.iter()
.any(|entry| entry.as_str() == Some("self_improvement_loop")),
"support matrix should keep non-tool advanced consumers explicit"
);
}
#[test]
fn tool_surface_policy_lists_explore_as_extended_only() {
let json = resource_text(TOOL_SURFACE_RESOURCE_URI, ToolSurfaceProfile::Extended);
let parsed =
serde_json::from_str::<Value>(&json).expect("tool surface policy JSON should parse");
assert!(
parsed["extended_only_tools"]
.as_array()
.expect("extended_only_tools should be an array")
.iter()
.any(|entry| entry == "explore")
);
}
#[test]
fn tool_surface_policy_matches_profile_manifests() {
let json = resource_text(TOOL_SURFACE_RESOURCE_URI, ToolSurfaceProfile::Extended);
let parsed =
serde_json::from_str::<Value>(&json).expect("tool surface policy JSON should parse");
let core =
crate::mcp::tool_surface::manifest_for_tool_surface_profile(ToolSurfaceProfile::Core);
let extended = crate::mcp::tool_surface::manifest_for_tool_surface_profile(
ToolSurfaceProfile::Extended,
);
let expected_extended_only = extended
.tool_names
.iter()
.filter(|tool_name| !core.tool_names.contains(tool_name))
.cloned()
.map(Value::String)
.collect::<Vec<_>>();
assert_eq!(
parsed["default_profile"].as_str(),
Some(ToolSurfaceProfile::Extended.as_str())
);
assert_eq!(
parsed["core_tools"].as_array(),
Some(
&core
.tool_names
.iter()
.cloned()
.map(Value::String)
.collect::<Vec<_>>()
)
);
assert_eq!(
parsed["extended_only_tools"].as_array(),
Some(&expected_extended_only)
);
}
#[test]
fn shell_replacement_map_is_machine_readable() {
let json = resource_text(SHELL_REPLACEMENT_MAP_RESOURCE_URI, ToolSurfaceProfile::Core);
let parsed =
serde_json::from_str::<Value>(&json).expect("shell replacement map JSON should parse");
assert_eq!(
parsed["schema_id"],
json!("frigg.policy.shell_replacement_map.v1")
);
assert!(
parsed["replacements"]
.as_array()
.expect("replacements should be an array")
.iter()
.any(|entry| {
entry["shell"] == json!("sed -n '10,80p' path")
&& entry["tool"] == json!("read_file")
&& entry["params"]
.as_array()
.expect("params should be an array")
.iter()
.any(|param| param == "start_line")
&& entry["params"]
.as_array()
.expect("params should be an array")
.iter()
.any(|param| param == "line_count")
})
);
assert!(
parsed["replacements"]
.as_array()
.expect("replacements should be an array")
.iter()
.any(|entry| {
entry["shell"] == json!("rg -l PATTERN")
&& entry["tool"] == json!("search_text")
&& entry["params"]
.as_array()
.expect("params should be an array")
.iter()
.any(|param| param == "files_with_matches=true")
})
);
}
#[test]
fn routing_prompt_links_policy_resources() {
let prompt = read_guidance_prompt(
ROUTING_GUIDE_PROMPT_NAME,
Some(&serde_json::Map::from_iter([(
"task".to_owned(),
json!("where is runtime state wired"),
)])),
ToolSurfaceProfile::Extended,
)
.expect("routing prompt should exist");
assert_eq!(prompt.messages.len(), 5);
}
#[test]
fn agent_directive_core_sentences_are_in_guidance_surfaces() {
let shell_guidance = resource_text(SHELL_GUIDANCE_RESOURCE_URI, ToolSurfaceProfile::Core);
let tool_surface = resource_text(TOOL_SURFACE_RESOURCE_URI, ToolSurfaceProfile::Core);
let prompt = read_guidance_prompt(
ROUTING_GUIDE_PROMPT_NAME,
Some(&serde_json::Map::from_iter([(
"task".to_owned(),
json!("find every caller of load_config"),
)])),
ToolSurfaceProfile::Extended,
)
.expect("routing prompt should exist");
let prompt_debug = format!("{prompt:?}");
assert!(shell_guidance.contains(
"Use Frigg as the default for code discovery, file listing, navigation, exact code search, and bounded source reads."
));
assert!(tool_surface.contains(
"Use Frigg as the default for code discovery, file listing, navigation, exact code search, and bounded source reads."
));
assert!(
shell_guidance.contains(
"Use `search_hybrid` only for broad discovery-style repository questions"
)
);
assert!(
tool_surface
.contains("Use search_hybrid only for broad discovery-style repository questions")
);
assert!(
prompt_debug.contains(
"Use `search_hybrid` only for broad discovery-style repository questions"
)
);
assert!(prompt_debug.contains(
"Prefer Frigg for code discovery, navigation, exact code search, and bounded source reads."
));
assert!(shell_guidance.contains(
"Use shell tools only as the exception for git state and diffs, non-code files, build/test output, generated or unindexed files, explicit live-disk verification, and unavailable Frigg results."
));
assert!(tool_surface.contains(
"Use shell tools as the exception for git state and diffs, non-code files, build/test output, generated or unindexed files, explicit live-disk verification, and unavailable Frigg results."
));
assert!(prompt_debug.contains(
"Use shell tools as the exception for non-code files, git/filesystem inspection, explicit live-disk verification, trivial one-offs, or ripgrep-specific flags outside `search_text`."
));
}
}