use crate::config::{Config, KumihoConfig, McpServerConfig, McpTransport};
use directories::UserDirs;
use std::collections::HashMap;
pub const KUMIHO_SERVER_NAME: &str = "kumiho-memory";
pub const DEFAULT_MCP_PATH_SUFFIX: &str = ".construct/kumiho/run_kumiho_mcp.py";
pub const KUMIHO_BOOTSTRAP_PROMPT: &str = "\
SESSION-START INSTRUCTION (kumiho-memory — Construct daemon)
=== EVERY TURN ===
Follow these rules on every turn:
- Do NOT greet the user unless they greeted you first. If their \
message is a question or task, answer directly.
- ENGAGE: Call kumiho_memory_engage ONCE when prior context \
is needed to answer correctly — for example, when the user \
references prior work, a past decision, a person, a project, \
asks 'do you remember', or mentions something not in the current \
conversation. Skip engage for greetings, acknowledgements, yes/no \
answers, simple status checks, tool-availability confirmations, \
brief meta chat, and short direct answers. Your query MUST derive \
from the user's current message. Hold the returned source_krefs \
for reflect. Use limit={{KUMIHO_MEMORY_RETRIEVAL_LIMIT}} unless \
the user explicitly asks for broader recall. IMPORTANT: User \
memories and conversation history live in the CognitiveMemory \
project — use space_paths=['CognitiveMemory'] for memory recall. \
Do NOT search Construct/ for user memories — that space holds agent \
operational data (AgentPool, Teams, Plans).
- NEVER SAY 'I DON'T KNOW' WITHOUT CHECKING MEMORY — If the \
answer is not in the current conversation and you would otherwise \
say you don't know, don't have context, or can't find something, \
you MUST call kumiho_memory_engage first. Only after engage \
returns empty may you tell the user you don't have the information.
- ONE TOOL FOR RECALL — Always use kumiho_memory_engage for \
recall. It returns aggregated results in a single call. Do NOT \
chain low-level tools (fulltext_search, search_items, get_item, \
etc.) to piece together recall — that wastes tokens and time.
- REFLECT: Call kumiho_memory_reflect only for explicit \
'remember this' requests, durable preferences/facts/decisions/\
corrections, meaningful multi-step outcomes worth preserving, or \
compacted session summaries/handoffs. Skip reflect for greetings, \
acknowledgements, yes/no answers, simple status checks, \
tool-availability confirmations, brief meta chat, and short direct \
answers.
- EXPLICIT REMEMBER REQUESTS — When the user says 'remember \
this', 'keep this in mind', 'note that', or similar, you MUST \
capture it via kumiho_memory_reflect. Do NOT rely on built-in \
memory tools — Kumiho MCP tools are the canonical memory store.
- Do NOT narrate memory operations.
- Do NOT repeat content you already showed the user. Refer to \
it briefly (e.g. 'the draft above') instead of reproducing it.
- Do NOT re-ask questions already answered in this conversation.
- Do NOT re-execute tasks already completed.
- If you need user input, ask and STOP. Never simulate the \
user's answer.
- On the first message of the session: if the user's message is a \
greeting or casual talk (hi, hey, good morning, etc.), just greet \
back — do NOT call kumiho_memory_engage. Only engage if their \
message is a question or task that would benefit from prior context. \
Never narrate the bootstrap (no 'Memory connected!' or similar).
=== ALWAYS ===
TEMPORAL AWARENESS — When using engage results, compare each \
result's created_at against today's date. Express memory age \
naturally ('earlier today', 'yesterday', 'last Tuesday', 'about \
two weeks ago'). Recent memories take precedence over stale ones \
when they conflict. When capturing memories via reflect, always \
use absolute dates in titles ('on Mar 29', not 'today') — relative \
time becomes meaningless when recalled in a future session.
COMPACTION — On /compact or auto-compression, capture summary via \
kumiho_memory_reflect with type='summary', tags=['compact','session-context'].
SKILL DISCOVERY — Search CognitiveMemory/Skills via engage when \
you need specialised guidance. Cache discovered skills for the session.
=== CONSTRUCT NAMESPACES ===
Construct/ is the operational root. CognitiveMemory/ is the user's \
personal memory — never write agent data there.
- Construct/AgentPool/ — agent templates (keyed by name)
- Construct/Plans/ — task plans with DEPENDS_ON edges
- Construct/Sessions/ — session summaries and handoffs
- Construct/Sessions/<session_id>/Outcomes/ — APPEND-ONLY findings \
agents share with each other (multi-agent learning, see below).
- Construct/Teams/ — agent team DAGs (REPORTS_TO, SUPPORTS)
- CognitiveMemory/Skills/ — shared skill library (only shared space)
Use space_hint in reflect to route captures to the correct subspace.
=== AGENT OUTCOMES (multi-agent learning) ===
When you run as part of a workflow / handoff chain / group chat, \
your peers can benefit from what you learn — and you can benefit \
from what they already learned. Coordinate via the per-session \
Outcomes namespace: Construct/Sessions/<session_id>/Outcomes/.
session_id resolution — your task context will mention it (look in \
the initial prompt, system_hint, or env). If you are in a workflow \
run, get_workflow_context returns it. If none is available, you \
are running ad-hoc — skip the outcomes pattern entirely.
INHERIT (first turn, after greeting, only when session_id is \
known): pull what siblings have already discovered so you do not \
re-do their work.
kumiho_memory_engage(
query=<your task summary>,
space_paths=['Construct/Sessions/<session_id>/Outcomes']
)
CONTRIBUTE (during work): when you find something DURABLE that \
future agents in this session would need — a non-obvious risk, \
a settled architectural decision, a hard-won lesson, an action \
plan, a load-bearing fact — record it. Skip noise, things specific \
to your own setup, or trivia.
kumiho_memory_reflect(
captures=[{
type: 'discovery' | 'decision' | 'lesson' | 'insight' \
| 'warning' | 'fact',
title: '<short title with absolute date>',
content: '<the actual finding>',
space_hint: 'Construct/Sessions/<session_id>/Outcomes',
tags: ['outcome', '<kind>', 'session:<session_id>']
}]
)
Outcomes are append-only — do not try to overwrite a sibling's \
outcome, record a refining one and let the graph show the chain.";
pub const KUMIHO_BOOTSTRAP_PROMPT_LITE: &str = "\
SESSION-START INSTRUCTION (kumiho-memory — Construct daemon, lite mode)
Advanced memory reflexes are unavailable in this session. Use the bare \
memory tools listed below for any persistence work; do not assume \
higher-level reflex tools exist.
Available tools:
- kumiho_memory_store — store a memory item to the graph.
- kumiho_memory_retrieve — retrieve a memory item by id or filter.
Rules:
- For explicit 'remember this' requests, use kumiho_memory_store with an \
absolute date in the title (e.g. 'on Mar 27', not 'today').
- For recall, use kumiho_memory_retrieve. Do not say 'I don't know' or \
'I don't have context' before searching memory.
- User memories live under the CognitiveMemory project. Construct/ holds \
agent operational data (AgentPool, Teams, Plans).
- Skip memory operations for greetings, acknowledgements, yes/no answers, \
and other trivial exchanges.
- Do not narrate memory operations. Do not repeat content already shown.";
pub const KUMIHO_CHANNEL_BOOTSTRAP_PROMPT: &str = "\
SESSION-START INSTRUCTION (kumiho-memory — Construct channel)
You have access to kumiho-memory MCP for persistent memory.
ENGAGE: Call kumiho_memory_engage ONCE when prior context is needed \
(user references past work, decisions, people, or asks 'do you \
remember' something not in current conversation). Use \
space_paths=['CognitiveMemory'] and \
limit={{KUMIHO_MEMORY_RETRIEVAL_LIMIT}} unless the user explicitly \
asks for broader recall. Skip for greetings, simple answers, and \
casual chat. NEVER say 'I don't know' or 'I don't have that context' \
without calling engage first. Use \
kumiho_memory_engage for all recall — do NOT chain low-level tools.
REFLECT: Call kumiho_memory_reflect only for explicit 'remember \
this' requests, durable preferences, corrections, or significant \
decisions. Use absolute dates in titles ('on Apr 1', not 'today').
Rules:
- Do not call memory tools on every turn — skip for trivial exchanges.
- Do not narrate memory operations.
- Do not repeat content already shown.
- Recent memories take precedence over stale ones.";
pub const KUMIHO_CHANNEL_BOOTSTRAP_PROMPT_LITE: &str = "\
SESSION-START INSTRUCTION (kumiho-memory — Construct channel, lite mode)
Advanced memory reflexes are unavailable in this session.
For 'remember this' requests, use kumiho_memory_store with an absolute date \
in the title. For recall, use kumiho_memory_retrieve before saying you don't \
know. Skip memory ops for greetings and trivial exchanges. Do not narrate \
memory operations.";
pub fn resolve_mcp_path(kumiho_cfg: &KumihoConfig) -> String {
let configured = kumiho_cfg.mcp_path.trim();
if !configured.is_empty() {
return expand_tilde(configured);
}
let home = UserDirs::new()
.map(|u| u.home_dir().to_string_lossy().into_owned())
.unwrap_or_else(|| "~".to_string());
format!("{home}/{DEFAULT_MCP_PATH_SUFFIX}")
}
const ADVANCED_PROBE_TOOL_SUFFIX: &str = "kumiho_memory_engage";
fn prefixed_kumiho_tool(tool: &str) -> String {
format!("{}__{}", KUMIHO_SERVER_NAME, tool)
}
pub fn registry_has_advanced_kumiho_tools(tool_names: &[String]) -> bool {
let target = prefixed_kumiho_tool(ADVANCED_PROBE_TOOL_SUFFIX);
tool_names.iter().any(|n| n == &target)
}
pub fn warn_if_kumiho_advanced_missing(config: &Config, advanced_available: bool) {
if !config.kumiho.enabled || advanced_available {
return;
}
tracing::warn!(
"Kumiho high-level memory tools were not registered after MCP startup. \
Bootstrap prompt is using the lite variant. Check ~/.construct/logs/ \
for MCP startup errors. To re-install: \
`~/.construct/kumiho/venv/bin/pip install 'kumiho_memory>=0.5.0'` \
(or re-run scripts/install-sidecars.sh)."
);
}
pub fn kumiho_mcp_server_config(kumiho_cfg: &KumihoConfig) -> McpServerConfig {
let script_path = resolve_mcp_path(kumiho_cfg);
let mut env: HashMap<String, String> = HashMap::new();
env.insert(
"CONSTRUCT_AGENT_ROOT".to_string(),
expand_tilde("~/.construct"),
);
if !kumiho_cfg.space_prefix.trim().is_empty() {
env.insert(
"KUMIHO_SPACE_PREFIX".to_string(),
kumiho_cfg.space_prefix.clone(),
);
}
env.insert(
"KUMIHO_MEMORY_PROJECT".to_string(),
kumiho_cfg.memory_project.clone(),
);
env.insert(
"KUMIHO_HARNESS_PROJECT".to_string(),
kumiho_cfg.harness_project.clone(),
);
env.insert(
"KUMIHO_MEMORY_RETRIEVAL_LIMIT".to_string(),
kumiho_cfg.memory_retrieval_limit.max(1).to_string(),
);
let auth_token = std::env::var("KUMIHO_AUTH_TOKEN")
.ok()
.filter(|t| !t.trim().is_empty())
.or_else(|| {
std::env::var("KUMIHO_SERVICE_TOKEN")
.ok()
.filter(|t| !t.trim().is_empty())
});
if let Some(token) = auth_token {
env.insert("KUMIHO_AUTH_TOKEN".to_string(), token);
}
if let Ok(url) = std::env::var("KUMIHO_CONTROL_PLANE_URL") {
if !url.trim().is_empty() {
env.insert("KUMIHO_CONTROL_PLANE_URL".to_string(), url);
}
}
env.insert("KUMIHO_AUTO_CONFIGURE".to_string(), "1".to_string());
for var in &[
"KUMIHO_LLM_API_KEY",
"KUMIHO_LLM_PROVIDER",
"KUMIHO_LLM_MODEL",
"KUMIHO_LLM_LIGHT_MODEL",
"KUMIHO_LLM_BASE_URL",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
] {
if let Ok(val) = std::env::var(var) {
if !val.trim().is_empty() {
env.insert(var.to_string(), val);
}
}
}
McpServerConfig {
name: KUMIHO_SERVER_NAME.to_string(),
transport: McpTransport::Stdio,
command: crate::sidecars::python::default_python_command().to_string(),
args: vec![script_path],
env,
url: None,
headers: HashMap::new(),
tool_timeout_secs: None,
}
}
pub fn inject_kumiho(mut config: Config, is_internal: bool) -> Config {
if is_internal {
return config;
}
if !config.kumiho.enabled {
return config;
}
config.mcp.enabled = true;
let already_registered = config
.mcp
.servers
.iter()
.any(|s| s.name == KUMIHO_SERVER_NAME);
if !already_registered {
let kumiho_cfg = config.kumiho.clone();
let mut server = kumiho_mcp_server_config(&kumiho_cfg);
if !server.env.contains_key("KUMIHO_LLM_API_KEY")
&& !server.env.contains_key("OPENAI_API_KEY")
&& !server.env.contains_key("ANTHROPIC_API_KEY")
{
let provider_name = config.default_provider.as_deref().unwrap_or("");
let candidates: Vec<&str> = if provider_name.is_empty() {
vec!["openai", "anthropic"]
} else {
vec![provider_name, "openai", "anthropic"]
};
for candidate in candidates {
if let Some(key) = crate::providers::resolve_provider_credential(candidate, None) {
let kumiho_provider = match candidate {
c if c.contains("anthropic") => "anthropic",
_ => "openai",
};
server.env.insert("KUMIHO_LLM_API_KEY".to_string(), key);
server.env.insert(
"KUMIHO_LLM_PROVIDER".to_string(),
kumiho_provider.to_string(),
);
break;
}
}
}
config.mcp.servers.insert(0, server);
}
config
}
pub fn substitute_project_names(template: &str, config: &Config) -> String {
let mem = &config.kumiho.memory_project;
let har = &config.kumiho.harness_project;
let mut out = template.to_string();
if mem != "CognitiveMemory" {
out = out.replace("CognitiveMemory", mem);
}
if har != "Construct" {
out = out.replace("Construct/", &format!("{har}/"));
}
out = out.replace(
"{{KUMIHO_MEMORY_RETRIEVAL_LIMIT}}",
&config.kumiho.memory_retrieval_limit.max(1).to_string(),
);
out
}
pub fn append_kumiho_bootstrap(
system_prompt: &mut String,
config: &Config,
is_internal: bool,
advanced_available: bool,
) {
if is_internal || !config.kumiho.enabled {
return;
}
if system_prompt.contains("SESSION-START INSTRUCTION (kumiho-memory") {
return; }
let template = if advanced_available {
KUMIHO_BOOTSTRAP_PROMPT
} else {
KUMIHO_BOOTSTRAP_PROMPT_LITE
};
let prompt = substitute_project_names(template, config);
system_prompt.push_str("\n\n---\n\n");
system_prompt.push_str(&prompt);
}
pub fn append_kumiho_channel_bootstrap(
system_prompt: &mut String,
config: &Config,
is_internal: bool,
advanced_available: bool,
) {
if is_internal || !config.kumiho.enabled {
return;
}
if system_prompt.contains("SESSION-START INSTRUCTION (kumiho-memory") {
return;
}
let template = if advanced_available {
KUMIHO_CHANNEL_BOOTSTRAP_PROMPT
} else {
KUMIHO_CHANNEL_BOOTSTRAP_PROMPT_LITE
};
let prompt = substitute_project_names(template, config);
system_prompt.push_str("\n\n---\n\n");
system_prompt.push_str(&prompt);
}
fn expand_tilde(path: &str) -> String {
let expanded = shellexpand::tilde(path);
let expanded_str = expanded.as_ref();
if expanded_str.starts_with('~') {
if let Some(user_dirs) = UserDirs::new() {
let home = user_dirs.home_dir();
if let Some(rest) = expanded_str.strip_prefix('~') {
return format!(
"{}{}{}",
home.display(),
if rest.starts_with('/') { "" } else { "/" },
rest.trim_start_matches('/')
);
}
}
}
expanded_str.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KumihoConfig;
#[test]
fn inject_kumiho_adds_server() {
let cfg = Config::default();
assert!(!cfg.mcp.servers.iter().any(|s| s.name == KUMIHO_SERVER_NAME));
let injected = inject_kumiho(cfg, false);
assert!(injected.mcp.enabled);
assert!(
injected
.mcp
.servers
.iter()
.any(|s| s.name == KUMIHO_SERVER_NAME)
);
}
#[test]
fn append_kumiho_bootstrap_adds_prompt() {
let cfg = Config::default();
let mut prompt = "## Identity\n\nYou are Construct.".to_string();
append_kumiho_bootstrap(&mut prompt, &cfg, false, true);
assert!(prompt.contains("SESSION-START INSTRUCTION (kumiho-memory"));
assert!(prompt.contains("kumiho_memory_engage"));
assert!(prompt.contains("kumiho_memory_reflect"));
}
#[test]
fn append_kumiho_bootstrap_lite_when_advanced_unavailable() {
let cfg = Config::default();
let mut prompt = String::new();
append_kumiho_bootstrap(&mut prompt, &cfg, false, false);
assert!(prompt.contains("SESSION-START INSTRUCTION (kumiho-memory"));
assert!(prompt.contains("lite mode"));
assert!(prompt.contains("kumiho_memory_store"));
assert!(prompt.contains("kumiho_memory_retrieve"));
assert!(!prompt.contains("kumiho_memory_engage"));
assert!(!prompt.contains("kumiho_memory_reflect"));
assert!(!prompt.contains("kumiho_memory_recall"));
assert!(!prompt.contains("kumiho_memory_consolidate"));
assert!(!prompt.contains("kumiho_memory_dream_state"));
}
#[test]
fn bootstrap_prompts_have_no_bare_legacy_memory_tool_names() {
for prompt in &[
KUMIHO_BOOTSTRAP_PROMPT,
KUMIHO_BOOTSTRAP_PROMPT_LITE,
KUMIHO_CHANNEL_BOOTSTRAP_PROMPT,
KUMIHO_CHANNEL_BOOTSTRAP_PROMPT_LITE,
] {
for stale in &[
"memory_store",
"memory_recall",
"memory_forget",
"memory_search",
] {
let mut search_start = 0usize;
while let Some(idx) = prompt[search_start..].find(stale) {
let abs = search_start + idx;
let preceded_by_kumiho =
abs >= "kumiho_".len() && &prompt[abs - "kumiho_".len()..abs] == "kumiho_";
assert!(
preceded_by_kumiho,
"bare legacy tool name '{stale}' found in bootstrap prompt at offset {abs}: \
{snippet}",
snippet = &prompt
[abs.saturating_sub(40)..(abs + stale.len() + 40).min(prompt.len())],
);
search_start = abs + stale.len();
}
}
}
}
#[test]
fn bootstrap_prompt_has_no_phantom_paseo_refs() {
for fragment in &[
"kumiho-memory:kumiho-memory",
"kumiho_get_revision_by_tag",
"Identity is already loaded",
] {
assert!(
!KUMIHO_BOOTSTRAP_PROMPT.contains(fragment),
"phantom Paseo fragment '{fragment}' must not appear in KUMIHO_BOOTSTRAP_PROMPT"
);
assert!(
!KUMIHO_BOOTSTRAP_PROMPT_LITE.contains(fragment),
"phantom Paseo fragment '{fragment}' must not appear in KUMIHO_BOOTSTRAP_PROMPT_LITE"
);
assert!(
!KUMIHO_CHANNEL_BOOTSTRAP_PROMPT.contains(fragment),
"phantom Paseo fragment '{fragment}' must not appear in KUMIHO_CHANNEL_BOOTSTRAP_PROMPT"
);
assert!(
!KUMIHO_CHANNEL_BOOTSTRAP_PROMPT_LITE.contains(fragment),
"phantom Paseo fragment '{fragment}' must not appear in KUMIHO_CHANNEL_BOOTSTRAP_PROMPT_LITE"
);
}
}
#[test]
fn append_kumiho_bootstrap_is_idempotent() {
let cfg = Config::default();
let mut prompt = String::new();
append_kumiho_bootstrap(&mut prompt, &cfg, false, true);
let after_first = prompt.len();
append_kumiho_bootstrap(&mut prompt, &cfg, false, true);
assert_eq!(prompt.len(), after_first);
}
#[test]
fn inject_kumiho_skips_internal_agents() {
let cfg = Config::default();
let original_servers = cfg.mcp.servers.len();
let unchanged = inject_kumiho(cfg, true);
assert_eq!(unchanged.mcp.servers.len(), original_servers);
}
#[test]
fn inject_kumiho_is_idempotent() {
let cfg = Config::default();
let once = inject_kumiho(cfg, false);
let count_after_once = once
.mcp
.servers
.iter()
.filter(|s| s.name == KUMIHO_SERVER_NAME)
.count();
let twice = inject_kumiho(once, false);
let count_after_twice = twice
.mcp
.servers
.iter()
.filter(|s| s.name == KUMIHO_SERVER_NAME)
.count();
assert_eq!(count_after_once, count_after_twice);
}
#[test]
fn inject_kumiho_respects_disabled_flag() {
let mut cfg = Config::default();
cfg.kumiho.enabled = false;
let unchanged = inject_kumiho(cfg, false);
assert!(
!unchanged
.mcp
.servers
.iter()
.any(|s| s.name == KUMIHO_SERVER_NAME)
);
}
#[test]
fn kumiho_mcp_server_config_uses_custom_path() {
let kc = KumihoConfig {
enabled: true,
mcp_path: "/opt/kumiho/run_kumiho_mcp.py".to_string(),
space_prefix: "MyProject".to_string(),
api_url: "http://localhost:8000".to_string(),
memory_project: "CognitiveMemory".to_string(),
harness_project: "Construct".to_string(),
memory_retrieval_limit: 7,
};
let server = kumiho_mcp_server_config(&kc);
assert_eq!(server.command, "python3");
assert_eq!(server.args, vec!["/opt/kumiho/run_kumiho_mcp.py"]);
assert_eq!(
server.env.get("KUMIHO_SPACE_PREFIX").map(|s| s.as_str()),
Some("MyProject")
);
assert_eq!(
server
.env
.get("KUMIHO_MEMORY_RETRIEVAL_LIMIT")
.map(|s| s.as_str()),
Some("7")
);
}
#[test]
fn substitute_project_names_with_defaults_fills_retrieval_limit() {
let cfg = Config::default();
let result = substitute_project_names(KUMIHO_BOOTSTRAP_PROMPT, &cfg);
assert!(result.contains("CognitiveMemory"));
assert!(result.contains("Construct/"));
assert!(result.contains("limit=3"));
assert!(!result.contains("{{KUMIHO_MEMORY_RETRIEVAL_LIMIT}}"));
}
#[test]
fn substitute_project_names_replaces_memory_project() {
let mut cfg = Config::default();
cfg.kumiho.memory_project = "MyMemory".to_string();
let result = substitute_project_names(KUMIHO_BOOTSTRAP_PROMPT, &cfg);
assert!(result.contains("MyMemory"));
assert!(!result.contains("CognitiveMemory"));
assert!(result.contains("Construct/"));
}
#[test]
fn substitute_project_names_replaces_harness_project() {
let mut cfg = Config::default();
cfg.kumiho.harness_project = "MyHarness".to_string();
let result = substitute_project_names(KUMIHO_BOOTSTRAP_PROMPT, &cfg);
assert!(result.contains("MyHarness/"));
assert!(!result.contains("Construct/"));
assert!(result.contains("CognitiveMemory"));
}
#[test]
fn substitute_project_names_replaces_both() {
let mut cfg = Config::default();
cfg.kumiho.memory_project = "ProdMemory".to_string();
cfg.kumiho.harness_project = "ProdHarness".to_string();
let result = substitute_project_names(KUMIHO_BOOTSTRAP_PROMPT, &cfg);
assert!(result.contains("ProdMemory"));
assert!(result.contains("ProdHarness/"));
assert!(!result.contains("CognitiveMemory"));
assert!(!result.contains("Construct/"));
}
#[test]
fn substitute_project_names_works_on_channel_prompt() {
let mut cfg = Config::default();
cfg.kumiho.memory_project = "ChannelMem".to_string();
let result = substitute_project_names(KUMIHO_CHANNEL_BOOTSTRAP_PROMPT, &cfg);
assert!(result.contains("ChannelMem"));
assert!(!result.contains("CognitiveMemory"));
}
#[test]
fn append_kumiho_bootstrap_substitutes_custom_projects() {
let mut cfg = Config::default();
cfg.kumiho.memory_project = "CustomMem".to_string();
cfg.kumiho.harness_project = "CustomHarness".to_string();
let mut prompt = String::new();
append_kumiho_bootstrap(&mut prompt, &cfg, false, true);
assert!(prompt.contains("CustomMem"));
assert!(prompt.contains("CustomHarness/"));
assert!(!prompt.contains("CognitiveMemory"));
assert!(!prompt.contains("Construct/"));
}
#[test]
fn registry_probe_empty_registry_is_unavailable() {
assert!(!registry_has_advanced_kumiho_tools(&[]));
}
#[test]
fn registry_probe_unprefixed_name_is_unavailable() {
let names = vec!["kumiho_memory_engage".to_string()];
assert!(!registry_has_advanced_kumiho_tools(&names));
}
#[test]
fn registry_probe_only_bare_tools_is_unavailable() {
let names = vec![
"kumiho-memory__kumiho_memory_store".to_string(),
"kumiho-memory__kumiho_memory_retrieve".to_string(),
"kumiho-memory__kumiho_search_items".to_string(),
];
assert!(!registry_has_advanced_kumiho_tools(&names));
}
#[test]
fn registry_probe_detects_advanced_tool_when_present() {
let names = vec![
"kumiho-memory__kumiho_memory_store".to_string(),
"kumiho-memory__kumiho_memory_engage".to_string(),
"operator__delegate".to_string(),
];
assert!(registry_has_advanced_kumiho_tools(&names));
}
#[test]
fn warn_if_kumiho_advanced_missing_skips_when_disabled() {
let mut cfg = Config::default();
cfg.kumiho.enabled = false;
warn_if_kumiho_advanced_missing(&cfg, false);
warn_if_kumiho_advanced_missing(&cfg, true);
}
#[test]
fn warn_if_kumiho_advanced_missing_skips_when_available() {
let cfg = Config::default(); warn_if_kumiho_advanced_missing(&cfg, true);
}
}