§9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed

The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
This commit is contained in:
2026-07-18 02:48:46 -04:00
parent 86e3724310
commit 6f31415ed6
1056 changed files with 8410 additions and 18307 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ mod acp_send_failure_tests {
fn ext_request() -> acp::ExtRequest {
acp::ExtRequest::new(
"x.ai/test",
"kigi/test",
serde_json::value::to_raw_value(&serde_json::json!({}))
.unwrap()
.into(),
@@ -1,6 +1,6 @@
//! Dedicated-thread reader for the ACP stdio transport's standard input.
//!
//! Every ACP client (VS Code extension, grok-desktop, the leader bridge) drives
//! Every ACP client (VS Code extension, kigi-desktop, the leader bridge) drives
//! the agent over a **persistent, bidirectional** newline-delimited JSON-RPC
//! stream on stdio: it writes requests on the child's stdin and reads responses
//! on stdout, keeping **stdin open for the whole session**.
@@ -4,7 +4,7 @@ use crate::send::contributors::command::{
CommandAction, CommandContributor, CommandInvocation, CommandSpec,
};
/// `?Send` twin of [`CommandContributor`] for single-threaded hosts like grok build's TUI agent, whose session state is `Rc`/`RefCell`-based and can
/// `?Send` twin of [`CommandContributor`] for single-threaded hosts like kigi build's TUI agent, whose session state is `Rc`/`RefCell`-based and can
/// never satisfy the `Send` bounds the send flavor bakes into its boxed hook futures.
#[async_trait(?Send)]
pub trait LocalCommandContributor {
@@ -4,7 +4,7 @@ use crate::send::contributors::turn_input::{
TurnInputContext, TurnInputContributor, TurnInputFragment,
};
/// `?Send` twin of [`TurnInputContributor`] for single-threaded hosts like grok build's TUI agent, whose session state is `Rc`/`RefCell`-based
/// `?Send` twin of [`TurnInputContributor`] for single-threaded hosts like kigi build's TUI agent, whose session state is `Rc`/`RefCell`-based
/// and can never satisfy the `Send` bounds the send flavor bakes into its boxed hook futures.
#[async_trait(?Send)]
pub trait LocalTurnInputContributor {
@@ -4,7 +4,7 @@ use crate::send::contributors::turn_lifecycle::{
TurnAbortInput, TurnDoneInput, TurnErrorInput, TurnLifecycleContributor, TurnStartInput,
};
/// `?Send` twin of [`TurnLifecycleContributor`] for single-threaded hosts like grok build's TUI
/// `?Send` twin of [`TurnLifecycleContributor`] for single-threaded hosts like kigi build's TUI
/// agent, whose session state is `Rc`/`RefCell`-based and can never satisfy the `Send` bounds the
/// send flavor bakes into its boxed hook futures.
#[async_trait(?Send)]
+2 -2
View File
@@ -234,7 +234,7 @@ Agent definitions are discovered from multiple locations with priority:
2. **User-level**: `~/.kigi/agents/*.md`
3. **Compat paths** (lowest priority): additional vendor agent
directories under the user home (when enabled)
4. **Built-in**: `default_grok_build()`, `browser_use()`
4. **Built-in**: `default_kigi()`, `browser_use()`
Name-based dedup ensures the highest-priority definition wins. For
example, a project `.kigi/agents/code-reviewer.md` shadows a
@@ -275,7 +275,7 @@ user-level definition with the same name.
| Name | Prompt Mode | Description |
|---|---|---|
| `grok-build` | extend | Default agent for software engineering tasks |
| `kigi` | extend | Default agent for software engineering tasks |
| `browser-use` | full | Web browsing and interaction agent |
## Error Handling
+78 -92
View File
@@ -15,8 +15,8 @@ use kigi_tools::types::tool::ToolKind;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
/// The Grok [`ToolKind`] a vendor-compat `tools:` allowlist entry resolves to, so
/// a plugin's upstream allowlist still binds. Backed by the shared vendor-to-Grok
/// The Kigi [`ToolKind`] a vendor-compat `tools:` allowlist entry resolves to, so
/// a plugin's upstream allowlist still binds. Backed by the shared vendor-to-Kigi
/// tool registry in `kigi-tools` (also used by the hook matcher).
fn claude_tool_kind(name: &str) -> Option<ToolKind> {
kigi_tools::types::kind_for(name)
@@ -55,7 +55,7 @@ pub struct AgentBuilder {
notification_handle: ToolNotificationHandle,
owner_session_id: Option<String>,
parent_scheduler_handle:
Option<kigi_tools::implementations::grok_build::scheduler::types::SchedulerHandle>,
Option<kigi_tools::implementations::kigi::scheduler::types::SchedulerHandle>,
/// The agent definition — set via from_definition() or built up
/// via individual with_*() calls.
definition: Option<AgentDefinition>,
@@ -91,10 +91,10 @@ pub struct AgentBuilder {
/// tools for execution by the agentic sampler, instead of being
/// registered as local Function tools.
backend_search: bool,
web_fetch_config: kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig,
web_fetch_config: kigi_tools::implementations::kigi::web_fetch::WebFetchConfig,
lsp: Option<std::sync::Arc<dyn kigi_tools::implementations::lsp::LspBackend>>,
app_builder_deployer_config:
kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig,
kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig,
write_file_enabled: bool,
subagents_enabled: bool,
ask_user_question_enabled: bool,
@@ -135,27 +135,21 @@ pub struct AgentBuilder {
/// Ensure plan mode tools (`enter_plan_mode`, `exit_plan_mode`,
/// `ask_user_question`) are present in the tool config.
fn ensure_plan_mode_tools(tool_config: &mut kigi_tools::registry::types::ToolServerConfig) {
use kigi_tools::implementations::grok_build;
use kigi_tools::implementations::kigi;
let existing: std::collections::HashSet<&str> =
tool_config.tools.iter().map(|tc| tc.id.as_str()).collect();
let missing_enter = !existing.contains("GrokBuild:enter_plan_mode");
let missing_exit = !existing.contains("GrokBuild:exit_plan_mode");
let missing_ask = !existing.contains("GrokBuild:ask_user_question");
let missing_enter = !existing.contains("Kigi:enter_plan_mode");
let missing_exit = !existing.contains("Kigi:exit_plan_mode");
let missing_ask = !existing.contains("Kigi:ask_user_question");
drop(existing);
if missing_enter {
tool_config
.tools
.push((&grok_build::EnterPlanModeTool).into());
tool_config.tools.push((&kigi::EnterPlanModeTool).into());
}
if missing_exit {
tool_config
.tools
.push((&grok_build::ExitPlanModeTool).into());
tool_config.tools.push((&kigi::ExitPlanModeTool).into());
}
if missing_ask {
tool_config
.tools
.push((&grok_build::AskUserQuestionTool).into());
tool_config.tools.push((&kigi::AskUserQuestionTool).into());
}
}
/// Merge a shell-resolved params map into every matching tool's
@@ -401,7 +395,7 @@ impl AgentBuilder {
/// Share the parent's scheduler handle so scheduled tasks survive subagent exit.
pub fn with_parent_scheduler_handle(
mut self,
handle: kigi_tools::implementations::grok_build::scheduler::types::SchedulerHandle,
handle: kigi_tools::implementations::kigi::scheduler::types::SchedulerHandle,
) -> Self {
self.parent_scheduler_handle = Some(handle);
self
@@ -435,7 +429,7 @@ impl AgentBuilder {
/// `KIGI_WEB_FETCH` env var.
pub fn with_web_fetch_config(
mut self,
config: kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig,
config: kigi_tools::implementations::kigi::web_fetch::WebFetchConfig,
) -> Self {
self.web_fetch_config = config;
self
@@ -450,7 +444,7 @@ impl AgentBuilder {
/// Set the deploy service configuration.
pub fn with_app_builder_deployer_config(
mut self,
config: kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig,
config: kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig,
) -> Self {
self.app_builder_deployer_config = config;
self
@@ -501,14 +495,14 @@ impl AgentBuilder {
self.subagents_enabled = enabled;
self
}
/// Set public model slugs advertised in the GrokBuild Task description.
/// Set public model slugs advertised in the Kigi Task description.
pub fn with_task_model_slugs(mut self, slugs: Vec<String>) -> Self {
self.task_model_slugs = slugs;
self
}
/// Enable or disable the `ask_user_question` tool.
///
/// When disabled, `GrokBuild:ask_user_question` is stripped from the
/// When disabled, `Kigi:ask_user_question` is stripped from the
/// agent's tool config after `ensure_plan_mode_tools` injection, so
/// the model cannot ask the user structured questions regardless of
/// which built-in profile is in use. Driven by the shell's resolved gate
@@ -537,7 +531,7 @@ impl AgentBuilder {
}
/// Set the skills config (custom paths, ignore globs) from config.toml.
/// Without this, only auto-discovered skills (cwd/.kigi/skills, ~/.kigi/skills)
/// are included — custom paths added via `x.ai/skills/add` would be ignored.
/// are included — custom paths added via `kigi/skills/add` would be ignored.
pub fn with_skills_config(mut self, config: crate::prompt::skills::SkillsConfig) -> Self {
self.skills_config = config;
self
@@ -585,7 +579,7 @@ impl AgentBuilder {
if let Some(ref def) = self.definition {
return def.clone();
}
let mut def = AgentDefinition::default_grok_build();
let mut def = AgentDefinition::default_kigi();
if let Some(ref name) = self.name {
def.name = name.clone();
}
@@ -668,17 +662,17 @@ impl AgentBuilder {
.push((&memory::get_tool::MemoryGetImpl).into());
}
if self.web_search_config.is_enabled() {
use kigi_tools::implementations::grok_build;
tool_config.tools.push((&grok_build::WebSearchTool).into());
use kigi_tools::implementations::kigi;
tool_config.tools.push((&kigi::WebSearchTool).into());
}
if self.web_fetch_config.is_enabled() {
use kigi_tools::implementations::grok_build;
tool_config.tools.push((&grok_build::WebFetchTool).into());
use kigi_tools::implementations::kigi;
tool_config.tools.push((&kigi::WebFetchTool).into());
}
if self.lsp.is_some() {
tool_config
.tools
.push((&kigi_tools::implementations::grok_build::LspTool).into());
.push((&kigi_tools::implementations::kigi::LspTool).into());
}
let has_write_tool = tool_config
.tools
@@ -692,13 +686,13 @@ impl AgentBuilder {
ensure_plan_mode_tools(&mut tool_config);
}
if self.memory_backend.is_none() {
let grok_build_ns = kigi_tools::types::tool::ToolNamespace::GrokBuild.to_string();
let kigi_ns = kigi_tools::types::tool::ToolNamespace::Kigi.to_string();
let mem_search_id = format!(
"{grok_build_ns}:{}",
"{kigi_ns}:{}",
kigi_tools::implementations::memory::MEMORY_SEARCH_TOOL_NAME
);
let mem_get_id = format!(
"{grok_build_ns}:{}",
"{kigi_ns}:{}",
kigi_tools::implementations::memory::MEMORY_GET_TOOL_NAME
);
tool_config
@@ -708,13 +702,13 @@ impl AgentBuilder {
if !self.ask_user_question_enabled {
let ask_user_id = format!(
"{}:ask_user_question",
kigi_tools::types::tool::ToolNamespace::GrokBuild,
kigi_tools::types::tool::ToolNamespace::Kigi,
);
tool_config.tools.retain(|tc| tc.id != ask_user_id);
}
let task_tool_id = format!(
"{}:{}",
kigi_tools::types::tool::ToolNamespace::GrokBuild,
kigi_tools::types::tool::ToolNamespace::Kigi,
"task"
);
let mut task_stripped = false;
@@ -762,8 +756,8 @@ impl AgentBuilder {
.unwrap_or(true))
})
};
if !has_satisfier(ToolNamespace::GrokBuild, "run_terminal_cmd", true)
&& !has_satisfier(ToolNamespace::GrokBuildConcise, "run_terminal_cmd", true)
if !has_satisfier(ToolNamespace::Kigi, "run_terminal_cmd", true)
&& !has_satisfier(ToolNamespace::KigiConcise, "run_terminal_cmd", true)
&& !has_satisfier(ToolNamespace::OpenCode, "bash", false)
{
let lifecycle = ["get_task_output", "wait_tasks", "kill_task"];
@@ -772,30 +766,22 @@ impl AgentBuilder {
.retain(|tc| !lifecycle.contains(&short_tool_name(&tc.id)));
}
}
if let kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig::Enabled {
ref params,
} = self.web_fetch_config
if let kigi_tools::implementations::kigi::web_fetch::WebFetchConfig::Enabled { ref params } =
self.web_fetch_config
&& let Ok(params_value) = serde_json::to_value(params)
&& let Some(obj) = params_value.as_object()
{
merge_tool_params(&mut tool_config, &["GrokBuild:web_fetch"], obj);
merge_tool_params(&mut tool_config, &["Kigi:web_fetch"], obj);
}
if let Some(ref bash_params) = self.bash_params_json {
merge_tool_params(
&mut tool_config,
&[
"GrokBuild:run_terminal_cmd",
"GrokBuildConcise:run_terminal_cmd",
],
&["Kigi:run_terminal_cmd", "KigiConcise:run_terminal_cmd"],
bash_params,
);
}
if let Some(ref ask_params) = self.ask_user_question_params_json {
merge_tool_params(
&mut tool_config,
&["GrokBuild:ask_user_question"],
ask_params,
);
merge_tool_params(&mut tool_config, &["Kigi:ask_user_question"], ask_params);
}
if !definition.disallowed_tools.is_empty() {
let before: std::collections::HashSet<String> =
@@ -878,7 +864,7 @@ impl AgentBuilder {
tracing::warn!(
agent = % definition.name, unresolved = ? unresolved, allowed = ?
definition.tools,
"tools allowlist had unmappable entries; keeping full grok toolset"
"tools allowlist had unmappable entries; keeping full kigi toolset"
);
}
}
@@ -1419,7 +1405,7 @@ mod tests {
.contains("If the user does not explicitly request a model, omit `${{ params.task.model }}` to inherit the parent model.")
);
assert!(!desc.contains("Available model slugs:"));
assert!(!desc.contains(concat!("grok", " models")));
assert!(!desc.contains(concat!("kigi", " models")));
}
#[test]
fn build_task_description_handles_empty_model_catalog() {
@@ -1431,7 +1417,7 @@ mod tests {
let desc = build_task_description(&subagents, &[]);
assert!(desc.contains("No explicit model slugs are currently available."));
assert!(desc.contains("Omit `${{ params.task.model }}` to inherit the parent model."));
assert!(!desc.contains(concat!("grok", " models")));
assert!(!desc.contains(concat!("kigi", " models")));
}
#[test]
fn task_model_guidance_resolves_model_param_override() {
@@ -1525,62 +1511,62 @@ mod tests {
}
let cases: &[PagerFlagCase] = &[
PagerFlagCase {
label: "grok-build / subagents+ask_user",
profile: AgentDefinition::default_grok_build,
label: "kigi / subagents+ask_user",
profile: AgentDefinition::default_kigi,
subagents: true,
ask_user: true,
},
PagerFlagCase {
label: "grok-build / subagents / no-ask-user",
profile: AgentDefinition::default_grok_build,
label: "kigi / subagents / no-ask-user",
profile: AgentDefinition::default_kigi,
subagents: true,
ask_user: false,
},
PagerFlagCase {
label: "grok-build / no-subagents / ask_user",
profile: AgentDefinition::default_grok_build,
label: "kigi / no-subagents / ask_user",
profile: AgentDefinition::default_kigi,
subagents: false,
ask_user: true,
},
PagerFlagCase {
label: "grok-build / no-subagents / no-ask-user",
profile: AgentDefinition::default_grok_build,
label: "kigi / no-subagents / no-ask-user",
profile: AgentDefinition::default_kigi,
subagents: false,
ask_user: false,
},
PagerFlagCase {
label: "grok-build-ask-user / subagents",
profile: AgentDefinition::grok_build_ask_user,
label: "kigi-ask-user / subagents",
profile: AgentDefinition::kigi_ask_user,
subagents: true,
ask_user: true,
},
PagerFlagCase {
label: "grok-build-ask-user / no-subagents",
profile: AgentDefinition::grok_build_ask_user,
label: "kigi-ask-user / no-subagents",
profile: AgentDefinition::kigi_ask_user,
subagents: false,
ask_user: true,
},
PagerFlagCase {
label: "grok-build-plan",
profile: AgentDefinition::grok_build_plan,
label: "kigi-plan",
profile: AgentDefinition::kigi_plan,
subagents: true,
ask_user: true,
},
PagerFlagCase {
label: "grok-build-plan / no-ask-user",
profile: AgentDefinition::grok_build_plan,
label: "kigi-plan / no-ask-user",
profile: AgentDefinition::kigi_plan,
subagents: true,
ask_user: false,
},
PagerFlagCase {
label: "grok-build-plan-no-subagents",
profile: AgentDefinition::grok_build_plan_no_subagents,
label: "kigi-plan-no-subagents",
profile: AgentDefinition::kigi_plan_no_subagents,
subagents: false,
ask_user: true,
},
PagerFlagCase {
label: "grok-build-plan-no-subagents / no-ask-user",
profile: AgentDefinition::grok_build_plan_no_subagents,
label: "kigi-plan-no-subagents / no-ask-user",
profile: AgentDefinition::kigi_plan_no_subagents,
subagents: false,
ask_user: false,
},
@@ -1629,7 +1615,7 @@ mod tests {
async fn curated_empty_toolset_fails_agent_build() {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::notification::ToolNotificationHandle;
let mut profile = crate::config::AgentDefinition::default_grok_build();
let mut profile = crate::config::AgentDefinition::default_kigi();
profile.tool_config = Default::default();
profile.inject_default_tools = false;
let result = AgentBuilder::new(
@@ -1657,16 +1643,16 @@ mod tests {
#[tokio::test]
async fn plan_mode_injected_ask_user_question_receives_params() {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::implementations::grok_build::ask_user_question::AskUserQuestionParams;
use kigi_tools::implementations::kigi::ask_user_question::AskUserQuestionParams;
use kigi_tools::notification::ToolNotificationHandle;
use kigi_tools::types::resources::Params;
let profile = crate::config::AgentDefinition::default_grok_build();
let profile = crate::config::AgentDefinition::default_kigi();
assert!(
!profile
.tool_config
.tools
.iter()
.any(|tc| tc.id == "GrokBuild:ask_user_question"),
.any(|tc| tc.id == "Kigi:ask_user_question"),
"test premise: the profile must not pre-declare ask_user_question"
);
let mut params = serde_json::Map::new();
@@ -1693,7 +1679,7 @@ mod tests {
async fn build_with_tools(tools: Vec<String>, disallowed: Vec<String>) -> crate::agent::Agent {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::notification::ToolNotificationHandle;
let mut def = crate::config::AgentDefinition::default_grok_build();
let mut def = crate::config::AgentDefinition::default_kigi();
def.tools = tools;
def.disallowed_tools = disallowed;
AgentBuilder::new(
@@ -1714,7 +1700,7 @@ mod tests {
) -> Vec<String> {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::notification::ToolNotificationHandle;
let mut def = crate::config::AgentDefinition::default_grok_build();
let mut def = crate::config::AgentDefinition::default_kigi();
def.tools = own_tools;
def.session_tools_allowlist = Some(session_allow);
let agent = AgentBuilder::new(
@@ -1791,7 +1777,7 @@ mod tests {
let mut def = crate::config::AgentDefinition::general_purpose();
assert!(def.session_tools_allowed("read_file"));
def.session_tools_allowlist = Some(vec!["read_file".into()]);
assert!(def.session_tools_allowed("GrokBuild:read_file"));
assert!(def.session_tools_allowed("Kigi:read_file"));
assert!(!def.session_tools_allowed("grep"));
def.session_tools_denylist = Some(vec!["read_file".into()]);
assert!(!def.session_tools_allowed("read_file"));
@@ -1848,7 +1834,7 @@ mod tests {
assert_eq!(agent.definition().allowed_subagent_types, None);
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::notification::ToolNotificationHandle;
let mut def = crate::config::AgentDefinition::default_grok_build();
let mut def = crate::config::AgentDefinition::default_kigi();
def.disallowed_tools = vec!["Agent".into()];
let agent = AgentBuilder::new(
std::env::temp_dir(),
@@ -1864,10 +1850,10 @@ mod tests {
#[tokio::test]
async fn spawning_blocked_disables_all_background_bash_modes() {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::implementations::grok_build::bash::BashParams;
use kigi_tools::implementations::kigi::bash::BashParams;
use kigi_tools::notification::ToolNotificationHandle;
use kigi_tools::types::resources::Params;
let mut definition = crate::config::AgentDefinition::default_grok_build();
let mut definition = crate::config::AgentDefinition::default_kigi();
definition.tools = vec!["run_terminal_cmd".into()];
let bash_params = serde_json::json!(
{ "max_timeout_secs" : 36_000.0, "auto_background_on_timeout" : true,
@@ -1907,10 +1893,10 @@ mod tests {
Some(vec!["worker".into()])
);
}
/// Compat allowlist names (`Read`, `Bash`, `Grep`) map to their Grok
/// Compat allowlist names (`Read`, `Bash`, `Grep`) map to their Kigi
/// equivalents by `ToolKind` — a real restricted toolset, not zero tools.
#[tokio::test]
async fn claude_tool_names_map_to_grok_equivalents() {
async fn claude_tool_names_map_to_kigi_equivalents() {
let tools = vec!["Read".into(), "Bash".into(), "Grep".into()];
let agent = build_with_tools(tools, vec![]).await;
let names: Vec<String> = agent
@@ -1936,7 +1922,7 @@ mod tests {
"Edit must be excluded by the allowlist; got: {names:?}"
);
}
/// Shell, LSP, ask, and task-lifecycle tool names resolve to their grok
/// Shell, LSP, ask, and task-lifecycle tool names resolve to their kigi
/// `ToolKind`, so those allowlists are honored instead of failing open.
#[test]
fn shell_lsp_ask_and_task_tool_names_map() {
@@ -2045,7 +2031,7 @@ mod tests {
);
}
/// A restrictive allowlist must never strip MCP access. Compat allowlists
/// treat `mcp__*` as always-on, so grok keeps the MCP meta-tools
/// treat `mcp__*` as always-on, so kigi keeps the MCP meta-tools
/// (`search_tool` / `use_tool`) regardless of what the allowlist names.
#[tokio::test]
async fn restrictive_allowlist_keeps_mcp_access() {
@@ -2098,10 +2084,10 @@ mod tests {
#[tokio::test]
async fn requested_enabled_web_tools_survive_allowlist() {
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig;
use kigi_tools::implementations::kigi::web_fetch::WebFetchConfig;
use kigi_tools::implementations::web_search::WebSearchConfig;
use kigi_tools::notification::ToolNotificationHandle;
let mut definition = crate::config::AgentDefinition::default_grok_build();
let mut definition = crate::config::AgentDefinition::default_kigi();
definition.tools = vec![
"read_file".into(),
"grep".into(),
@@ -2139,7 +2125,7 @@ mod tests {
assert!(!names.contains(&excluded.to_string()), "got: {names:?}");
}
}
/// grok-build toolsets have no Skill tool — skills are read from
/// kigi toolsets have no Skill tool — skills are read from
/// `SKILL.md` via `read_file` — so a compat `Skill` allowlist entry grants
/// toolset.
#[tokio::test]
@@ -2209,7 +2195,7 @@ mod tests {
"no full-toolset fallback — unlisted tools must be excluded; got: {names:?}"
);
}
/// Compat `ToolSearch` meta-tool maps to grok's `search_tool` (MCP
/// Compat `ToolSearch` meta-tool maps to kigi's `search_tool` (MCP
/// is a filter (`retain`) over a `HashSet` of kinds, not an inserter — so the
/// falling back to the full toolset.
#[tokio::test]
@@ -2247,7 +2233,7 @@ mod tests {
} else {
WebSearchConfig::Disabled
};
let mut def = crate::config::AgentDefinition::default_grok_build();
let mut def = crate::config::AgentDefinition::default_kigi();
def.disallowed_tools = disallowed_tools.iter().map(|s| s.to_string()).collect();
AgentBuilder::new(
std::env::temp_dir(),
File diff suppressed because it is too large Load Diff
+52 -53
View File
@@ -72,8 +72,8 @@ pub enum SubagentSource {
/// `visible == callable` guarantee)
/// 4. Filter: remove agents toggled off via `[subagents.toggle]`
pub fn all_subagents(cwd: &Path, toggle: &HashMap<String, bool>) -> Vec<SubagentEntry> {
let grok = kigi_config::user_kigi_home();
all_subagents_with_home(cwd, toggle, dirs::home_dir().as_deref(), grok.as_deref())
let kigi = kigi_config::user_kigi_home();
all_subagents_with_home(cwd, toggle, dirs::home_dir().as_deref(), kigi.as_deref())
}
fn all_subagents_with_home(
@@ -189,7 +189,7 @@ fn merge_subagents(
/// 4. `~/.kigi/bundled/agents/` (bundled, lowest priority)
///
/// Deduplicates by name — higher-priority definitions win.
/// User-level agent directories in priority order: user grok agents, `.claude`
/// User-level agent directories in priority order: user kigi agents, `.claude`
/// compat agents, then bundled. `.kigi` dirs resolve from `kigi_home`
/// (KIGI_SHARE_DIR-aware) plus the legacy literal `~/.kigi` when KIGI_SHARE_DIR points
/// elsewhere; `.claude` resolves from `home`.
@@ -200,7 +200,7 @@ pub(crate) fn user_agent_dirs(
// Legacy literal ~/.kigi, included only when it differs from kigi_home
// (i.e. KIGI_SHARE_DIR points elsewhere) so agents left in the old location are
// still discovered and stay consistent with scope_from_path classification.
let legacy_grok = home
let legacy_kigi = home
.map(|h| h.join(".kigi"))
.filter(|legacy| kigi_home != Some(legacy.as_path()));
@@ -208,7 +208,7 @@ pub(crate) fn user_agent_dirs(
if let Some(g) = kigi_home {
dirs.push((g.join("agents"), AgentScope::User));
}
if let Some(l) = &legacy_grok {
if let Some(l) = &legacy_kigi {
dirs.push((l.join("agents"), AgentScope::User));
}
if let Some(h) = home {
@@ -217,15 +217,15 @@ pub(crate) fn user_agent_dirs(
if let Some(g) = kigi_home {
dirs.push((g.join("bundled").join("agents"), AgentScope::Bundled));
}
if let Some(l) = &legacy_grok {
if let Some(l) = &legacy_kigi {
dirs.push((l.join("bundled").join("agents"), AgentScope::Bundled));
}
dirs
}
pub fn discover(cwd: &Path) -> Vec<AgentDefinition> {
let grok = kigi_config::user_kigi_home();
discover_with_home(cwd, dirs::home_dir().as_deref(), grok.as_deref())
let kigi = kigi_config::user_kigi_home();
discover_with_home(cwd, dirs::home_dir().as_deref(), kigi.as_deref())
}
fn discover_with_home(
@@ -251,8 +251,8 @@ fn discover_with_home(
///
/// Checks built-ins first, then user-level dirs, then bundled.
pub fn by_name(name: &str) -> Option<AgentDefinition> {
let grok = kigi_config::user_kigi_home();
by_name_with_home(name, dirs::home_dir().as_deref(), grok.as_deref())
let kigi = kigi_config::user_kigi_home();
by_name_with_home(name, dirs::home_dir().as_deref(), kigi.as_deref())
}
fn by_name_with_home(
@@ -260,8 +260,12 @@ fn by_name_with_home(
home: Option<&Path>,
kigi_home: Option<&Path>,
) -> Option<AgentDefinition> {
// Check built-ins first — type-safe via BuiltinAgentName strum enum
if let Ok(builtin) = BuiltinAgentName::from_str(name) {
// Check built-ins first — type-safe via BuiltinAgentName strum enum.
// Legacy pre-rebrand agent types (persisted in old session files) are
// mapped onto their current names first; see `canonical_agent_type`.
if let Ok(builtin) =
BuiltinAgentName::from_str(crate::config::canonical_agent_type(name).as_ref())
{
return Some(builtin.definition());
}
@@ -287,8 +291,8 @@ fn by_name_with_home(
/// Project-level `.kigi/agents/` has highest priority, then falls back
/// to built-ins, user-level, and finally bundled definitions.
pub fn by_name_in_cwd(name: &str, cwd: &Path) -> Option<AgentDefinition> {
let grok = kigi_config::user_kigi_home();
by_name_in_cwd_with_home(name, cwd, dirs::home_dir().as_deref(), grok.as_deref())
let kigi = kigi_config::user_kigi_home();
by_name_in_cwd_with_home(name, cwd, dirs::home_dir().as_deref(), kigi.as_deref())
}
fn by_name_in_cwd_with_home(
@@ -363,13 +367,13 @@ pub fn all_subagents_with_plugins(
toggle: &HashMap<String, bool>,
plugins: Option<&crate::plugins::PluginRegistry>,
) -> Vec<SubagentEntry> {
let grok = kigi_config::user_kigi_home();
let kigi = kigi_config::user_kigi_home();
all_subagents_with_plugins_and_home(
cwd,
toggle,
plugins,
dirs::home_dir().as_deref(),
grok.as_deref(),
kigi.as_deref(),
)
}
@@ -450,13 +454,13 @@ pub fn by_name_in_cwd_with_plugins(
cwd: &Path,
plugins: Option<&crate::plugins::PluginRegistry>,
) -> Option<AgentDefinition> {
let grok = kigi_config::user_kigi_home();
let kigi = kigi_config::user_kigi_home();
by_name_in_cwd_with_plugins_and_home(
name,
cwd,
plugins,
dirs::home_dir().as_deref(),
grok.as_deref(),
kigi.as_deref(),
)
}
@@ -533,7 +537,7 @@ fn by_name_in_cwd_with_plugins_and_home(
None
}
/// Expand `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` (and the Grok
/// Expand `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` (and the Kigi
/// aliases) in a plugin agent's body so the model receives absolute paths,
/// matching the expected load-time resolution for these variables.
fn substitute_plugin_vars(def: &mut AgentDefinition, plugin: &crate::plugins::LoadedPlugin) {
@@ -675,8 +679,8 @@ mod tests {
use crate::plugins::PluginOrigin;
match scope {
PluginScope::CliOverride => PluginOrigin::CliOverride,
PluginScope::Project => PluginOrigin::ProjectGrok,
PluginScope::User => PluginOrigin::UserGrok,
PluginScope::Project => PluginOrigin::ProjectKigi,
PluginScope::User => PluginOrigin::UserKigi,
PluginScope::ConfigPath => PluginOrigin::ConfigPath,
}
}
@@ -763,27 +767,27 @@ mod tests {
}
#[test]
fn user_agent_dirs_includes_legacy_grok_when_kigi_home_differs() {
fn user_agent_dirs_includes_legacy_kigi_when_kigi_home_differs() {
let home = Path::new("/home/u");
let grok = Path::new("/custom/grokhome");
let paths: Vec<_> = user_agent_dirs(Some(home), Some(grok))
let kigi = Path::new("/custom/kigihome");
let paths: Vec<_> = user_agent_dirs(Some(home), Some(kigi))
.into_iter()
.map(|(p, _)| p)
.collect();
assert!(paths.contains(&grok.join("agents")));
assert!(paths.contains(&kigi.join("agents")));
assert!(paths.contains(&home.join(".kigi").join("agents")));
assert!(paths.contains(&home.join(".claude").join("agents")));
assert!(paths.contains(&grok.join("bundled").join("agents")));
assert!(paths.contains(&kigi.join("bundled").join("agents")));
assert!(paths.contains(&home.join(".kigi").join("bundled").join("agents")));
}
#[test]
fn user_agent_dirs_dedups_legacy_when_kigi_home_is_dot_grok() {
fn user_agent_dirs_dedups_legacy_when_kigi_home_is_dot_kigi() {
let home = Path::new("/home/u");
let grok = home.join(".kigi");
let count = user_agent_dirs(Some(home), Some(&grok))
let kigi = home.join(".kigi");
let count = user_agent_dirs(Some(home), Some(&kigi))
.into_iter()
.filter(|(p, _)| *p == grok.join("agents"))
.filter(|(p, _)| *p == kigi.join("agents"))
.count();
assert_eq!(
count, 1,
@@ -807,10 +811,10 @@ mod tests {
}
#[test]
fn test_by_name_builtin_grok_build() {
let def = by_name("grok-build");
fn test_by_name_builtin_kigi() {
let def = by_name("kigi");
assert!(def.is_some());
assert_eq!(def.unwrap().name, "grok-build");
assert_eq!(def.unwrap().name, "kigi");
}
#[test]
@@ -1004,19 +1008,14 @@ mod tests {
let agents_dir = tmp.path().join(".kigi").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
// Create a project-level "grok-build" that shadows the built-in
write_agent_file(
&agents_dir,
"grok-build.md",
"grok-build",
"Custom grok-build",
);
// Create a project-level "kigi" that shadows the built-in
write_agent_file(&agents_dir, "kigi.md", "kigi", "Custom kigi");
let def = by_name_in_cwd("grok-build", tmp.path());
let def = by_name_in_cwd("kigi", tmp.path());
assert!(def.is_some());
let def = def.unwrap();
assert_eq!(def.name, "grok-build");
assert_eq!(def.description, "Custom grok-build");
assert_eq!(def.name, "kigi");
assert_eq!(def.description, "Custom kigi");
}
#[test]
@@ -1024,10 +1023,10 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
// No .kigi/agents/ directory — should fall back to built-in
let def = by_name_in_cwd("grok-build", tmp.path());
let def = by_name_in_cwd("kigi", tmp.path());
assert!(def.is_some());
let def = def.unwrap();
assert_eq!(def.name, "grok-build");
assert_eq!(def.name, "kigi");
// Should be the built-in, not a custom one
assert_eq!(def.scope, AgentScope::BuiltIn);
}
@@ -1048,11 +1047,11 @@ mod tests {
#[test]
fn test_orchestrator_from_str_resolves() {
use std::str::FromStr;
let variant = BuiltinAgentName::from_str("grok-build-orchestrator")
.expect("from_str must resolve grok-build-orchestrator");
assert_eq!(variant, BuiltinAgentName::GrokBuildOrchestrator);
let variant = BuiltinAgentName::from_str("kigi-orchestrator")
.expect("from_str must resolve kigi-orchestrator");
assert_eq!(variant, BuiltinAgentName::KigiOrchestrator);
let def = variant.definition();
assert_eq!(def.name, "grok-build-orchestrator");
assert_eq!(def.name, "kigi-orchestrator");
assert!(
def.prompt_body.is_some(),
"orchestrator must have prompt_body"
@@ -1067,9 +1066,9 @@ mod tests {
#[test]
fn test_orchestrator_by_name_in_cwd() {
let tmp = tempfile::tempdir().unwrap();
let def = by_name_in_cwd("grok-build-orchestrator", tmp.path())
.expect("by_name_in_cwd must find grok-build-orchestrator");
assert_eq!(def.name, "grok-build-orchestrator");
let def = by_name_in_cwd("kigi-orchestrator", tmp.path())
.expect("by_name_in_cwd must find kigi-orchestrator");
assert_eq!(def.name, "kigi-orchestrator");
assert!(def.prompt_body.is_some());
}
@@ -1461,7 +1460,7 @@ mod tests {
let registry = make_plugin_registry("plugin-one", PluginScope::User, vec![]);
let plugin = registry.get("plugin-one").unwrap();
let mut def = AgentDefinition::default_grok_build();
let mut def = AgentDefinition::default_kigi();
def.prompt_body = Some("Body ${CLAUDE_PLUGIN_ROOT}/x".to_string());
def.system_prompt =
TemplateOverride::Custom("Data at ${CLAUDE_PLUGIN_DATA}/db".to_string());
+1 -1
View File
@@ -23,7 +23,7 @@ pub use compaction::CompactionPolicy;
pub use config::AgentDefinition;
pub use config::preset_names;
pub use config::toolset_for_preset;
pub use config::workspace_grok_build_toolset;
pub use config::workspace_kigi_toolset;
pub use error::AgentBuildError;
pub use prompt::context::{DEFAULT_SYSTEM_PROMPT_LABEL, PromptContext};
pub use system_reminder::ReminderPolicy;
@@ -69,11 +69,11 @@ pub enum PluginOrigin {
/// CLI `--plugin-dir`.
CliOverride,
/// Project `.kigi/plugins/`.
ProjectGrok,
ProjectKigi,
/// Project `.claude/plugins/`.
ProjectClaude,
/// `$KIGI_SHARE_DIR/plugins/`.
UserGrok,
UserKigi,
/// `~/.claude/plugins/`.
UserClaude,
/// A compat marketplace clone (project `extraKnownMarketplaces`
@@ -87,7 +87,7 @@ pub enum PluginOrigin {
/// Marketplace name from the `name@marketplace` JSON key, when present.
marketplace: Option<String>,
},
/// Grok's install registry (`~/.kigi/installed-plugins`).
/// Kigi's install registry (`~/.kigi/installed-plugins`).
MarketplaceInstall {
/// Git URL of the installed repo (None for local installs).
git_url: Option<String>,
@@ -214,10 +214,10 @@ impl DiscoveryConfig {
/// paths all resolve under `kigi_home()`, so a plugin scanned from the legacy
/// tree would appear untrusted and lose its persisted state. Keeping plugins on
/// `kigi_home()` only avoids that half-initialized state.
fn user_plugin_dirs(home: Option<&Path>, grok: Option<&Path>) -> Vec<(PathBuf, PluginOrigin)> {
fn user_plugin_dirs(home: Option<&Path>, kigi: Option<&Path>) -> Vec<(PathBuf, PluginOrigin)> {
let mut dirs = Vec::new();
if let Some(g) = grok {
dirs.push((g.join("plugins"), PluginOrigin::UserGrok));
if let Some(g) = kigi {
dirs.push((g.join("plugins"), PluginOrigin::UserKigi));
}
if let Some(h) = home {
dirs.push((h.join(".claude").join("plugins"), PluginOrigin::UserClaude));
@@ -234,7 +234,7 @@ fn project_plugins_dir_origin(plugins_dir: &Path) -> PluginOrigin {
if is_claude {
PluginOrigin::ProjectClaude
} else {
PluginOrigin::ProjectGrok
PluginOrigin::ProjectKigi
}
}
@@ -336,10 +336,10 @@ pub fn discover_plugins(
}
// 4-5. User plugins: $KIGI_SHARE_DIR/plugins, legacy ~/.kigi/plugins, ~/.claude/plugins.
// Gate the grok plugins dir on user_kigi_home() so a project's .kigi/plugins
// Gate the kigi plugins dir on user_kigi_home() so a project's .kigi/plugins
// is never scanned as user-global when no home resolves.
let grok = kigi_config::user_kigi_home();
let plugin_dirs = user_plugin_dirs(dirs::home_dir().as_deref(), grok.as_deref());
let kigi = kigi_config::user_kigi_home();
let plugin_dirs = user_plugin_dirs(dirs::home_dir().as_deref(), kigi.as_deref());
for (plugins_dir, origin) in plugin_dirs {
if plugins_dir.is_dir() {
scan_plugin_dir(
@@ -903,11 +903,11 @@ mod tests {
}
#[test]
fn user_plugin_dirs_are_grok_and_claude_only_no_legacy() {
fn user_plugin_dirs_are_kigi_and_claude_only_no_legacy() {
let home = Path::new("/home/u");
let grok = Path::new("/custom/grokhome");
let dirs = user_plugin_dirs(Some(home), Some(grok));
assert!(dirs.contains(&(grok.join("plugins"), PluginOrigin::UserGrok)));
let kigi = Path::new("/custom/kigihome");
let dirs = user_plugin_dirs(Some(home), Some(kigi));
assert!(dirs.contains(&(kigi.join("plugins"), PluginOrigin::UserKigi)));
assert!(dirs.contains(&(
home.join(".claude").join("plugins"),
PluginOrigin::UserClaude
@@ -921,7 +921,7 @@ mod tests {
}
#[test]
fn user_plugin_dirs_empty_without_home_or_grok() {
fn user_plugin_dirs_empty_without_home_or_kigi() {
assert!(user_plugin_dirs(None, None).is_empty());
}
@@ -951,10 +951,10 @@ mod tests {
}
#[test]
fn project_plugins_dir_origin_distinguishes_grok_and_claude() {
fn project_plugins_dir_origin_distinguishes_kigi_and_claude() {
assert_eq!(
project_plugins_dir_origin(Path::new("/repo/.kigi/plugins")),
PluginOrigin::ProjectGrok
PluginOrigin::ProjectKigi
);
assert_eq!(
project_plugins_dir_origin(Path::new("/repo/.claude/plugins")),
@@ -967,18 +967,18 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
// Create ~/.kigi/plugins/ structure
let grok_plugins = tmp.path().join(".kigi").join("plugins");
std::fs::create_dir_all(&grok_plugins).unwrap();
make_manifest_plugin(&grok_plugins, "user-tool");
let kigi_plugins = tmp.path().join(".kigi").join("plugins");
std::fs::create_dir_all(&kigi_plugins).unwrap();
make_manifest_plugin(&kigi_plugins, "user-tool");
// Override home dir by directly scanning
let trust = TrustStore::load_from(tmp.path().join("trust"));
let mut seen = HashSet::new();
let mut candidates = Vec::new();
scan_plugin_dir(
&grok_plugins,
&kigi_plugins,
PluginScope::User,
PluginOrigin::UserGrok,
PluginOrigin::UserKigi,
&trust,
false,
&mut seen,
@@ -1002,7 +1002,7 @@ mod tests {
scan_plugin_dir(
&plugins_dir,
PluginScope::User,
PluginOrigin::UserGrok,
PluginOrigin::UserKigi,
&trust,
false,
&mut seen,
@@ -1325,7 +1325,7 @@ mod tests {
collect_plugin(
&user_plugin,
PluginScope::User,
PluginOrigin::UserGrok,
PluginOrigin::UserKigi,
&trust,
false,
&mut seen,
@@ -1381,7 +1381,7 @@ mod tests {
collect_plugin(
&empty_dir,
PluginScope::User,
PluginOrigin::UserGrok,
PluginOrigin::UserKigi,
&trust,
false,
&mut seen,
@@ -1404,7 +1404,7 @@ mod tests {
collect_plugin(
&plugin_dir,
PluginScope::Project,
PluginOrigin::ProjectGrok,
PluginOrigin::ProjectKigi,
&trust,
false,
&mut seen,
@@ -1427,7 +1427,7 @@ mod tests {
collect_plugin(
&plugin_dir,
PluginScope::Project,
PluginOrigin::ProjectGrok,
PluginOrigin::ProjectKigi,
&trust,
true,
&mut seen,
@@ -1458,7 +1458,7 @@ mod tests {
PluginScope::CliOverride,
PluginOrigin::CliOverride,
),
(&user_dir, PluginScope::User, PluginOrigin::UserGrok),
(&user_dir, PluginScope::User, PluginOrigin::UserKigi),
(
&config_dir,
PluginScope::ConfigPath,
@@ -1505,7 +1505,7 @@ mod tests {
.find(|p| p.manifest.name == "proj-mcp")
.expect("project plugin discovered");
assert_eq!(p.scope, PluginScope::Project);
assert_eq!(p.origin, PluginOrigin::ProjectGrok);
assert_eq!(p.origin, PluginOrigin::ProjectKigi);
assert!(!p.trusted, "untrusted folder must block the project plugin");
// Trusted folder: the same plugin is allowed.
@@ -654,8 +654,8 @@ mod tests {
scope,
origin: match scope {
PluginScope::CliOverride => PluginOrigin::CliOverride,
PluginScope::Project => PluginOrigin::ProjectGrok,
PluginScope::User => PluginOrigin::UserGrok,
PluginScope::Project => PluginOrigin::ProjectKigi,
PluginScope::User => PluginOrigin::UserKigi,
PluginScope::ConfigPath => PluginOrigin::ConfigPath,
},
trusted,
@@ -40,13 +40,13 @@ impl TrustStore {
pub fn load() -> Self {
// Gate on user_kigi_home() so a project's `.kigi/trusted-plugins` is never
// read as the user trust store when neither KIGI_SHARE_DIR nor a home dir resolves.
let Some(grok) = kigi_config::user_kigi_home() else {
let Some(kigi) = kigi_config::user_kigi_home() else {
return Self {
trusted: HashSet::new(),
file_path: PathBuf::new(),
};
};
let file_path = grok.join(TRUST_FILE_NAME);
let file_path = kigi.join(TRUST_FILE_NAME);
let trusted = Self::read_trust_file(&file_path);
Self { trusted, file_path }
}
@@ -144,13 +144,13 @@ pub struct PromptContext {
/// stdio / generic-ACP).
#[serde(default)]
pub is_non_interactive: bool,
/// Identity in the primary grok-build system prompt (`You are <label>…`).
/// Identity in the primary kigi system prompt (`You are <label>…`).
/// Not the UI picker name. Defaults to [`DEFAULT_SYSTEM_PROMPT_LABEL`].
#[serde(default = "default_system_prompt_label")]
pub system_prompt_label: String,
}
/// Default identity on trim-tool-descriptions (`You are Grok released by xAI`).
pub const DEFAULT_SYSTEM_PROMPT_LABEL: &str = "Grok";
/// Default identity on trim-tool-descriptions (`You are Kigi released by xAI`).
pub const DEFAULT_SYSTEM_PROMPT_LABEL: &str = "Kigi";
fn default_system_prompt_label() -> String {
DEFAULT_SYSTEM_PROMPT_LABEL.to_string()
}
@@ -427,9 +427,9 @@ mod tests {
#[test]
fn test_placeholders_system_prompt_label_override() {
let mut ctx = test_context();
ctx.system_prompt_label = "Grok Internal".into();
ctx.system_prompt_label = "Kigi Internal".into();
let p = ctx.placeholders();
assert_eq!(p["system_prompt_label"], "Grok Internal");
assert_eq!(p["system_prompt_label"], "Kigi Internal");
}
#[test]
fn test_missing_system_prompt_label_deserializes_to_default() {
@@ -1229,7 +1229,7 @@ mod tests {
("plan", plan),
] {
assert!(
!prompt.contains("You are a Grok Build agent"),
!prompt.contains("You are a Kigi agent"),
"{name} prompt should not duplicate base template identity"
);
}
File diff suppressed because one or more lines are too long
+27 -27
View File
@@ -832,12 +832,12 @@ mod tests {
fn find_skill_paths_flat_layout() {
// Traditional flat layout: skills/<name>/SKILL.md
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
let kigi_dir = tmp.path().join(".kigi");
write_skill_md(&grok_dir.join("skills").join("alpha"), "alpha");
write_skill_md(&grok_dir.join("skills").join("beta"), "beta");
write_skill_md(&kigi_dir.join("skills").join("alpha"), "alpha");
write_skill_md(&kigi_dir.join("skills").join("beta"), "beta");
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert_eq!(paths.len(), 2);
assert!(paths.iter().all(|p| p.file_name().unwrap() == "SKILL.md"));
}
@@ -846,13 +846,13 @@ mod tests {
fn find_skill_paths_nested_layout() {
// Nested: skills/team/infra/SKILL.md, skills/team/training/SKILL.md
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
let skills = grok_dir.join("skills");
let kigi_dir = tmp.path().join(".kigi");
let skills = kigi_dir.join("skills");
write_skill_md(&skills.join("team").join("infra"), "infra");
write_skill_md(&skills.join("team").join("training"), "training");
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert_eq!(paths.len(), 2);
let path_strs: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
@@ -864,8 +864,8 @@ mod tests {
fn find_skill_paths_mixed_flat_and_nested() {
// Mix of flat and nested skills
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
let skills = grok_dir.join("skills");
let kigi_dir = tmp.path().join(".kigi");
let skills = kigi_dir.join("skills");
// Flat
write_skill_md(&skills.join("top-level"), "top-level");
@@ -874,7 +874,7 @@ mod tests {
// Nested 2 levels
write_skill_md(&skills.join("org").join("team").join("deep"), "deep");
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert_eq!(paths.len(), 3);
}
@@ -882,8 +882,8 @@ mod tests {
fn find_skill_paths_dir_without_skill_md_is_skipped() {
// A subdirectory exists but has no SKILL.md — should not appear
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
let skills = grok_dir.join("skills");
let kigi_dir = tmp.path().join(".kigi");
let skills = kigi_dir.join("skills");
write_skill_md(&skills.join("valid"), "valid");
// Create a dir with no SKILL.md
@@ -893,7 +893,7 @@ mod tests {
fs::create_dir_all(&other).unwrap();
fs::write(other.join("README.md"), "not a skill").unwrap();
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert_eq!(paths.len(), 1);
assert!(paths[0].display().to_string().contains("valid"));
}
@@ -902,10 +902,10 @@ mod tests {
fn find_skill_paths_no_skills_dir() {
// .kigi exists but no skills/ subdirectory
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
fs::create_dir_all(&grok_dir).unwrap();
let kigi_dir = tmp.path().join(".kigi");
fs::create_dir_all(&kigi_dir).unwrap();
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert!(paths.is_empty());
}
@@ -944,15 +944,15 @@ mod tests {
fn find_skill_paths_parent_and_child_both_have_skill_md() {
// A directory has SKILL.md and also has subdirectories with SKILL.md
let tmp = tempfile::tempdir().unwrap();
let grok_dir = tmp.path().join(".kigi");
let skills = grok_dir.join("skills");
let kigi_dir = tmp.path().join(".kigi");
let skills = kigi_dir.join("skills");
// Parent skill
write_skill_md(&skills.join("parent"), "parent-skill");
// Child skill inside parent
write_skill_md(&skills.join("parent").join("child"), "child-skill");
let paths = find_skill_paths(&grok_dir);
let paths = find_skill_paths(&kigi_dir);
assert_eq!(paths.len(), 2);
let path_strs: Vec<String> = paths.iter().map(|p| p.display().to_string()).collect();
@@ -1108,9 +1108,9 @@ mod tests {
#[test]
fn parse_model_and_effort() {
let content = "---\nname: my-skill\ndescription: test\nmodel: grok-3\neffort: high\n---\n";
let content = "---\nname: my-skill\ndescription: test\nmodel: kigi-3\neffort: high\n---\n";
let parsed = parse_skill_frontmatter(content, None).unwrap();
assert_eq!(parsed.model.as_deref(), Some("grok-3"));
assert_eq!(parsed.model.as_deref(), Some("kigi-3"));
assert_eq!(parsed.effort.as_deref(), Some("high"));
}
@@ -1251,7 +1251,7 @@ mod tests {
#[test]
fn parse_full_spec_plus_extensions() {
// Mixed agentskills.io spec fields + our extensions — all must parse.
let content = "---\nname: my-skill\ndescription: A full skill\nlicense: MIT\ncompatibility: Python 3.12+\nmetadata:\n author: test-org\n version: \"2.0\"\nallowed-tools:\n - bash\n - read_file\nargument-hint: file path\nmodel: grok-3\neffort: high\nuser-invocable: true\ndisable-model-invocation: false\n---\nBody content.\n";
let content = "---\nname: my-skill\ndescription: A full skill\nlicense: MIT\ncompatibility: Python 3.12+\nmetadata:\n author: test-org\n version: \"2.0\"\nallowed-tools:\n - bash\n - read_file\nargument-hint: file path\nmodel: kigi-3\neffort: high\nuser-invocable: true\ndisable-model-invocation: false\n---\nBody content.\n";
let parsed = parse_skill_frontmatter(content, None).unwrap();
assert_eq!(parsed.name, "my-skill");
assert_eq!(parsed.description, "A full skill");
@@ -1263,7 +1263,7 @@ mod tests {
Some(["bash".to_string(), "read_file".to_string()].as_slice())
);
assert_eq!(parsed.argument_hint.as_deref(), Some("file path"));
assert_eq!(parsed.model.as_deref(), Some("grok-3"));
assert_eq!(parsed.model.as_deref(), Some("kigi-3"));
assert_eq!(parsed.effort.as_deref(), Some("high"));
assert!(parsed.user_invocable);
assert!(!parsed.disable_model_invocation);
@@ -1575,7 +1575,7 @@ mod tests {
root: root.clone(),
canonical_root: root.clone(),
scope: PluginScope::User,
origin: crate::plugins::PluginOrigin::UserGrok,
origin: crate::plugins::PluginOrigin::UserKigi,
trusted: true,
enabled: true,
version: Some("1.0.0".to_string()),
@@ -1653,7 +1653,7 @@ mod tests {
root: root.to_path_buf(),
canonical_root: root.to_path_buf(),
scope: PluginScope::User,
origin: crate::plugins::PluginOrigin::UserGrok,
origin: crate::plugins::PluginOrigin::UserKigi,
trusted: true,
skill_dirs,
command_dirs: vec![],
@@ -2323,7 +2323,7 @@ mod tests {
root: root.clone(),
canonical_root: root,
scope: PluginScope::Project,
origin: crate::plugins::PluginOrigin::ProjectGrok,
origin: crate::plugins::PluginOrigin::ProjectKigi,
trusted: true,
enabled: true,
version: Some("1.0.0".to_string()),
@@ -2458,7 +2458,7 @@ mod tests {
"cursor must be gated off: {dirs:?}"
);
assert!(ends_with(&dirs, ".claude"), "claude must remain: {dirs:?}");
assert!(ends_with(&dirs, ".kigi"), "grok must remain: {dirs:?}");
assert!(ends_with(&dirs, ".kigi"), "kigi must remain: {dirs:?}");
}
// ── Same-scope frontmatter-name collisions (copied skill dirs) ──────
@@ -32,7 +32,7 @@ pub(crate) fn base_template() -> Zeroizing<String> {
decrypt(BASE_PROMPT_ENC, PROMPT_SEEDS[0])
}
/// The base prompt template source, exposed for `grok prompt --section template`.
/// The base prompt template source, exposed for `kigi prompt --section template`.
pub fn base_template_source() -> Zeroizing<String> {
base_template()
}
@@ -41,7 +41,7 @@ pub(crate) fn apply_patch_template() -> Zeroizing<String> {
decrypt(CODEX_PROMPT_ENC, PROMPT_SEEDS[1])
}
/// Apply-patch prompt template source, exposed for `grok prompt --section apply-patch-template`.
/// Apply-patch prompt template source, exposed for `kigi prompt --section apply-patch-template`.
pub fn apply_patch_template_source() -> Zeroizing<String> {
apply_patch_template()
}
@@ -93,7 +93,7 @@ mod tests {
);
}
/// Build a TemplateRenderer with the standard grok-build tool kinds.
/// Build a TemplateRenderer with the standard kigi tool kinds.
fn default_renderer() -> TemplateRenderer {
let tools: HashMap<ToolKind, String> = [
(ToolKind::Read, "read_file"),
@@ -5,7 +5,7 @@
//! workspace overview, optional rules / skills / MCP listings).
//!
//! `UserMessageTemplate` selects the rendering strategy:
//! - `Default` -- the legacy Grok Build prefix (built by the shell layer).
//! - `Default` -- the legacy Kigi prefix (built by the shell layer).
//! - `Custom` -- caller-supplied template string (MiniJinja, same delimiters
//! as the system prompt templates).
//!
@@ -63,7 +63,7 @@ fn normalize_git_status(status: &str) -> Option<String> {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum UserMessageTemplate {
/// Legacy Grok Build prefix: `<user_info>` + optional `<git_status>`.
/// Legacy Kigi prefix: `<user_info>` + optional `<git_status>`.
/// Built directly by the shell layer; this
/// renderer returns `None` for `Default` and the caller falls back to
/// its own legacy path.
+1 -1
View File
@@ -1,4 +1,4 @@
const TARGET: &str = "xai_grok_instrumentation";
const TARGET: &str = "xai_kigi_instrumentation";
pub struct TimingGuard {
name: &'static str,
@@ -1,4 +1,4 @@
You are ${{ system_prompt_label }} released by xAI. You are ${%- if is_non_interactive %} an autonomous agent that completes software engineering tasks.${%- else %} an interactive CLI tool that helps users with software engineering tasks.${%- endif %} Your main goal is to complete the user's request, denoted within the <user_query> tag.
You are ${{ system_prompt_label }}, an unofficial community CLI for Kimi. You are ${%- if is_non_interactive %} an autonomous agent that completes software engineering tasks.${%- else %} an interactive CLI tool that helps users with software engineering tasks.${%- endif %} Your main goal is to complete the user's request, denoted within the <user_query> tag.
<action_safety>
Weigh each action by how easily it can be undone and how far its effects reach. Local, reversible work such as editing files and running tests is fine to do freely. Before executing any actions that are hard to reverse, reach shared external systems, or are otherwise risky or destructive, check with the user first.
@@ -35,7 +35,7 @@ pub trait AuthCredentialProvider: HttpAuth + Send + Sync + 'static {
/// Return the current credential snapshot. Implementations should
/// issue a cheap disk re-read (`AuthManager::refresh`) before
/// snapshotting so callers see updates from sibling processes
/// (`grok-desktop`, `kigi login`). The `token` field MUST mirror
/// (`kigi-desktop`, `kigi login`). The `token` field MUST mirror
/// the bearer that `HttpAuth::apply` would send on the wire so
/// 401-attribution prefixes match the actual request.
fn snapshot(&self) -> CredentialSnapshot;
+43 -43
View File
@@ -48,7 +48,7 @@ fn apply_agent_endpoint_args(agent_args: &kigi_tui::app::AgentArgs, config: &mut
config.endpoints.coding_api_base_url = Some(v.clone());
}
if let Some(v) = &agent_args.api_base_url {
config.endpoints.api_base_url = v.clone();
config.endpoints.api_base_url = Some(v.clone());
}
}
/// Resolve --agent-profile path: canonicalize and verify the file exists.
@@ -71,7 +71,7 @@ fn resolve_agent_profile_path(path: &std::path::Path) -> std::path::PathBuf {
/// Print startup information for the serve command.
fn print_serve_startup_info(bind_addr: SocketAddr, secret: &str) {
eprintln!();
eprintln!(" Grok agent server starting...");
eprintln!(" Kigi agent server starting...");
eprintln!();
eprintln!(" Address: {}:{}", bind_addr.ip(), bind_addr.port());
eprintln!(" Secret: {}", secret);
@@ -82,7 +82,7 @@ fn print_serve_startup_info(bind_addr: SocketAddr, secret: &str) {
);
eprintln!();
}
/// Entrypoint tag for `grok -p`; keys the quiet stderr default in `init_tracing_simple`.
/// Entrypoint tag for `kigi -p`; keys the quiet stderr default in `init_tracing_simple`.
const HEADLESS_ENTRYPOINT: &str = "headless";
/// Initialize simple tracing for non-TUI agent modes.
fn init_tracing_simple(app_entrypoint: &'static str) {
@@ -112,7 +112,7 @@ fn init_tracing_simple(app_entrypoint: &'static str) {
.with(kigi_log::hooks_log::layer());
kigi_log::debug_log::install_firehose(registry, app_entrypoint);
}
/// `grok setup`: rendering + exit codes only; fetch logic lives in `kigi_shell::managed_config`.
/// `kigi setup`: rendering + exit codes only; fetch logic lives in `kigi_shell::managed_config`.
/// `json` prints the served configuration instead of installing it.
async fn run_setup_command(json: bool) {
use kigi_shell::managed_config::{self, SetupOutcome};
@@ -127,7 +127,7 @@ async fn run_setup_command(json: bool) {
} else {
eprintln!(" $env:KIGI_DEPLOYMENT_KEY=\"<your-key>\"");
}
eprintln!(" grok setup");
eprintln!(" kigi setup");
eprintln!();
eprintln!("Or add the key to ~/.kigi/config.toml:");
eprintln!();
@@ -135,7 +135,7 @@ async fn run_setup_command(json: bool) {
eprintln!(" deployment_key = \"<your-key>\"");
eprintln!();
eprintln!(
"If you don't have a deployment key, contact your organization's Grok administrator."
"If you don't have a deployment key, contact your organization's Kigi administrator."
);
std::process::exit(1);
}
@@ -147,7 +147,7 @@ async fn run_setup_command(json: bool) {
println!("{out}");
if !report.configured {
eprintln!(
"Your team doesn't have a managed configuration yet. A team admin can set one up at console.x.ai."
"Your team doesn't have a managed configuration yet. Ask a team admin to provision one."
);
}
}
@@ -162,7 +162,7 @@ async fn run_setup_command(json: bool) {
SetupOutcome::Installed => eprintln!("Applied managed configuration."),
SetupOutcome::NothingConfigured => {
eprintln!(
"Your team doesn't have a managed configuration yet. A team admin can set one up at console.x.ai."
"Your team doesn't have a managed configuration yet. Ask a team admin to provision one."
);
}
SetupOutcome::Failed(e) => {
@@ -229,9 +229,9 @@ async fn kill_leaders() -> Result<()> {
let Some(pid) = leader_pid(d) else {
continue;
};
if !kigi_shell::util::is_grok_process(pid) {
if !kigi_shell::util::is_kigi_process(pid) {
if let Some(ref lock) = d.lock_path {
eprintln!(" PID {pid} is not a grok process, removing stale lock");
eprintln!(" PID {pid} is not a kigi process, removing stale lock");
let _ = std::fs::remove_file(lock);
cleaned += 1;
}
@@ -274,7 +274,7 @@ async fn connect_to_leader(
.ok_or_else(|| anyhow::anyhow!("resolved leader target did not include a socket path"))?;
let client = kigi_shell::leader::LeaderClient::connect(
socket_path.to_path_buf(),
"grok-pager-leader-cli",
"kigi-pager-leader-cli",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -354,7 +354,7 @@ struct StdioReplayState {
/// old leader and is its to retry).
pending_new: Option<CachedSession>,
/// Most recently created/loaded session id — reported in
/// `x.ai/leader_reconnected` as the primary restored session.
/// `kigi/leader_reconnected` as the primary restored session.
last_session_id: Option<String>,
}
impl StdioReplayState {
@@ -422,7 +422,7 @@ fn cache_outgoing_acp_state(msg: &str, state: &std::sync::Mutex<StdioReplayState
.and_then(|m| serde_json::to_string(m).ok()),
});
}
"x.ai/session/close" | "_x.ai/session/close" => {
"kigi/session/close" | "_kigi/session/close" => {
if let Some(sid) = json
.get("params")
.and_then(|p| p.get("sessionId").or_else(|| p.get("session_id")))
@@ -454,7 +454,7 @@ fn cache_incoming_session_id(msg: &str, state: &std::sync::Mutex<StdioReplayStat
/// Synthetic JSON-RPC id for the `session/load` the bridge constructs itself
/// (when the external client only ever sent `session/new`). A string id can
/// never collide with a numeric id the external client may have in flight.
const REPLAY_LOAD_REQUEST_ID: &str = "x.ai/leader-replay/session-load";
const REPLAY_LOAD_REQUEST_ID: &str = "kigi/leader-replay/session-load";
/// Max silence between two messages from the leader during a replayed request.
/// A `session/load` streams replay notifications continuously once it starts,
/// but the pre-replay phase (MCP resolution, session file reads) can be quiet
@@ -587,7 +587,7 @@ fn replay_load_json(sid: &str, cached: &CachedSession) -> Option<String> {
/// Returns the primary restored session id (the most recently active one,
/// falling back to any successfully restored session). `None` when there was
/// nothing to replay or every restore failed — callers emit
/// `x.ai/leader_reconnected` with empty params in that case, signalling the
/// `kigi/leader_reconnected` with empty params in that case, signalling the
/// external client to re-establish state itself.
async fn replay_acp_state_after_reconnect(
tx: &tokio::sync::mpsc::UnboundedSender<String>,
@@ -657,7 +657,7 @@ fn shutdown_and_flush_telemetry(exit_code: i32) -> ! {
}
/// Emitted by both leader guards (server mode and leader-connect) so the two sites
/// can't drift.
const PLUGIN_DIR_LEADER_WARNING: &str = "grok: --plugin-dir is ignored in leader mode; run with --no-leader to \
const PLUGIN_DIR_LEADER_WARNING: &str = "kigi: --plugin-dir is ignored in leader mode; run with --no-leader to \
load per-process plugins";
/// Run the `agent` subcommand, dispatching to the appropriate mode.
async fn run_agent_command(
@@ -743,7 +743,7 @@ async fn run_agent_command(
None,
);
if let Some(warning) = launch_yolo.blocked_warning {
eprintln!("grok: {warning}");
eprintln!("kigi: {warning}");
}
agent_config.default_yolo_mode = launch_yolo.yolo;
agent_config.default_auto_mode = kigi_shell::util::config::effective_auto_for_launch(
@@ -916,7 +916,7 @@ async fn run_agent_command(
None => "{}".to_string(),
};
let notification = format!(
r#"{{"jsonrpc":"2.0","method":"x.ai/leader_reconnected","params":{params}}}"#
r#"{{"jsonrpc":"2.0","method":"kigi/leader_reconnected","params":{params}}}"#
);
let _ = stdout.write_all(notification.as_bytes()).await;
let _ = stdout.write_all(b"\n").await;
@@ -1051,12 +1051,12 @@ fn raise_fd_limit() {
fn raise_fd_limit() {}
/// Single audit point for the `Command::Dashboard` soft-subcommand.
/// Sets `KIGI_OPEN_DASHBOARD_AT_STARTUP=1` if the user asked for
/// `grok dashboard`, and clears `args.command` so the regular
/// `kigi dashboard`, and clears `args.command` so the regular
/// subcommand match doesn't try to handle it.
///
/// The dashboard is independent of leader mode — it renders local
/// sessions and, when a leader happens to be present, additionally shows
/// the leader roster — so `grok dashboard` does NOT force leader mode and
/// the leader roster — so `kigi dashboard` does NOT force leader mode and
/// is compatible with `--no-leader`.
///
/// The only gate is the feature flag: a disabled dashboard
@@ -1185,10 +1185,10 @@ fn main() {
kigi_tui::memory_trace::start(kigi_shell::util::kigi_home::kigi_home().join("memtrace"));
raise_fd_limit();
if let Err(e) = kigi_config::validate_requirements() {
eprintln!("Couldn't start Grok: {e}");
eprintln!("Couldn't start Kigi: {e}");
eprintln!();
eprintln!(
"Update Grok to a version the policy allows, or ask your administrator \
"Update Kigi to a version the policy allows, or ask your administrator \
to fix the managed requirements."
);
std::process::exit(2);
@@ -1198,7 +1198,7 @@ fn main() {
if kigi_shell::util::config::load_crash_handler_enabled_sync() {
let crash_dir = kigi_shell::util::kigi_home::kigi_home().join("crash");
if let Some(report) = kigi_crash_handler::check_previous_crash(&crash_dir) {
eprintln!("Grok crashed during your last session.");
eprintln!("Kigi crashed during your last session.");
eprintln!(" Signal: {}", report.signal_name);
eprintln!(" Version: {}", report.app_version);
eprintln!(" Report: {}", report.report_path.display());
@@ -1244,7 +1244,7 @@ async fn async_main() -> Result<()> {
}
if args.chat() {
anyhow::bail!(
"--chat is no longer supported: the grok.com chat frontend it drove was \
"--chat is no longer supported: the kimi.com chat frontend it drove was \
removed along with the xAI backend."
);
}
@@ -1307,7 +1307,7 @@ async fn async_main() -> Result<()> {
&& args.prompt_json.is_none()
&& args.prompt_file.is_none();
kigi_shell::http::set_client_name(if is_interactive {
kigi_workspace::permission::ClientType::GrokPager
kigi_workspace::permission::ClientType::KigiPager
} else {
kigi_workspace::permission::ClientType::Generic
});
@@ -1323,7 +1323,7 @@ async fn async_main() -> Result<()> {
println!("{}", serde_json::to_string(&payload)?);
} else {
println!(
"grok {}",
"kigi {}",
kigi_version::display_version_with_commit(
env!("VERSION_WITH_COMMIT"),
kigi_update::channel_label(),
@@ -1359,7 +1359,7 @@ async fn async_main() -> Result<()> {
};
anyhow::bail!(
"top-level {flag} applies to the pager TUI, not the agent subcommand. \
Use `grok-pager agent {flag}` instead."
Use `kigi-pager agent {flag}` instead."
);
}
enforce_minimum_version_or_exit(&update_config).await;
@@ -1501,7 +1501,7 @@ async fn async_main() -> Result<()> {
None,
);
if let Some(warning) = launch_yolo.blocked_warning {
eprintln!("grok: {warning}");
eprintln!("kigi: {warning}");
}
let json_schema = args
.json_schema
@@ -1583,9 +1583,9 @@ async fn async_main() -> Result<()> {
Ok(true) => {
let adopted = bg_update_wait.lock().await.take();
if finish_update_on_exit(adopted, &update_config).await {
eprintln!("Update installed. Run `grok` to start.");
eprintln!("Update installed. Run `kigi` to start.");
} else {
eprintln!("Update did not complete. Run `grok update` to retry.");
eprintln!("Update did not complete. Run `kigi update` to retry.");
}
Ok(())
}
@@ -1596,11 +1596,11 @@ async fn async_main() -> Result<()> {
/// Complete the update after a quit-for-update (Ctrl+U) exit. Returns `true`
/// when an update path completed without a reported failure.
///
/// Prefers awaiting the parked waiter for the background `grok update` child
/// Prefers awaiting the parked waiter for the background `kigi update` child
/// spawned at startup — the download is usually already done or in flight.
/// Only when there is no waiter (spawn failed, or no download was needed
/// because the target was already on disk) or the child failed does this
/// fall back to a fresh blocking `grok update`, which itself resolves to
/// fall back to a fresh blocking `kigi update`, which itself resolves to
/// "Already up to date" without downloading when the disk is current.
async fn finish_update_on_exit(
adopted: Option<tokio::task::JoinHandle<std::io::Result<std::process::ExitStatus>>>,
@@ -1700,7 +1700,7 @@ fn get_channel_switch(alpha: bool, stable: bool, enterprise: bool) -> Option<&'s
None
}
}
/// Handle `grok-pager update [--check] [--json] [--force-reinstall] [--version X] [--alpha|--stable|--enterprise]`.
/// Handle `kigi-pager update [--check] [--json] [--force-reinstall] [--version X] [--alpha|--stable|--enterprise]`.
async fn run_update_command(
check: bool,
json: bool,
@@ -1742,7 +1742,7 @@ async fn run_update_command(
}
Ok(())
}
/// After a successful `grok update`, ask any running leader on this machine that
/// After a successful `kigi update`, ask any running leader on this machine that
/// is older than `installed_version` to relaunch onto the new binary (bounded
/// grace; running sessions close and reconnect via `session/load`).
///
@@ -1764,7 +1764,7 @@ async fn signal_leaders_to_relaunch(installed_version: &str) {
}
let client = match kigi_shell::leader::LeaderClient::connect(
socket_path,
"grok-pager-update",
"kigi-pager-update",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -1839,13 +1839,13 @@ mod tests {
);
}
use clap::Parser as _;
/// `grok dashboard` flags the startup hook without forcing leader mode —
/// `kigi dashboard` flags the startup hook without forcing leader mode —
/// the dashboard is independent of leader mode, so the launch keeps
/// whatever leader setting the user (or config) chose.
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
#[test]
fn dashboard_subcommand_flags_startup_without_forcing_leader() {
let mut args = PagerArgs::try_parse_from(["grok", "dashboard"]).unwrap();
let mut args = PagerArgs::try_parse_from(["kigi", "dashboard"]).unwrap();
assert!(!args.leader, "fixture: no explicit --leader");
flag_dashboard_at_startup_if_requested(&mut args).unwrap();
assert!(!args.leader, "dashboard must NOT force leader mode");
@@ -1860,13 +1860,13 @@ mod tests {
);
unsafe { std::env::remove_var("KIGI_OPEN_DASHBOARD_AT_STARTUP") };
}
/// `grok dashboard --no-leader` is allowed — the dashboard does not
/// `kigi dashboard --no-leader` is allowed — the dashboard does not
/// require a leader, so the combination launches into the dashboard in
/// non-leader mode.
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
#[test]
fn dashboard_subcommand_allows_no_leader() {
let mut args = PagerArgs::try_parse_from(["grok", "--no-leader", "dashboard"]).unwrap();
let mut args = PagerArgs::try_parse_from(["kigi", "--no-leader", "dashboard"]).unwrap();
flag_dashboard_at_startup_if_requested(&mut args)
.expect("--no-leader + dashboard must be allowed");
assert!(args.no_leader, "--no-leader must be preserved");
@@ -1888,7 +1888,7 @@ mod tests {
#[test]
fn dashboard_subcommand_errors_when_disabled() {
unsafe { std::env::set_var("KIGI_AGENT_DASHBOARD", "0") };
let mut args = PagerArgs::try_parse_from(["grok", "dashboard"]).unwrap();
let mut args = PagerArgs::try_parse_from(["kigi", "dashboard"]).unwrap();
let result = flag_dashboard_at_startup_if_requested(&mut args);
unsafe { std::env::remove_var("KIGI_AGENT_DASHBOARD") };
let err = result.expect_err("disabled dashboard must error");
@@ -1958,7 +1958,7 @@ mod tests {
&state,
);
cache_outgoing_acp_state(
r#"{"jsonrpc":"2.0","id":3,"method":"_x.ai/session/close","params":{"sessionId":"s1"}}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"_kigi/session/close","params":{"sessionId":"s1"}}"#,
&state,
);
let s = state.lock().unwrap();
@@ -2216,7 +2216,7 @@ mod tests {
let _init = leader_rx.recv().await.unwrap();
response_tx
.send(
r#"{"jsonrpc":"2.0","method":"x.ai/leader/version_mismatch","params":{}}"#
r#"{"jsonrpc":"2.0","method":"kigi/leader/version_mismatch","params":{}}"#
.to_string(),
)
.unwrap();
@@ -2262,7 +2262,7 @@ mod tests {
}
/// A `session/load` rejected by the new leader (error response) must
/// surface as a failed replay (`None`) so the bridge emits
/// `x.ai/leader_reconnected` with empty params and the external client
/// `kigi/leader_reconnected` with empty params and the external client
/// knows to re-establish state itself.
#[tokio::test]
async fn replay_returns_none_when_load_is_rejected() {
@@ -77,7 +77,7 @@ impl ChatStateActor {
});
}
/// Out-of-band history repair (`x.ai/session/repair`): run
/// Out-of-band history repair (`kigi/session/repair`): run
/// [`crate::compaction_utils::repair_history`] and persist changes via
/// [`Self::replace_conversation`]. Unlike
/// [`Self::ensure_conversation_integrity`], this also removes orphaned
@@ -134,13 +134,13 @@ impl ChatStateActor {
temperature: self.state.sampling_config.temperature,
max_output_tokens: self.state.sampling_config.max_completion_tokens,
top_p: self.state.sampling_config.top_p,
x_grok_conv_id: Some(conv_id),
x_grok_req_id: Some(req_id),
x_grok_session_id: None,
x_grok_turn_idx: None,
x_grok_agent_id: None,
x_grok_deployment_id: None,
x_grok_user_id: None,
x_kigi_conv_id: Some(conv_id),
x_kigi_req_id: Some(req_id),
x_kigi_session_id: None,
x_kigi_turn_idx: None,
x_kigi_agent_id: None,
x_kigi_deployment_id: None,
x_kigi_user_id: None,
trace,
reasoning_effort: self.state.sampling_config.reasoning_effort,
json_schema: None,
@@ -84,13 +84,13 @@ pub fn estimate_conversation_tokens(items: &[ConversationItem]) -> u64 {
items.iter().map(estimate_item_tokens).sum()
}
/// grok-build's [`ItemTokenCounter`](kigi_compaction::ItemTokenCounter)
/// for the shared compaction engine: the bytes/4 estimate grok-build already
/// kigi's [`ItemTokenCounter`](kigi_compaction::ItemTokenCounter)
/// for the shared compaction engine: the bytes/4 estimate kigi already
/// uses to drive its compaction triggers, exposed through the seam so the
/// shared budgeting math gets the *same* trusted count.
///
/// Where another host plugs a real BPE tokenizer into the same seam,
/// grok-build estimates instead, reusing [`estimate_item_tokens`] so the
/// kigi estimates instead, reusing [`estimate_item_tokens`] so the
/// per-variant arithmetic (images, reasoning blobs, tool-call args) stays in
/// one place.
pub struct EstimatedItemTokenCounter;
@@ -908,7 +908,7 @@ async fn update_sampling_config_is_queryable() {
let h = TestHarness::new();
let new_config = SamplingConfig {
base_url: "https://new.example.com".to_string(),
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
max_completion_tokens: Some(4096),
temperature: Some(0.5),
top_p: None,
@@ -921,7 +921,7 @@ async fn update_sampling_config_is_queryable() {
h.handle.update_sampling_config(new_config.clone());
let config = h.handle.get_sampling_config().await.unwrap();
assert_eq!(config.model, "grok-3");
assert_eq!(config.model, "kigi-3");
assert_eq!(config.context_window, NonZeroU64::new(200_000).unwrap());
}
@@ -1155,8 +1155,8 @@ async fn build_request_includes_all_messages() {
.await
.unwrap();
assert_eq!(request.items.len(), 2);
assert_eq!(request.x_grok_conv_id, Some("conv-1".to_string()));
assert_eq!(request.x_grok_req_id, Some("req-1".to_string()));
assert_eq!(request.x_kigi_conv_id, Some("conv-1".to_string()));
assert_eq!(request.x_kigi_req_id, Some("req-1".to_string()));
}
#[tokio::test]
@@ -1293,7 +1293,7 @@ async fn build_request_with_tool_definitions() {
async fn build_request_uses_sampling_config() {
let config = SamplingConfig {
base_url: "https://api.example.com".to_string(),
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
max_completion_tokens: Some(8192),
temperature: Some(0.7),
top_p: Some(0.9),
@@ -1311,7 +1311,7 @@ async fn build_request_uses_sampling_config() {
.await
.unwrap();
assert_eq!(request.model, Some("grok-3".to_string()));
assert_eq!(request.model, Some("kigi-3".to_string()));
assert_eq!(request.temperature, Some(0.7));
assert_eq!(request.max_output_tokens, Some(8192));
assert_eq!(request.top_p, Some(0.9));
@@ -1484,7 +1484,7 @@ async fn parallel_tool_calls_accept_first_reject_second_skip_third() {
arguments: r#"{"command":"cargo test"}"#.into(),
},
],
model_id: Some("grok-3".to_string()),
model_id: Some("kigi-3".to_string()),
model_fingerprint: None,
reasoning_effort: None,
});
@@ -1770,7 +1770,7 @@ async fn dangling_tool_calls_after_crash_are_repaired_on_load() {
arguments: r#"{"command":"cargo test"}"#.into(),
},
],
model_id: Some("grok-3".to_string()),
model_id: Some("kigi-3".to_string()),
model_fingerprint: None,
reasoning_effort: None,
}),
@@ -3365,13 +3365,13 @@ async fn get_last_model_metadata_returns_both_fields() {
ConversationItem::Assistant(kigi_sampling_types::AssistantItem {
content: "hello".into(),
tool_calls: vec![],
model_id: Some("grok-4.5".into()),
model_id: Some("kigi-4.5".into()),
model_fingerprint: Some("fp_abc123".into()),
reasoning_effort: None,
}),
]);
let meta = h.handle.get_last_model_metadata().await;
assert_eq!(meta.resolved_model_id.as_deref(), Some("grok-4.5"));
assert_eq!(meta.resolved_model_id.as_deref(), Some("kigi-4.5"));
assert_eq!(meta.model_fingerprint.as_deref(), Some("fp_abc123"));
}
@@ -3396,7 +3396,7 @@ async fn sampling_config_survives_compaction_replacement() {
let config = SamplingConfig {
base_url: "https://api.example.com".to_string(),
model: "grok-build".to_string(),
model: "kigi".to_string(),
max_completion_tokens: None,
temperature: Some(0.7),
top_p: Some(0.95),
@@ -3414,7 +3414,7 @@ async fn sampling_config_survives_compaction_replacement() {
ConversationItem::Assistant(kigi_sampling_types::AssistantItem {
content: "I'll fix it.".into(),
tool_calls: vec![],
model_id: Some("grok-4.5".into()),
model_id: Some("kigi-4.5".into()),
model_fingerprint: Some("fp_abc123".into()),
reasoning_effort: None,
}),
@@ -3424,12 +3424,12 @@ async fn sampling_config_survives_compaction_replacement() {
// Pre-compaction: everything correct.
let pre = h.handle.get_sampling_config().await.unwrap();
assert_eq!(pre.model, "grok-build");
assert_eq!(pre.model, "kigi");
assert_eq!(pre.context_window.get(), 500_000);
assert_eq!(pre.api_backend, ApiBackend::Responses);
let pre_meta = h.handle.get_last_model_metadata().await;
assert_eq!(pre_meta.resolved_model_id.as_deref(), Some("grok-4.5"));
assert_eq!(pre_meta.resolved_model_id.as_deref(), Some("kigi-4.5"));
assert_eq!(pre_meta.model_fingerprint.as_deref(), Some("fp_abc123"));
// Simulate compaction: replace conversation with compacted history.
@@ -3441,10 +3441,7 @@ async fn sampling_config_survives_compaction_replacement() {
// Post-compaction: SamplingConfig MUST be preserved.
let post = h.handle.get_sampling_config().await.unwrap();
assert_eq!(
post.model, "grok-build",
"BUG: model changed after compaction"
);
assert_eq!(post.model, "kigi", "BUG: model changed after compaction");
assert_eq!(
post.context_window.get(),
500_000,
@@ -3471,7 +3468,7 @@ async fn sampling_config_survives_compaction_replacement() {
/// After compaction, the `build_session_info` display path uses
/// `get_sampling_config().model` as the source-of-truth model slug.
/// If that model slug is e.g. "grok-build" and not in the ModelState
/// If that model slug is e.g. "kigi" and not in the ModelState
/// catalog with a display name, the pager shows the raw slug. This
/// test verifies the pager's `current_model_name()` behavior when the
/// model ID doesn't match any catalog entry.
@@ -3479,7 +3476,7 @@ async fn sampling_config_survives_compaction_replacement() {
async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
let config = SamplingConfig {
base_url: "https://api.example.com".to_string(),
model: "grok-build".to_string(),
model: "kigi".to_string(),
max_completion_tokens: None,
temperature: Some(0.7),
top_p: Some(0.95),
@@ -3497,7 +3494,7 @@ async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
ConversationItem::Assistant(kigi_sampling_types::AssistantItem {
content: "done".into(),
tool_calls: vec![],
model_id: Some("grok-4.5".into()),
model_id: Some("kigi-4.5".into()),
model_fingerprint: Some("fp_acd3142484d3ad6f".into()),
reasoning_effort: None,
}),
@@ -3507,7 +3504,7 @@ async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
// Before compaction: metadata present.
let meta = h.handle.get_last_model_metadata().await;
assert_eq!(meta.resolved_model_id.as_deref(), Some("grok-4.5"));
assert_eq!(meta.resolved_model_id.as_deref(), Some("kigi-4.5"));
assert_eq!(
meta.model_fingerprint.as_deref(),
Some("fp_acd3142484d3ad6f")
@@ -3532,7 +3529,7 @@ async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
kigi_sampling_types::AssistantItem {
content: "working on it".into(),
tool_calls: vec![],
model_id: Some("grok-4.5".into()),
model_id: Some("kigi-4.5".into()),
model_fingerprint: Some("fp_acd3142484d3ad6f".into()),
reasoning_effort: None,
},
@@ -3540,7 +3537,7 @@ async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
// Metadata recovered.
let meta = h.handle.get_last_model_metadata().await;
assert_eq!(meta.resolved_model_id.as_deref(), Some("grok-4.5"));
assert_eq!(meta.resolved_model_id.as_deref(), Some("kigi-4.5"));
assert_eq!(
meta.model_fingerprint.as_deref(),
Some("fp_acd3142484d3ad6f")
@@ -3564,10 +3561,10 @@ async fn model_metadata_lost_after_compaction_then_recovered_on_next_turn() {
async fn context_window_downgrade_triggers_auto_compact() {
use kigi_sampling_types::ApiBackend;
// Initial config: 500k context, Responses backend (matches grok-4.5)
// Initial config: 500k context, Responses backend (matches kigi-4.5)
let config = SamplingConfig {
base_url: "https://api.x.ai/v1".to_string(),
model: "grok-4.5".to_string(),
base_url: "https://byok.example/v1".to_string(),
model: "kigi-4.5".to_string(),
max_completion_tokens: None,
temperature: Some(0.7),
top_p: Some(0.95),
@@ -3606,7 +3603,7 @@ async fn context_window_downgrade_triggers_auto_compact() {
128_000,
"context_window should be overwritten by update_sampling_config"
);
assert_eq!(post.model, "grok-4.5", "model slug must not change");
assert_eq!(post.model, "kigi-4.5", "model slug must not change");
assert_eq!(
post.api_backend,
ApiBackend::Responses,
@@ -3934,7 +3931,7 @@ async fn prefix_stable_after_model_switch() {
.push_user_message(ConversationItem::user("continue"));
let new_config = SamplingConfig {
model: "grok-3-mini".to_string(),
model: "kigi-3-mini".to_string(),
..test_config()
};
h.handle.update_sampling_config(new_config);
@@ -4317,7 +4314,7 @@ async fn prefix_stable_after_session_resume() {
}
// ============================================================================
// Out-of-band history repair (x.ai/session/repair)
// Out-of-band history repair (kigi/session/repair)
// ============================================================================
/// Bricked-session shape: an orphaned tool result survives load (the eager
@@ -111,7 +111,7 @@ pub enum ChatStateCommand {
is_compaction: bool,
},
/// Out-of-band history repair (`x.ai/session/repair`): run
/// Out-of-band history repair (`kigi/session/repair`): run
/// [`crate::compaction_utils::repair_history`] and persist when changed;
/// `dry_run` only reports.
///
@@ -598,7 +598,7 @@ impl CompactionStateContext {
/// For a sub-agent with
/// a single real user turn, `recent_messages` is the ENTIRE working
/// transcript, and keeping it frees almost nothing while re-cueing the
/// model to re-read the same files. grok-build retains
/// model to re-read the same files. kigi retains
/// `recent_messages` so the model keeps verbatim tool context.
pub fn for_compaction(&self) -> Self {
Self {
@@ -803,17 +803,17 @@ pub struct CompactedHistoryInput<'a> {
/// summary. `None` means no state reminder is appended.
pub system_reminder: Option<String>,
/// When `true`, emit the compaction summary *before* recent messages.
/// When `false` (the default), recent messages come first (grok-build
/// When `false` (the default), recent messages come first (kigi
/// ordering).
pub summary_before_recent: bool,
/// Pre-built transcript hint appended to the summary (caller builds it via
/// [`crate::CompactionMode::transcript_hint`] or
/// [`format_transcript_location`]). `None` to omit. Appended to BOTH the
/// carrier and the grok-build summary.
/// carrier and the kigi summary.
pub transcript_hint: Option<String>,
/// Number of summaries generated so far for this user query, *including*
/// the one being built. Rendered verbatim into the carrier's
/// "Total summaries generated so far …" footer. Ignored by the grok-build
/// "Total summaries generated so far …" footer. Ignored by the kigi
/// (`summary_before_recent == false`) path. Callers that don't track a
/// counter pass `1`.
pub summary_count: u64,
@@ -2679,7 +2679,7 @@ actual user question";
arguments: r#"{"target_file":"src/lib.rs"}"#.into(),
},
],
model_id: Some("grok-3".to_string()),
model_id: Some("kigi-3".to_string()),
model_fingerprint: None,
reasoning_effort: None,
}),
@@ -2712,7 +2712,7 @@ actual user question";
arguments: r#"{"command":"cargo test"}"#.into(),
},
],
model_id: Some("grok-3".to_string()),
model_id: Some("kigi-3".to_string()),
model_fingerprint: None,
reasoning_effort: None,
}),
@@ -3390,7 +3390,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
assert!(
kept.iter()
.any(|i| matches!(i, ConversationItem::Reasoning(_))),
"reasoning must be kept when strip_reasoning = false (Grok backends)"
"reasoning must be kept when strip_reasoning = false (Kigi backends)"
);
let stripped = prepare_conversation_for_verbatim_summarization(mk(), true);
assert!(
+1 -1
View File
@@ -186,7 +186,7 @@ impl ChatStateHandle {
});
}
/// Out-of-band history repair (`x.ai/session/repair`); see
/// Out-of-band history repair (`kigi/session/repair`); see
/// [`ChatStateCommand::RepairHistory`]. Returns `None` if the actor is
/// dead, `Some(Err(_))` if a turn was in flight at processing time.
pub async fn repair_history(
+1 -1
View File
@@ -216,7 +216,7 @@ mod tests {
],
sampling_config: SamplingConfig {
base_url: "https://api.example.com".to_string(),
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
max_completion_tokens: Some(4096),
temperature: Some(0.7),
top_p: None,
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-config-types"
version.workspace = true
edition.workspace = true
description = "Leaf configuration value types for the grok CLI, extracted from kigi-shell for dependency inversion."
description = "Leaf configuration value types for the kigi CLI, extracted from kigi-shell for dependency inversion."
[dependencies]
agent-client-protocol = { workspace = true }
+47 -47
View File
@@ -35,7 +35,7 @@ pub struct CampaignOverride {
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default)]
pub struct DoomLoopRecoverySettings {
/// Send the `x-grok-doom-loop-check` header and parse the reported
/// Send the `x-kigi-doom-loop-check` header and parse the reported
/// triggers. `Some(false)` is a kill-switch; absent ⇒ client default (off).
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
@@ -284,7 +284,7 @@ pub struct RemoteSettings {
pub dream_check_interval_secs: Option<u64>,
#[serde(default)]
pub writeback_enabled: Option<bool>,
/// OAuth2 provider issuer URL (e.g., "https://auth.x.ai"). When present
/// OAuth2 provider issuer URL (e.g., "https://auth.kimi.com"). When present
/// together with `oauth2_client_id`, the client uses OAuth2 authorization code
/// flow. Controlled via remote settings for gradual rollout.
#[serde(default)]
@@ -292,11 +292,11 @@ pub struct RemoteSettings {
/// OAuth2 client_id for the CLI. Paired with `oauth2_issuer`.
#[serde(default)]
pub oauth2_client_id: Option<String>,
/// When `Some(true)`, enable grok's default OAuth2 (xAI auth.x.ai).
/// When `Some(true)`, enable kigi's default OAuth2 (auth.kimi.com).
/// Enterprise OIDC (user's own IdP via `oidc` config) always wins.
/// Controlled via remote settings; `--oauth` CLI flag overrides.
#[serde(default)]
pub grok_oauth_enabled: Option<bool>,
pub kigi_oauth_enabled: Option<bool>,
#[serde(default)]
pub lsp_tools_enabled: Option<bool>,
/// Folder-trust gate kill-switch / remote default. Gates whether repo-local
@@ -323,7 +323,7 @@ pub struct RemoteSettings {
/// fallback (per-server config, env, and requirements/managed override it).
#[serde(default)]
pub mcp_startup_timeout_secs: Option<u64>,
/// remote settings `grok_build_settings.max_mcp_output_bytes` — global default
/// remote settings `kigi_settings.max_mcp_output_bytes` — global default
/// MCP tool-result inline cap (bytes). Overridden by requirements, env,
/// and `config.toml [mcp] max_output_bytes`. Built-in default 20_000.
#[serde(default)]
@@ -340,7 +340,7 @@ pub struct RemoteSettings {
/// Enable/disable the runtime turn-end TodoGate remotely.
/// Precedence: CLI `--todo-gate` > this field > built-in default (`false`).
/// The gate ships disabled; set this to `Some(true)` (via the
/// `grok_build_settings` remote settings key) to enable it. See
/// `kigi_settings` remote settings key) to enable it. See
/// `session::acp_session::resolve_reminder_policy`.
#[serde(default)]
pub todo_gate_enabled: Option<bool>,
@@ -463,7 +463,7 @@ pub struct RemoteSettings {
#[serde(default)]
pub tips: Option<Vec<String>>,
/// When present, controls the non-Git-repo warning at session start.
/// Controlled via remote settings (`non_git_warning` in `grok_build_settings`).
/// Controlled via remote settings (`non_git_warning` in `kigi_settings`).
/// Takes precedence over `[features] non_git_warning` in config.toml:
/// `Some(true)` enables, `Some(false)` acts as a kill-switch, `None` falls back to local config.
#[serde(default)]
@@ -473,10 +473,10 @@ pub struct RemoteSettings {
#[serde(default)]
pub image_description_model: Option<String>,
/// Server-side pin for the next-prompt suggestion model (tab-autocomplete
/// ghost text), from the `grok_build_settings` remote settings flag. Sits below
/// ghost text), from the `kigi_settings` remote settings flag. Sits below
/// env (`KIGI_PROMPT_SUGGESTIONS_MODEL`) and `[models] prompt_suggestion`
/// in config.toml, above the client hint and the built-in
/// `grok-build-0.1` default. The effective model is catalog-guarded: when
/// `kigi-0.1` default. The effective model is catalog-guarded: when
/// it is not in the shell's model catalog the suggestion request is
/// skipped entirely (never the session model). See
/// `ModelOverrideConfig::resolve` and `handle_suggest_prompt`.
@@ -570,7 +570,7 @@ pub struct RemoteSettings {
pub sharing_enabled: Option<bool>,
/// Voice mode (STT dictation). Client default is **on** when absent.
/// `Some(false)` is a remote kill switch; `Some(true)` forces on.
/// Overridable locally via `KIGI_VOICE_MODE`. Free-tier SuperGrok upsell
/// Overridable locally via `KIGI_VOICE_MODE`. Free-tier subscription upsell
/// is a separate client tier gate.
#[serde(default)]
pub voice_mode_enabled: Option<bool>,
@@ -631,7 +631,7 @@ pub struct RemoteSettings {
pub on_demand_enabled: Option<bool>,
/// When set to a non-empty URL, the pager's `/usage` command shows a link
/// to that URL instead of fetching billing data from the backend.
/// Server-controlled via the remote settings `grok_build_usage_redirect_url`
/// Server-controlled via the remote settings `kigi_usage_redirect_url`
/// feature flag (target it at personal-team users). `None`/empty keeps the
/// default behaviour of fetching usage from the backend.
#[serde(default)]
@@ -643,8 +643,8 @@ pub struct RemoteSettings {
#[serde(default)]
pub suggestions_ai_enabled: Option<bool>,
/// Global auto-compact threshold percent (0-100) from remote settings
/// `grok_build_settings`. Per-model override on `ModelInfo`
/// (`grok_build_models`) takes precedence; user config and env var
/// `kigi_settings`. Per-model override on `ModelInfo`
/// (`kigi_models`) takes precedence; user config and env var
/// further override per the resolver chain.
#[serde(default)]
pub auto_compact_threshold_percent: Option<u8>,
@@ -761,15 +761,15 @@ where
}
/// A model + the harness whose system prompt / toolset flavor that model must
/// run against. The pair is the atomic configurable unit because a model is
/// only guaranteed to work with a compatible harness (cursor vs grok-build).
/// only guaranteed to work with a compatible harness (cursor vs kigi).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoalRoleModel {
/// Model id, e.g. "grok-4". Resolved against available models at
/// Model id, e.g. "kigi-4". Resolved against available models at
/// spawn time; unknown/unauthorized ⇒ fail-open to current model.
pub model: String,
/// Harness `agent_type` (e.g. "cursor", "grok-build-plan") whose
/// Harness `agent_type` (e.g. "cursor", "kigi-plan") whose
/// `AgentDefinition` decides the role subagent's harness flavor (system
/// prompt + cursor-vs-grok-build toolset), applied REGARDLESS of the
/// prompt + cursor-vs-kigi toolset), applied REGARDLESS of the
/// session/parent agent. Resolved by NAME (project/plugin/builtin lookup,
/// then re-flavored by the subagent toolset resolver) — NOT via the main
/// session's env/ACP/strict-harness precedence chain. NOT a subagent type:
@@ -813,18 +813,18 @@ mod tests {
}
#[test]
fn remote_settings_image_description_model_round_trip() {
let json = r#"{"image_description_model": "grok-build"}"#;
let json = r#"{"image_description_model": "kigi"}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(s.image_description_model.as_deref(), Some("grok-build"));
assert_eq!(s.image_description_model.as_deref(), Some("kigi"));
let out = serde_json::to_string(&s).unwrap();
let s2: RemoteSettings = serde_json::from_str(&out).unwrap();
assert_eq!(s2.image_description_model, s.image_description_model);
}
#[test]
fn remote_settings_prompt_suggestion_model_round_trip() {
let json = r#"{"prompt_suggestion_model": "grok-build-0.1"}"#;
let json = r#"{"prompt_suggestion_model": "kigi-0.1"}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(s.prompt_suggestion_model.as_deref(), Some("grok-build-0.1"));
assert_eq!(s.prompt_suggestion_model.as_deref(), Some("kigi-0.1"));
let out = serde_json::to_string(&s).unwrap();
let s2: RemoteSettings = serde_json::from_str(&out).unwrap();
assert_eq!(s2.prompt_suggestion_model, s.prompt_suggestion_model);
@@ -842,12 +842,12 @@ mod tests {
#[test]
fn remote_settings_goal_planner_model_round_trip() {
let json =
r#"{"goal_planner_model": {"model": "grok-4", "agent_type": "general-purpose"}}"#;
r#"{"goal_planner_model": {"model": "kigi-4", "agent_type": "general-purpose"}}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_planner_model,
Some(GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
})
);
@@ -857,12 +857,12 @@ mod tests {
}
#[test]
fn remote_settings_goal_strategist_model_round_trip() {
let json = r#"{"goal_strategist_model": {"model": "grok-4.5", "agent_type": "cursor"}}"#;
let json = r#"{"goal_strategist_model": {"model": "kigi-4.5", "agent_type": "cursor"}}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_strategist_model,
Some(GoalRoleModel {
model: "grok-4.5".to_string(),
model: "kigi-4.5".to_string(),
agent_type: "cursor".to_string(),
})
);
@@ -873,19 +873,19 @@ mod tests {
#[test]
fn remote_settings_goal_skeptic_models_fully_valid_pool_round_trips() {
let json = r#"{"goal_skeptic_models": [
{"model": "grok-4", "agent_type": "general-purpose"},
{"model": "grok-3", "agent_type": "cursor"}
{"model": "kigi-4", "agent_type": "general-purpose"},
{"model": "kigi-3", "agent_type": "cursor"}
]}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_skeptic_models,
vec![
GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
},
GoalRoleModel {
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
agent_type: "cursor".to_string(),
},
]
@@ -897,20 +897,20 @@ mod tests {
#[test]
fn remote_settings_goal_skeptic_models_one_bad_item_does_not_poison_pool() {
let json = r#"{"goal_skeptic_models": [
{"model": "grok-4", "agent_type": "general-purpose"},
{"model": "grok-broken"},
{"model": "grok-3", "agent_type": "cursor"}
{"model": "kigi-4", "agent_type": "general-purpose"},
{"model": "kigi-broken"},
{"model": "kigi-3", "agent_type": "cursor"}
]}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_skeptic_models,
vec![
GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
},
GoalRoleModel {
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
agent_type: "cursor".to_string(),
},
]
@@ -957,13 +957,13 @@ mod tests {
fn remote_settings_goal_skeptic_models_missing_model_entry_dropped() {
let json = r#"{"goal_skeptic_models": [
{"agent_type": "general-purpose"},
{"model": "grok-3", "agent_type": "cursor"}
{"model": "kigi-3", "agent_type": "cursor"}
]}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_skeptic_models,
vec![GoalRoleModel {
model: "grok-3".to_string(),
model: "kigi-3".to_string(),
agent_type: "cursor".to_string(),
}]
);
@@ -972,14 +972,14 @@ mod tests {
fn remote_settings_goal_skeptic_models_wrong_typed_scalar_dropped() {
let json = r#"{"goal_skeptic_models": [
{"model": 123, "agent_type": "general-purpose"},
{"model": "grok-3", "agent_type": ["cursor"]},
{"model": "grok-4", "agent_type": "general-purpose"}
{"model": "kigi-3", "agent_type": ["cursor"]},
{"model": "kigi-4", "agent_type": "general-purpose"}
]}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_skeptic_models,
vec![GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
}]
);
@@ -987,13 +987,13 @@ mod tests {
#[test]
fn remote_settings_goal_skeptic_models_extra_unknown_fields_kept() {
let json = r#"{"goal_skeptic_models": [
{"model": "grok-4", "agent_type": "general-purpose", "reasoning_effort": "high"}
{"model": "kigi-4", "agent_type": "general-purpose", "reasoning_effort": "high"}
]}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_skeptic_models,
vec![GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
}]
);
@@ -1045,28 +1045,28 @@ mod tests {
fn remote_settings_goal_role_models_malformed_pair_does_not_drop_other_fields() {
let json = r#"{
"goal_planner_model": {"model": "broken"},
"goal_strategist_model": {"model": "grok-4.5", "agent_type": "cursor"},
"default_model": "grok-4"
"goal_strategist_model": {"model": "kigi-4.5", "agent_type": "cursor"},
"default_model": "kigi-4"
}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(s.goal_planner_model, None);
assert_eq!(
s.goal_strategist_model,
Some(GoalRoleModel {
model: "grok-4.5".to_string(),
model: "kigi-4.5".to_string(),
agent_type: "cursor".to_string(),
})
);
assert_eq!(s.default_model.as_deref(), Some("grok-4"));
assert_eq!(s.default_model.as_deref(), Some("kigi-4"));
}
#[test]
fn remote_settings_goal_role_model_extra_unknown_fields_kept_single_pair() {
let json = r#"{"goal_planner_model": {"model": "grok-4", "agent_type": "general-purpose", "future": true}}"#;
let json = r#"{"goal_planner_model": {"model": "kigi-4", "agent_type": "general-purpose", "future": true}}"#;
let s: RemoteSettings = serde_json::from_str(json).unwrap();
assert_eq!(
s.goal_planner_model,
Some(GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
})
);
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-config"
version.workspace = true
edition.workspace = true
description = "Shared config loading for Grok — kigi_home, effective config (requirements > user > managed), TOML merge"
description = "Shared config loading for Kigi — kigi_home, effective config (requirements > user > managed), TOML merge"
[dependencies]
base64 = { workspace = true }
+1 -1
View File
@@ -1,4 +1,4 @@
//! Config file loading for Grok.
//! Config file loading for Kigi.
//!
//! Merge order (lowest → highest priority):
//! 1. `/etc/kigi/managed_config.toml`
+3 -3
View File
@@ -73,7 +73,7 @@ fn line_col(src: &str, byte: usize) -> (usize, usize) {
}
/// [`load_toml_file`] plus that layer's `[[version_overrides]]`. Use for
/// grok config files; use [`load_toml_file`] directly for unrelated TOML.
/// kigi config files; use [`load_toml_file`] directly for unrelated TOML.
pub fn load_config_file(path: &Path) -> std::io::Result<toml::Value> {
let mut v = load_toml_file(path)?;
apply_version_overrides_with_registered(&mut v)?;
@@ -637,7 +637,7 @@ mod tests {
fn load_user_config_layer_reads_file_when_home_present() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-load-layer-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-load-layer-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut f = std::fs::File::create(dir.join("config.toml")).unwrap();
writeln!(f, "[telemetry]\nmode = \"from_file\"\n").unwrap();
@@ -651,7 +651,7 @@ mod tests {
/// snippet, which can carry a secret and would reach a client caller.
#[test]
fn parse_error_keeps_kind_but_not_snippet() {
let dir = std::env::temp_dir().join(format!("grok-toml-leak-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-toml-leak-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("bad.toml");
// Duplicate key: the message names the key; the secret-bearing source line is only in Display.
@@ -67,7 +67,7 @@ pub fn mark_managed_config_synced(marker: SyncMarker<'_>) {
}
}
/// Server-side GrokBuildDeployment UUID from the last deploy-key managed-config
/// Server-side KigiDeployment UUID from the last deploy-key managed-config
/// sync, bound to the key that synced it: returns the marker's `principal` only
/// when the marker's `key_fingerprint` equals `key_fingerprint`, so a rotated or
/// removed key never reports the previous deployment's id. Team-path syncs store
@@ -257,7 +257,7 @@ fn managed_config_stale_at_is_false_without_user_home() {
#[test]
fn managed_config_stale_at_is_true_without_synced_marker() {
let dir = std::env::temp_dir().join(format!("grok-stale-nomark-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-nomark-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let _ = std::fs::remove_file(dir.join(MANAGED_CONFIG_CACHE_FILE));
// No recorded sync (even if config files exist) => stale.
@@ -267,7 +267,7 @@ fn managed_config_stale_at_is_true_without_synced_marker() {
#[test]
fn managed_config_stale_at_is_false_after_fresh_sync() {
let dir = std::env::temp_dir().join(format!("grok-stale-fresh-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-fresh-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -286,7 +286,7 @@ fn managed_config_stale_at_is_false_after_fresh_sync() {
#[test]
fn managed_deployment_id_at_requires_matching_fingerprint() {
let dir = std::env::temp_dir().join(format!("grok-dep-id-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-dep-id-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let server_dep = "37c96487-eda9-4bb2-a767-6444274423c8";
// Deploy-key path: fingerprint set, principal = server deployment UUID.
@@ -324,7 +324,7 @@ fn managed_deployment_id_at_requires_matching_fingerprint() {
#[test]
fn managed_config_stale_at_is_true_for_old_sync() {
let dir = std::env::temp_dir().join(format!("grok-stale-old-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-old-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let hour_ago = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -344,7 +344,7 @@ fn managed_config_stale_at_is_true_for_old_sync() {
/// A served-then-deleted artifact reads stale regardless of the timer.
#[test]
fn managed_config_stale_when_served_artifact_deleted() {
let dir = std::env::temp_dir().join(format!("grok-stale-artgone-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-artgone-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -366,7 +366,7 @@ fn managed_config_stale_when_served_artifact_deleted() {
/// A config-less principal that served nothing is never misread as stale.
#[test]
fn managed_config_not_stale_when_nothing_served() {
let dir = std::env::temp_dir().join(format!("grok-stale-noart-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-noart-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -385,7 +385,7 @@ fn managed_config_not_stale_when_nothing_served() {
/// A cache fetched for a different principal is stale for the current one.
#[test]
fn managed_config_stale_on_identity_mismatch() {
let dir = std::env::temp_dir().join(format!("grok-stale-ident-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-ident-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -408,7 +408,7 @@ fn managed_config_stale_on_identity_mismatch() {
/// Legacy marker (no `had_*`) is never flagged missing-artifact-stale.
#[test]
fn managed_config_legacy_marker_is_conservative() {
let dir = std::env::temp_dir().join(format!("grok-stale-legacy-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-stale-legacy-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -429,7 +429,7 @@ fn managed_config_legacy_marker_is_conservative() {
/// Hard-staleness: missing artifact or identity mismatch → true; a fresh same-identity cache → false.
#[test]
fn hard_stale_only_on_missing_or_identity() {
let dir = std::env::temp_dir().join(format!("grok-hardstale-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-hardstale-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -455,7 +455,7 @@ fn hard_stale_only_on_missing_or_identity() {
/// No marker → hard-stale (never synced → fetch before use).
#[test]
fn hard_stale_without_marker() {
let dir = std::env::temp_dir().join(format!("grok-hardstale-nomark-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-hardstale-nomark-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let _ = std::fs::remove_file(dir.join(MANAGED_CONFIG_CACHE_FILE));
assert!(is_managed_config_hard_stale_for_at(&dir, &team("team-a")));
@@ -466,7 +466,7 @@ fn hard_stale_without_marker() {
/// must not lock a managed user out) and the cache is hard-stale so the next sync rewrites it.
#[test]
fn corrupt_marker_reads_as_no_marker_and_allows() {
let dir = std::env::temp_dir().join(format!("grok-corrupt-marker-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-corrupt-marker-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("requirements.toml"), "fail_closed = true\n").unwrap();
std::fs::write(dir.join(MANAGED_CONFIG_CACHE_FILE), "{ not valid json").unwrap();
@@ -482,7 +482,7 @@ fn corrupt_marker_reads_as_no_marker_and_allows() {
/// A deploy-key switch is detected offline as an identity mismatch (`cache_unusable_for`) and refetched online.
#[test]
fn deployment_key_switch_is_stale_and_tampered_offline() {
let dir = std::env::temp_dir().join(format!("grok-dk-switch-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-dk-switch-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// Provisioned with key A: principal = served deployment_id, fingerprint = fp-a.
mark_managed_config_synced_at(
@@ -510,7 +510,7 @@ fn deployment_key_switch_is_stale_and_tampered_offline() {
/// A pre-upgrade marker (no `key_fingerprint`) must not fire when a key is now configured — it self-upgrades next sync.
#[test]
fn pre_upgrade_marker_without_fingerprint_does_not_fire_on_key() {
let dir = std::env::temp_dir().join(format!("grok-dk-preupgrade-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-dk-preupgrade-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -535,7 +535,7 @@ fn pre_upgrade_marker_without_fingerprint_does_not_fire_on_key() {
/// The team path keys on `principal` (team id), records no fingerprint, and never fires a key mismatch.
#[test]
fn team_path_keys_on_principal_not_key_fingerprint() {
let dir = std::env::temp_dir().join(format!("grok-team-nofp-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-team-nofp-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
mark_managed_config_synced_at(
&dir,
@@ -565,7 +565,7 @@ fn team_path_keys_on_principal_not_key_fingerprint() {
/// The eviction trigger fires only on a confirmed switch; first sync, same identity, `None`, and pre-upgrade markers never fire.
#[test]
fn identity_changed_only_on_confirmed_switch() {
let dir = std::env::temp_dir().join(format!("grok-ident-changed-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-ident-changed-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// No marker yet → first sync, nothing to evict.
@@ -646,7 +646,7 @@ fn identity_changed_only_on_confirmed_switch() {
/// make the gate purge / apply eviction shed a real tenant's policy.
#[test]
fn blank_principal_is_never_a_confirmed_switch() {
let dir = std::env::temp_dir().join(format!("grok-ident-blank-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-ident-blank-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// Real recorded team, blank current → not a switch.
@@ -724,7 +724,7 @@ fn blank_principal_is_never_a_confirmed_switch() {
/// Compromised only when opted in AND tampered; opted-out / never-synced / config-less / intact is never flagged.
#[test]
fn compromised_only_when_opted_in_and_deleted_or_substituted() {
let dir = std::env::temp_dir().join(format!("grok-compromised-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-compromised-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// No marker → not compromised.
@@ -787,7 +787,7 @@ fn compromised_only_when_opted_in_and_deleted_or_substituted() {
/// deleted under a fail_closed marker is compromised.
#[test]
fn compromised_on_managed_config_deletion_when_fail_closed() {
let dir = std::env::temp_dir().join(format!("grok-compromised-mc-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-compromised-mc-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("managed_config.toml"), "[cli]\n").unwrap();
mark_managed_config_synced_at(
@@ -811,7 +811,7 @@ fn compromised_on_managed_config_deletion_when_fail_closed() {
/// Deployment-key path: an opted-in marker is compromised on an offline key switch (the fingerprint is the only offline identity).
#[test]
fn compromised_on_deployment_key_switch_when_fail_closed() {
let dir = std::env::temp_dir().join(format!("grok-compromised-dk-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-compromised-dk-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// Provisioned with key A (fp-a), opted into fail_closed, artifact present.
@@ -853,7 +853,7 @@ fn compromised_on_deployment_key_switch_when_fail_closed() {
/// never a pure identity mismatch; staleness still treats that mismatch as a refetch trigger (asserted alongside).
#[test]
fn gate_excludes_pure_identity_mismatch_but_keeps_artifact_and_key_tamper() {
let dir = std::env::temp_dir().join(format!("grok-gate-fix1-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-gate-fix1-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// (1) Principal A (fail_closed), artifact intact; serving team-b = pure identity mismatch → ALLOWED.
@@ -918,7 +918,7 @@ fn gate_excludes_pure_identity_mismatch_but_keeps_artifact_and_key_tamper() {
/// Opt-in comes from the served response, not disk, so a no-write sync can't disarm the gate.
#[test]
fn mark_keeps_fail_closed_armed_without_on_disk_file() {
let dir = std::env::temp_dir().join(format!("grok-mark-disarm-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-mark-disarm-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// Opted-in policy served + present → not compromised (intact).
+6 -6
View File
@@ -261,7 +261,7 @@ mod tests {
fn load_requirements_layer_soft_fails_on_invalid_version_overrides() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-vo-soft-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-vo-soft-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("requirements.toml");
let mut f = std::fs::File::create(&path).unwrap();
@@ -285,7 +285,7 @@ telemetry = true
fn validate_requirements_layer_errs_on_fail_closed_violation() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-vo-validate-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-vo-validate-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("requirements.toml");
let mut f = std::fs::File::create(&path).unwrap();
@@ -311,7 +311,7 @@ minimum_version = "not-a-version"
fn validate_requirements_layer_ok_without_fail_closed() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-vo-soft2-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-vo-soft2-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("requirements.toml");
let mut f = std::fs::File::create(&path).unwrap();
@@ -377,7 +377,7 @@ minimum_version = "not-a-version"
fn fail_closed_key_is_stripped_from_returned_layer() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-vo-strip-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-vo-strip-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("requirements.toml");
let mut f = std::fs::File::create(&path).unwrap();
@@ -403,7 +403,7 @@ minimum_version = "not-a-version"
fn load_user_requirements_reads_layer_when_home_present() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-req-load-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-req-load-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut f = std::fs::File::create(dir.join("requirements.toml")).unwrap();
writeln!(f, "[features]\ntelemetry = true\n").unwrap();
@@ -423,7 +423,7 @@ minimum_version = "not-a-version"
fn validate_user_requirements_errs_on_fail_closed_violation() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("grok-req-validate-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("kigi-req-validate-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut f = std::fs::File::create(dir.join("requirements.toml")).unwrap();
writeln!(
@@ -438,7 +438,7 @@ mod imp {
/// Install the crash handler. Must be called early in `main()`, before any
/// terminal initialization or async runtime setup.
pub fn install(crash_dir: &Path, grok_version: &str) -> bool {
pub fn install(crash_dir: &Path, kigi_version: &str) -> bool {
let crash_file = crash_dir.join("last-crash.bin");
// Create the crash directory if it doesn't exist.
@@ -467,8 +467,8 @@ mod imp {
unsafe {
let version = &mut *std::ptr::addr_of_mut!(APP_VERSION);
version.fill(0);
let copy_len = grok_version.len().min(format::VERSION_STRING_LEN);
version[..copy_len].copy_from_slice(&grok_version.as_bytes()[..copy_len]);
let copy_len = kigi_version.len().min(format::VERSION_STRING_LEN);
version[..copy_len].copy_from_slice(&kigi_version.as_bytes()[..copy_len]);
}
save_termios();
@@ -749,7 +749,7 @@ mod win {
}
}
pub fn install(crash_dir: &Path, grok_version: &str) -> bool {
pub fn install(crash_dir: &Path, kigi_version: &str) -> bool {
use std::os::windows::ffi::OsStrExt;
let crash_file = crash_dir.join("last-crash.bin");
@@ -784,8 +784,8 @@ mod win {
unsafe {
let version = &mut *std::ptr::addr_of_mut!(APP_VERSION);
version.fill(0);
let copy_len = grok_version.len().min(format::VERSION_STRING_LEN);
version[..copy_len].copy_from_slice(&grok_version.as_bytes()[..copy_len]);
let copy_len = kigi_version.len().min(format::VERSION_STRING_LEN);
version[..copy_len].copy_from_slice(&kigi_version.as_bytes()[..copy_len]);
}
unsafe {
@@ -50,7 +50,7 @@ pub fn resolve_frames(blob: &CrashBlob) -> Vec<ResolvedFrame> {
pub fn format_report(blob: &CrashBlob, frames: &[ResolvedFrame]) -> String {
let mut out = String::with_capacity(4096);
out.push_str("=== Grok Crash Report ===\n\n");
out.push_str("=== Kigi Crash Report ===\n\n");
out.push_str(&format!("Signal: {}\n", signal_name(blob.signal)));
out.push_str(&format!(
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-env"
version.workspace = true
edition.workspace = true
description = "Backend environment presets for the Grok CLI crate family: endpoint URL defaults and env-var test support."
description = "Backend environment presets for the Kigi CLI crate family: endpoint URL defaults and env-var test support."
[features]
# single shared rlib per crate, so downstream Bazel test targets need the
+6 -6
View File
@@ -968,7 +968,7 @@ fn try_btrfs_remove(
// Known residual TOCTOU: validation `lstat`s/canonicalizes then we
// delete by path (the `btrfs subvolume delete` CLI takes a path, not
// an fd, so there is no `unlinkat` to close the window). Bounded by:
// `btrfs` refuses non-subvolumes, the snapshot dir is grok-owned, and
// `btrfs` refuses non-subvolumes, the snapshot dir is kigi-owned, and
// `..`/symlink targets are already rejected. Accepted as-is.
if let Some(report) = delete_snapshot_with_delegate_fallback(
&resolved,
@@ -2186,7 +2186,7 @@ mod tests {
use kigi_test_utils::git::{git_commit_all, init_git_repo};
// Isolate KIGI_SHARE_DIR so the post-removal unregister writes to a private DB.
#[cfg(feature = "metadata")]
let _fx = crate::db::GrokHomeFixture::new();
let _fx = crate::db::KigiHomeFixture::new();
let tmp = tempfile::TempDir::new().unwrap();
let repo = tmp.path().join("repo");
@@ -2837,7 +2837,7 @@ mod tests {
fn register_worktree_writes_correct_fields() {
// Isolate KIGI_SHARE_DIR so register_worktree's open_default write lands
// in our own DB (lock + private tmp + restore via the fixture).
let fx = crate::db::GrokHomeFixture::new();
let fx = crate::db::KigiHomeFixture::new();
// Unique basename → unique id, so a concurrent open_default writer
// (KIGI_SHARE_DIR is process-global) can't INSERT-OR-REPLACE our row.
@@ -3411,7 +3411,7 @@ mod tests {
// remove_worktree must keep the DB record when the on-disk removal
// fails, so the worktree isn't lost from tracking while leaking on
// disk (unregister only after a successful removal).
let fx = crate::db::GrokHomeFixture::new();
let fx = crate::db::KigiHomeFixture::new();
// A regular file makes remove_dir_all fail (ENOTDIR) deterministically.
let wt_path = fx.home.join("doomed-wt");
@@ -3454,7 +3454,7 @@ mod tests {
kigi_test_utils::require_git!();
use kigi_test_utils::git::{git_commit_all, init_git_repo};
let fx = crate::db::GrokHomeFixture::new();
let fx = crate::db::KigiHomeFixture::new();
// A real repo + a real worktree so remove_worktree succeeds on disk.
let repo = fx.home.join("repo");
@@ -3504,7 +3504,7 @@ mod tests {
// KIGI_SHARE_DIR == the gc DB dir so remove_worktree's open_default
// unregister hits the same DB the gc record lives in.
let fx = crate::db::GrokHomeFixture::new();
let fx = crate::db::KigiHomeFixture::new();
let db = WorktreeDb::open(&fx.home).unwrap();
let dir = fx.home.join("expired-wt");
@@ -141,7 +141,7 @@ pub fn create_snapshot_with_symlink(btrfs_info: &BtrfsInfo, dest: &Path) -> Resu
})?;
} else if !is_safe_snapshot_delete_target(&snapshot_path) {
bail!(
"refusing to delete pre-existing snapshot {}: outside grok-managed \
"refusing to delete pre-existing snapshot {}: outside kigi-managed \
btrfs storage",
snapshot_path.display()
);
@@ -251,7 +251,7 @@ pub fn snapshot_dest_path(btrfs_mount: &Path, subvolume_root: &Path, dest: &Path
///
/// Symlinks cross mount namespaces and persist across process exits, so the
/// worktree at `dest` stays visible to the user's other shells and survives a
/// grok restart.
/// kigi restart.
///
/// Destructive contract: a pre-existing **stale symlink** at `dest` is unlinked;
/// a pre-existing **directory** is removed only if empty (`remove_dir`). A
@@ -351,7 +351,7 @@ pub fn delete_snapshot(path: &Path) -> Result<()> {
/// canonicalized parent's final component is one of [`BTRFS_SNAPSHOT_SUBDIRS`]
/// (`worktrees` or `.kigi-snapshots`),
/// - and that directory sits **directly under a real btrfs mount point** (from
/// the live mount table), anchoring the delete to grok-managed storage rather
/// the live mount table), anchoring the delete to kigi-managed storage rather
/// than any directory that merely happens to be named `worktrees`.
///
/// Treat all symlink targets and metadata paths as untrusted input and pass
@@ -500,7 +500,7 @@ mod tests {
#[test]
fn test_copy_git_dir_preserves_worktree_source_marker() {
// A worktree-from-worktree (standalone) must inherit the source's
// `grok-worktree-source` marker so it still points at the ultimate
// `kigi-worktree-source` marker so it still points at the ultimate
// main repo rather than the intermediate worktree.
let temp = TempDir::new().unwrap();
let source_git = temp.path().join("source/.git");
@@ -508,12 +508,12 @@ mod tests {
std::fs::create_dir_all(&source_git).unwrap();
std::fs::write(source_git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::write(source_git.join("grok-worktree-source"), "/main/repo").unwrap();
std::fs::write(source_git.join("kigi-worktree-source"), "/main/repo").unwrap();
copy_git_dir(&source_git, &dest_git).unwrap();
assert_eq!(
std::fs::read_to_string(dest_git.join("grok-worktree-source")).unwrap(),
std::fs::read_to_string(dest_git.join("kigi-worktree-source")).unwrap(),
"/main/repo"
);
}
@@ -203,7 +203,7 @@ impl WorktreeDb {
/// Open the default DB at `~/.kigi/worktrees.db`.
///
/// Discovers grok home via `$KIGI_SHARE_DIR`, falling back to the canonicalized
/// Discovers kigi home via `$KIGI_SHARE_DIR`, falling back to the canonicalized
/// `$HOME/.kigi` (matching `kigi_config::kigi_home`).
/// Path is resolved fresh each call (~1µs env var read) to support
/// test overrides. Each call opens its own connection — callers in hot
@@ -365,23 +365,23 @@ static KIGI_SHARE_DIR_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(())
/// `Drop` restores `KIGI_SHARE_DIR` before `_lock` releases, so the env is correct
/// before another waiting setter proceeds.
#[cfg(test)]
pub(crate) struct GrokHomeFixture {
pub(crate) struct KigiHomeFixture {
_lock: std::sync::MutexGuard<'static, ()>,
prev: Option<std::ffi::OsString>,
/// The isolated grok home; pass to `WorktreeDb::open` to read the same DB
/// The isolated kigi home; pass to `WorktreeDb::open` to read the same DB
/// `open_default()` writes to.
pub home: PathBuf,
_tmp: tempfile::TempDir,
}
#[cfg(test)]
impl GrokHomeFixture {
impl KigiHomeFixture {
pub(crate) fn new() -> Self {
let lock = KIGI_SHARE_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let tmp = tempfile::TempDir::new().unwrap();
let home = tmp.path().join("grok-home");
let home = tmp.path().join("kigi-home");
std::fs::create_dir_all(&home).unwrap();
// Warm up the DB (journal-mode conversion + schema) before exposing it
// via KIGI_SHARE_DIR, sparing the test hot loop set_journal_mode's retry
@@ -401,7 +401,7 @@ impl GrokHomeFixture {
}
#[cfg(test)]
impl Drop for GrokHomeFixture {
impl Drop for KigiHomeFixture {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
@@ -247,7 +247,7 @@ fn scratch_index_path() -> PathBuf {
.unwrap_or(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"grok-snapshot-index-{}-{nanos}-{seq}",
"kigi-snapshot-index-{}-{nanos}-{seq}",
std::process::id()
))
}
@@ -259,7 +259,7 @@ fn scratch_index_path() -> PathBuf {
/// (even if they also match a `.gitignore` rule); only *untracked* files
/// matching `.gitignore` are excluded.
///
/// `ref_name` must be a fully-qualified ref (e.g. `refs/grok/subagents/<id>`);
/// `ref_name` must be a fully-qualified ref (e.g. `refs/kigi/subagents/<id>`);
/// it is overwritten unconditionally. The worktree must have a valid `HEAD`
/// (subagent worktrees are detached at their base commit), which becomes the
/// snapshot commit's parent (provenance only).
@@ -301,8 +301,8 @@ fn snapshot_worktree_to_ref_inner(
message: &str,
) -> Result<String> {
// Synthetic identity scoped to this call so it is never written to git config.
const NAME: &str = "Grok Snapshot";
const EMAIL: &str = "grok-snapshot@example.com";
const NAME: &str = "Kigi Snapshot";
const EMAIL: &str = "kigi-snapshot@example.com";
// Stage against a throwaway index so the worktree's real index is untouched.
let scratch = ScratchIndexGuard {
@@ -690,7 +690,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/snapshots/test";
let ref_name = "refs/kigi/snapshots/test";
let snap = snapshot_worktree_to_ref(&wt, ref_name, "snapshot test").unwrap();
assert!(!snap.is_empty());
@@ -738,7 +738,7 @@ mod tests {
std::fs::write(wt.join("ignored.txt"), "secret").unwrap();
std::fs::write(wt.join("kept.txt"), "keep me").unwrap();
let ref_name = "refs/grok/snapshots/ignored";
let ref_name = "refs/kigi/snapshots/ignored";
snapshot_worktree_to_ref(&wt, ref_name, "ignore test").unwrap();
let listing =
@@ -776,7 +776,7 @@ mod tests {
// Edit the tracked-but-ignored file in the worktree.
std::fs::write(wt.join("config.env"), "v2").unwrap();
let ref_name = "refs/grok/snapshots/tracked-ignored";
let ref_name = "refs/kigi/snapshots/tracked-ignored";
let snap = snapshot_worktree_to_ref(&wt, ref_name, "tracked-then-ignored").unwrap();
// A file tracked in HEAD must survive even though it matches .gitignore,
@@ -806,7 +806,7 @@ mod tests {
std::fs::write(repo_path.join("tracked.txt"), "original").unwrap();
git_commit_all(&repo_path, "initial");
let ref_name = "refs/grok/snapshots/clean";
let ref_name = "refs/kigi/snapshots/clean";
let snap = snapshot_worktree_to_ref(&repo_path, ref_name, "clean snapshot").unwrap();
let snap_tree =
@@ -823,7 +823,7 @@ mod tests {
kigi_test_utils::require_git!();
let temp = TempDir::new().unwrap();
let (_repo, wt) = repo_with_worktree(&temp);
let ref_name = "refs/grok/snapshots/overwrite";
let ref_name = "refs/kigi/snapshots/overwrite";
std::fs::write(wt.join("tracked.txt"), "first").unwrap();
let snap1 = snapshot_worktree_to_ref(&wt, ref_name, "first").unwrap();
@@ -851,7 +851,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/snapshots/survives";
let ref_name = "refs/kigi/snapshots/survives";
let snap = snapshot_worktree_to_ref(&wt, ref_name, "pre-removal").unwrap();
// Delete the worktree dir; the snapshot lives in the shared object/ref store.
@@ -895,7 +895,7 @@ mod tests {
!before.is_empty(),
"precondition: there are pending changes"
);
snapshot_worktree_to_ref(&wt, "refs/grok/snapshots/noindex", "no mutate").unwrap();
snapshot_worktree_to_ref(&wt, "refs/kigi/snapshots/noindex", "no mutate").unwrap();
let after = git_capture_in(&wt, &["status", "--porcelain"], &[]).unwrap();
assert_eq!(
@@ -916,7 +916,7 @@ mod tests {
// A tracked file whose working-tree content has CRLF line endings.
std::fs::write(wt.join("tracked.txt"), "line1\r\nline2\r\n").unwrap();
let ref_name = "refs/grok/snapshots/crlf";
let ref_name = "refs/kigi/snapshots/crlf";
let snap = snapshot_worktree_to_ref(&wt, ref_name, "crlf").unwrap();
// The snapshot blob must keep the raw CRLF bytes: our `-c
@@ -946,7 +946,7 @@ mod tests {
let name = "λ space.txt";
std::fs::write(wt.join(name), "x").unwrap();
let ref_name = "refs/grok/snapshots/unicode";
let ref_name = "refs/kigi/snapshots/unicode";
snapshot_worktree_to_ref(&wt, ref_name, "unicode path").unwrap();
// Read the tree with the same hardening (`core.quotepath=false`) so the
@@ -992,7 +992,7 @@ mod tests {
std::fs::write(wt.join("lf.txt"), "a\nb\n").unwrap();
let snap =
snapshot_worktree_to_ref(&wt, "refs/grok/snapshots/roundtrip", "round trip").unwrap();
snapshot_worktree_to_ref(&wt, "refs/kigi/snapshots/roundtrip", "round trip").unwrap();
let base = git_capture_in(&repo_path, &["rev-parse", &format!("{snap}^")], &[]).unwrap();
// Dispose of the worktree dir; only the ref/objects survive.
@@ -1054,7 +1054,7 @@ mod tests {
// Build a PARENTLESS commit holding the same working state, so its `^`
// never resolves — exercising the base-unreachable fallback without
// depending on gc to prune a real base.
let snap = snapshot_worktree_to_ref(&wt, "refs/grok/snapshots/orphan-src", "src").unwrap();
let snap = snapshot_worktree_to_ref(&wt, "refs/kigi/snapshots/orphan-src", "src").unwrap();
let tree = git_capture_in(&wt, &["rev-parse", &format!("{snap}^{{tree}}")], &[]).unwrap();
let ident = [
("GIT_AUTHOR_NAME", "T"),
@@ -1100,7 +1100,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let snap = snapshot_worktree_to_ref(&wt, "refs/grok/snapshots/idem", "idem").unwrap();
let snap = snapshot_worktree_to_ref(&wt, "refs/kigi/snapshots/idem", "idem").unwrap();
crate::remove_worktree(&wt).unwrap();
// First rehydrate recreates the dest dir.
@@ -1141,7 +1141,7 @@ mod tests {
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
std::fs::write(wt.join("untracked.txt"), "brand new").unwrap();
let ref_name = "refs/grok/subagents/standalone";
let ref_name = "refs/kigi/subagents/standalone";
let snap = snapshot_worktree_to_ref(&wt, ref_name, "standalone snapshot").unwrap();
// The snapshot lives only in the standalone's own `.git`, NOT in source.
@@ -1192,11 +1192,11 @@ mod tests {
let temp = TempDir::new().unwrap();
// Isolate the worktree DB (lock + KIGI_SHARE_DIR → private tmp + restore).
let fx = crate::db::GrokHomeFixture::new();
let fx = crate::db::KigiHomeFixture::new();
let (repo_path, wt) = repo_with_worktree(&temp);
std::fs::write(wt.join("tracked.txt"), "edited").unwrap();
let snap = snapshot_worktree_to_ref(&wt, "refs/grok/snapshots/db", "db test").unwrap();
let snap = snapshot_worktree_to_ref(&wt, "refs/kigi/snapshots/db", "db test").unwrap();
crate::remove_worktree(&wt).unwrap();
// Rehydrate into a UNIQUE-basename dest so its DB id can't collide with
@@ -226,7 +226,7 @@ fn log_unknown_mount_ns_once() {
if !LOGGED.swap(true, Ordering::Relaxed) {
tracing::info!(
"cannot read /proc/1/ns/mnt (likely non-root); treating mount namespace as \
non-private overlay/bind strategies stay enabled. If grok is in a private \
non-private overlay/bind strategies stay enabled. If kigi is in a private \
namespace as non-root, worktrees may be ephemeral."
);
}
@@ -220,7 +220,7 @@ pub(crate) fn execute_create_worktree(plan: WorktreePlan) -> Result<CreateWorktr
Ok(result)
}
/// Record the source repo root in `<worktree>/.git/grok-worktree-source`.
/// Record the source repo root in `<worktree>/.git/kigi-worktree-source`.
///
/// A standalone worktree is an independent repo whose `.git` is a directory:
/// nothing inside it points back to the source, so consumers like `.envrc`
@@ -234,7 +234,7 @@ fn record_main_repo_marker(source: &Path, worktree: &Path) {
if !git_dir.is_dir() {
return;
}
let marker = git_dir.join("grok-worktree-source");
let marker = git_dir.join("kigi-worktree-source");
if marker.exists() {
return;
}
@@ -1685,7 +1685,7 @@ mod tests {
record_main_repo_marker(&source, &dest);
let marker = dest.join(".git/grok-worktree-source");
let marker = dest.join(".git/kigi-worktree-source");
let recorded = std::fs::read_to_string(&marker).expect("marker should be written");
assert!(
Path::new(recorded.trim()).join(".git").is_dir(),
@@ -1704,7 +1704,7 @@ mod tests {
record_main_repo_marker(&source, &dest);
assert!(!dest.join(".git/grok-worktree-source").exists());
assert!(!dest.join(".git/kigi-worktree-source").exists());
}
#[test]
@@ -1717,7 +1717,7 @@ mod tests {
let dest = tmp.path().join("dest");
std::fs::create_dir_all(dest.join(".git")).unwrap();
let marker = dest.join(".git/grok-worktree-source");
let marker = dest.join(".git/kigi-worktree-source");
std::fs::write(&marker, "/the/ultimate/main/repo").unwrap();
record_main_repo_marker(&source, &dest);
@@ -108,7 +108,7 @@ mod tests {
writer.emit(Event::TurnStarted {
session_id: "test-session".into(),
turn_number: 1,
model_id: "grok-3".into(),
model_id: "kigi-3".into(),
yolo_mode: false,
conversation_message_count: 0,
session_relationship: SessionRelationship::Primary,
@@ -462,7 +462,7 @@ pub enum Event {
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InterjectionSource {
/// Direct `x.ai/interject` while a turn was running (Ctrl+Enter).
/// Direct `kigi/interject` while a turn was running (Ctrl+Enter).
Direct,
/// A queued (not-yet-running) prompt promoted into the running turn via
/// `InterjectQueuedPrompt` (queue "send now").
@@ -477,7 +477,7 @@ pub enum InterjectionSource {
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RedirectKind {
/// Mid-turn interjection — Ctrl+O / `x.ai/interject`, or "Send now" on a
/// Mid-turn interjection — Ctrl+O / `kigi/interject`, or "Send now" on a
/// queued row. The turn keeps running; nothing is cancelled.
Interjection,
/// The turn was aborted (Ctrl+C / Esc) and the user then typed and sent a
@@ -696,7 +696,7 @@ mod tests {
let with_kind = serde_json::to_value(Event::TurnStarted {
session_id: "s".into(),
turn_number: 2,
model_id: "grok-4".into(),
model_id: "kigi-4".into(),
yolo_mode: false,
conversation_message_count: 3,
session_relationship: SessionRelationship::Primary,
@@ -710,7 +710,7 @@ mod tests {
let normal = serde_json::to_value(Event::TurnStarted {
session_id: "s".into(),
turn_number: 1,
model_id: "grok-4".into(),
model_id: "kigi-4".into(),
yolo_mode: false,
conversation_message_count: 0,
session_relationship: SessionRelationship::Primary,
@@ -745,26 +745,26 @@ mod tests {
attempt: 2,
consecutive_failures: 6,
every: 3,
model_id: "grok-4".to_string(),
model_id: "kigi-4".to_string(),
};
let v = serde_json::to_value(&ev).unwrap();
assert_eq!(v["type"], "goal_strategist_fired");
assert_eq!(v["attempt"], 2);
assert_eq!(v["consecutive_failures"], 6);
assert_eq!(v["every"], 3);
assert_eq!(v["model_id"], "grok-4");
assert_eq!(v["model_id"], "kigi-4");
}
#[test]
fn goal_summarizer_events_serialize_tag_and_fields() {
let fired = Event::GoalSummarizerFired {
attempt: 2,
model_id: "grok-4".to_string(),
model_id: "kigi-4".to_string(),
};
let v = serde_json::to_value(&fired).unwrap();
assert_eq!(v["type"], "goal_summarizer_fired");
assert_eq!(v["attempt"], 2);
assert_eq!(v["model_id"], "grok-4");
assert_eq!(v["model_id"], "kigi-4");
let completed = Event::GoalSummarizerCompleted {
attempt: 2,
@@ -792,7 +792,7 @@ mod tests {
let ev = Event::GoalRoleModelResolved {
role: "skeptic",
skeptic_idx: Some(2),
model_id: "grok-4".to_string(),
model_id: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
source: "remote",
};
@@ -800,7 +800,7 @@ mod tests {
assert_eq!(v["type"], "goal_role_model_resolved");
assert_eq!(v["role"], "skeptic");
assert_eq!(v["skeptic_idx"], 2);
assert_eq!(v["model_id"], "grok-4");
assert_eq!(v["model_id"], "kigi-4");
assert_eq!(v["agent_type"], "general-purpose");
assert_eq!(v["source"], "remote");
}
@@ -810,7 +810,7 @@ mod tests {
let ev = Event::GoalRoleModelResolved {
role: "planner",
skeptic_idx: None,
model_id: "grok-4".to_string(),
model_id: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
source: "remote",
};
+3 -3
View File
@@ -21,7 +21,7 @@ fn parse_aws_credentials(content: &str) -> anyhow::Result<aws_sdk_s3::config::Cr
&parsed.aws_secret_access_key,
parsed.aws_session_token,
None,
"grok-shell-trace-upload",
"kigi-shell-trace-upload",
));
}
@@ -51,7 +51,7 @@ fn parse_aws_credentials(content: &str) -> anyhow::Result<aws_sdk_s3::config::Cr
&s,
token,
None,
"grok-shell-trace-upload",
"kigi-shell-trace-upload",
)),
_ => anyhow::bail!(
"AWS credentials are neither valid JSON \
@@ -109,7 +109,7 @@ pub(crate) async fn build_s3_client(
"test",
None,
None,
"grok-shell-test",
"kigi-shell-test",
));
}
@@ -252,7 +252,7 @@ mod tests {
use super::*;
#[test]
fn grok_dirs_are_unsafe() {
fn kigi_dirs_are_unsafe() {
if let Some(home) = dirs::home_dir() {
assert!(!is_project_dir(&home.join(".kigi")));
assert!(!is_project_dir(&home.join(".kigi/bin")));
@@ -260,7 +260,7 @@ mod tests {
}
#[test]
fn grok_prefixed_dirs_are_unsafe() {
fn kigi_prefixed_dirs_are_unsafe() {
if let Some(home) = dirs::home_dir() {
assert!(!is_project_dir(&home.join(".kigi-proxy-work")));
}
@@ -14,7 +14,7 @@
//! Tree shapes are scaled replicas of synthetic large-repo measurements:
//! - `js`: a JS/turbo monorepo where `node_modules/` trees nested below the
//! top level dominate the directory count (the shape behind the original
//! "grok holds 55k inotify watches" report).
//! "kigi holds 55k inotify watches" report).
//! - `large`: a wide multi-language monorepo — 44 top-level dirs, ~52k
//! non-ignored dirs, ~7k nested-ignored, a large top-level `target/`, and
//! a `.git` with 13k+ internal dirs (objects/modules/logs/refs-remotes).
+1 -1
View File
@@ -3767,7 +3767,7 @@ mod tests {
#[test]
fn external_ancestor_sl_arms_in_recursive_root_mode() {
// Subdir cwd whose `.sl` lives in an ancestor *outside* watch_path
// (e.g. `grok` run in `crates/codegen`): the production guard must
// (e.g. `kigi` run in `crates/codegen`): the production guard must
// still attach the watch under a recursive root (fanout=false).
let temp = TempDir::new().unwrap();
let repo = dunce::canonicalize(temp.path()).unwrap();
@@ -1,6 +1,6 @@
//! Shared DTO types for hooks/plugins ACP extensions.
//!
//! This crate defines the wire format for `x.ai/hooks/*` and `x.ai/plugins/*`
//! This crate defines the wire format for `kigi/hooks/*` and `kigi/plugins/*`
//! ACP extension methods. It is dependency-free (only `serde`) so both
//! `kigi-shell` and `kigi-tui` can depend on it without pulling
//! in domain logic.
@@ -38,11 +38,11 @@ pub enum PluginOrigin {
/// CLI `--plugin-dir`.
CliOverride,
/// Project `.kigi/plugins/`.
ProjectGrok,
ProjectKigi,
/// Project `.claude/plugins/`.
ProjectClaude,
/// `$KIGI_SHARE_DIR/plugins/`.
UserGrok,
UserKigi,
/// `~/.claude/plugins/`.
UserClaude,
/// A compat marketplace clone.
@@ -56,7 +56,7 @@ pub enum PluginOrigin {
#[serde(default, skip_serializing_if = "Option::is_none")]
marketplace: Option<String>,
},
/// Grok's install registry (marketplace or direct git/local install).
/// Kigi's install registry (marketplace or direct git/local install).
MarketplaceInstall {
/// Marketplace source display name (None for direct installs).
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -207,7 +207,7 @@ pub struct HookInfo {
pub disabled: bool,
}
/// Response for `x.ai/hooks/list`.
/// Response for `kigi/hooks/list`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HooksListResponse {
@@ -274,7 +274,7 @@ pub struct PluginInfo {
pub conflict: Option<String>,
}
/// Response for `x.ai/plugins/list`.
/// Response for `kigi/plugins/list`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginsListResponse {
@@ -332,7 +332,7 @@ pub struct McpServerInfo {
pub config_source: Option<String>,
}
/// Response for `x.ai/mcp/list` as consumed by the pager.
/// Response for `kigi/mcp/list` as consumed by the pager.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServersListResponse {
@@ -513,7 +513,7 @@ impl PluginComponents {
// Action types
// ---------------------------------------------------------------------------
/// Request wrapper for `x.ai/hooks/action`.
/// Request wrapper for `kigi/hooks/action`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HooksActionRequest {
@@ -552,7 +552,7 @@ pub enum HooksAction {
},
}
/// Request wrapper for `x.ai/plugins/action`.
/// Request wrapper for `kigi/plugins/action`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginsActionRequest {
@@ -593,7 +593,7 @@ pub enum PluginsAction {
},
}
/// Shared action response for both `x.ai/hooks/action` and `x.ai/plugins/action`.
/// Shared action response for both `kigi/hooks/action` and `kigi/plugins/action`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ActionOutcome {
@@ -739,7 +739,7 @@ mod tests {
mcp_server_count: 0,
mcp_status: McpStatus::None,
marketplace_source: None,
origin: Some(PluginOrigin::UserGrok),
origin: Some(PluginOrigin::UserKigi),
conflict: None,
};
let json = serde_json::to_string(&plugin).unwrap();
@@ -756,9 +756,9 @@ mod tests {
fn plugin_origin_serde_roundtrip_all_variants() {
for origin in [
PluginOrigin::CliOverride,
PluginOrigin::ProjectGrok,
PluginOrigin::ProjectKigi,
PluginOrigin::ProjectClaude,
PluginOrigin::UserGrok,
PluginOrigin::UserKigi,
PluginOrigin::UserClaude,
PluginOrigin::ClaudeMarketplace {
marketplace: "mp".into(),
@@ -1094,10 +1094,10 @@ mod tests {
}
// ---------------------------------------------------------------------------
// Marketplace types (wire format for x.ai/marketplace/* ACP endpoints)
// Marketplace types (wire format for kigi/marketplace/* ACP endpoints)
// ---------------------------------------------------------------------------
/// Response for `x.ai/marketplace/list`.
/// Response for `kigi/marketplace/list`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplaceListResponse {
@@ -1170,7 +1170,7 @@ pub struct MarketplacePluginEntry {
pub remote_subdir: Option<String>,
}
/// Request wrapper for `x.ai/marketplace/action`.
/// Request wrapper for `kigi/marketplace/action`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplaceActionRequest {
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-hooks"
version.workspace = true
edition.workspace = true
description = "Runtime hook system for Grok — file-based discovery, command execution, and policy enforcement"
description = "Runtime hook system for Kigi — file-based discovery, command execution, and policy enforcement"
[dependencies]
fastrand = { workspace = true }
+2 -2
View File
@@ -1,6 +1,6 @@
# Hook Examples
Sample hooks for Grok. Copy to `~/.kigi/hooks/` to enable globally, or to `<project>/.kigi/hooks/` for project-scoped hooks (requires `/hooks-trust`).
Sample hooks for Kigi. Copy to `~/.kigi/hooks/` to enable globally, or to `<project>/.kigi/hooks/` for project-scoped hooks (requires `/hooks-trust`).
## Available Examples
@@ -93,7 +93,7 @@ Hook files use the Claude-compatible JSON format:
```
- **Event names:** `SessionStart`, `PreToolUse`, `PostToolUse`, `SessionEnd`
- **Matcher:** regex on tool name. Claude names like `Bash`, `Read`, `Edit` are auto-expanded to also match Grok names (`run_terminal_cmd`, `read_file`, `search_replace`)
- **Matcher:** regex on tool name. Claude names like `Bash`, `Read`, `Edit` are auto-expanded to also match Kigi names (`run_terminal_cmd`, `read_file`, `search_replace`)
- **Timeout:** in seconds (default: 5)
- **Command:** path to script (relative to hook file directory) or inline shell command
@@ -361,7 +361,7 @@ def main() -> None:
command = extract_command(envelope)
if command is None or not command_is_recursive(command):
sys.exit(0) # nothing to block -> silent allow
# Deny. Emit the grok-native decision (read by this repo's runner) and the
# Deny. Emit the kigi-native decision (read by this repo's runner) and the
# Claude-style hookSpecificOutput for forward-compatibility, put the reason
# on stderr for runners that surface it there, and exit 2 so any exit-code
# based runner blocks too.
@@ -2,7 +2,7 @@
# session-log.sh — append session events to an audit log
#
# Reads the hook envelope from stdin and appends a one-line JSON entry
# to ~/.grok/session-audit.log with event name, session ID, cwd, and
# to ~/.kigi/session-audit.log with event name, session ID, cwd, and
# timestamp.
INPUT=$(cat)
@@ -12,7 +12,7 @@ SESSION=$(echo "$INPUT" | grep -o '"sessionId":"[^"]*"' | sed 's/"sessionId":"//
CWD=$(echo "$INPUT" | grep -o '"cwd":"[^"]*"' | sed 's/"cwd":"//;s/"$//')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_FILE="${HOME}/.grok/session-audit.log"
LOG_FILE="${HOME}/.kigi/session-audit.log"
mkdir -p "$(dirname "$LOG_FILE")"
echo "{\"timestamp\":\"${TIMESTAMP}\",\"event\":\"${EVENT}\",\"session\":\"${SESSION}\",\"cwd\":\"${CWD}\"}" >> "$LOG_FILE"
@@ -2,7 +2,7 @@
# tool-logger.sh — log tool calls to a local activity file
#
# Reads the hook envelope from stdin and appends a one-line JSON entry
# to ~/.grok/tool-activity.log with event name, tool name, and timestamp.
# to ~/.kigi/tool-activity.log with event name, tool name, and timestamp.
# `toolName` is the resolved tool (e.g. `linear__save_issue` for MCP calls).
INPUT=$(cat)
@@ -12,7 +12,7 @@ TOOL=$(echo "$INPUT" | grep -o '"toolName":"[^"]*"' | head -1 | sed 's/"toolName
BACKGROUNDED=$(echo "$INPUT" | grep -o '"isBackgrounded":[a-z]*' | sed 's/"isBackgrounded"://')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_FILE="${HOME}/.grok/tool-activity.log"
LOG_FILE="${HOME}/.kigi/tool-activity.log"
mkdir -p "$(dirname "$LOG_FILE")"
echo "{\"timestamp\":\"${TIMESTAMP}\",\"event\":\"${EVENT}\",\"tool\":\"${TOOL}\",\"backgrounded\":${BACKGROUNDED:-false}}" >> "$LOG_FILE"
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::matcher::HookMatcher;
#[derive(Debug)]
pub struct HooksMap {
pub events: HashMap<HookEventName, Vec<MatcherGroup>>,
/// Event names present in the JSON but not recognized by Grok.
/// Event names present in the JSON but not recognized by Kigi.
pub skipped_events: Vec<String>,
}
+1 -1
View File
@@ -650,7 +650,7 @@ mod tests {
fn load_from_settings_file_no_hooks_key() {
let dir = tempfile::tempdir().unwrap();
let settings = dir.path().join("settings.json");
std::fs::write(&settings, r#"{"theme": "dark", "model": "grok-3"}"#).unwrap();
std::fs::write(&settings, r#"{"theme": "dark", "model": "kigi-3"}"#).unwrap();
let (registry, errors) =
load_hooks_from_sources(&[HookSource::SettingsFile(&settings)], &[]);
+1 -1
View File
@@ -20,7 +20,7 @@ pub struct PreToolUseResult {
/// Hook failures (timeouts, crashes, command-not-found, env-var
/// pre-spawn refusals, malformed output) are **fail-open**: the failure
/// is logged and surfaced in the per-hook results for the UI scrollback,
/// but the tool call continues as if the hook had allowed it. Grok
/// but the tool call continues as if the hook had allowed it. Kigi
/// runs in protected environments where induced-failure bypass of
/// security hooks is not part of the threat model; the previous
/// fail-closed posture over-blocked innocent tool calls when
+3 -3
View File
@@ -63,7 +63,7 @@ use std::collections::HashMap;
/// "Apple logo" PUA char) plus a long magic ASCII prefix. The full
/// sentinel string adds 128 bits of per-call entropy as a hex suffix
/// followed by another `U+F8FF` char.
const SENTINEL_PREFIX: &str = "\u{f8ff}__GROK_HOOKS_MASK_";
const SENTINEL_PREFIX: &str = "\u{f8ff}__KIGI_HOOKS_MASK_";
const SENTINEL_SUFFIX: &str = "__\u{f8ff}";
/// Build a per-call sentinel string used to hide modifier-form
@@ -704,7 +704,7 @@ mod tests {
}
/// An earlier sentinel was a fixed string
/// `"\u{f8ff}__GROK_HOOKS_MASK__\u{f8ff}"`. A user-supplied
/// `"\u{f8ff}__KIGI_HOOKS_MASK__\u{f8ff}"`. A user-supplied
/// `extra_env` value containing that exact byte sequence would
/// have been silently rewritten to `${` by the unmask step. The
/// per-call randomized sentinel removes this hazard. This
@@ -713,7 +713,7 @@ mod tests {
/// though the input also references that variable through `${VAL}`.
#[test]
fn expand_preserves_pre_existing_legacy_fixed_sentinel_in_extra() {
let legacy_sentinel = "\u{f8ff}__GROK_HOOKS_MASK__\u{f8ff}";
let legacy_sentinel = "\u{f8ff}__KIGI_HOOKS_MASK__\u{f8ff}";
let mut extra = HashMap::new();
// Value embeds the legacy sentinel followed by what would
// have been parsed as an identifier+brace if the unmask
+1 -1
View File
@@ -529,7 +529,7 @@ mod tests {
prompt_id: None,
payload: HookPayload::SessionStart {
source: "new".into(),
model_id: Some("grok-3".into()),
model_id: Some("kigi-3".into()),
agent_type: None,
},
};
+2 -2
View File
@@ -1,11 +1,11 @@
//! # kigi-hooks
//!
//! Runtime hook system for Grok — file-based discovery, command execution,
//! Runtime hook system for Kigi — file-based discovery, command execution,
//! and policy enforcement.
//!
//! ## Overview
//!
//! This crate provides a minimal hooks system for Grok. Hooks are discovered
//! This crate provides a minimal hooks system for Kigi. Hooks are discovered
//! from dedicated directories (`~/.kigi/hooks/` and `<git-worktree-root>/.kigi/hooks/`),
//! defined in JSON files (compatible settings format), and executed as child processes.
//!
+12 -12
View File
@@ -6,9 +6,9 @@ use regex::Regex;
///
/// - an empty pattern or `"*"` matches every tool;
/// - a "simple" pattern (only `[A-Za-z0-9_|]`, i.e. a plain name or `|`-list) is an
/// **exact** match against each name (after external→Grok alias expansion), NOT a regex;
/// **exact** match against each name (after external→Kigi alias expansion), NOT a regex;
/// - anything else is an **unanchored** regex (also tested against the tool's external
/// alias names, so e.g. `^Bash$` matches the Grok tool `run_terminal_command`).
/// alias names, so e.g. `^Bash$` matches the Kigi tool `run_terminal_command`).
///
/// The simple-vs-regex split is deliberate: it avoids anchoring a `|`-alternation (a
/// naive `^a|b|c$` anchors only the first/last term and silently over-matches). Whitespace
@@ -61,8 +61,8 @@ fn is_simple_form(pattern: &str) -> bool {
}
/// Expand a simple-form pattern into the exact set of names it matches: each `|`-term
/// plus any Grok tool names that term aliases (so `"Bash"` also matches
/// `run_terminal_command`), per the shared external-name to Grok registry in
/// plus any Kigi tool names that term aliases (so `"Bash"` also matches
/// `run_terminal_command`), per the shared external-name to Kigi registry in
/// `kigi-tools`. Empty terms and duplicates are dropped.
fn exact_names(pattern: &str) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
@@ -155,22 +155,22 @@ mod tests {
// ── External tool-name aliases ────────────────────────────────
#[test]
fn claude_bash_matches_grok_tool() {
fn claude_bash_matches_kigi_tool() {
let m = HookMatcher::new("Bash").unwrap();
assert!(m.is_match("Bash")); // external alias name
assert!(m.is_match("run_terminal_command")); // Grok name
assert!(m.is_match("run_terminal_command")); // Kigi name
assert!(!m.is_match("read_file"));
// Bug-fix regression: exact, not prefix.
assert!(!m.is_match("run_terminal_command_v2"));
}
#[test]
fn claude_edit_write_matches_grok_tool_exactly() {
fn claude_edit_write_matches_kigi_tool_exactly() {
let m = HookMatcher::new("Edit|Write").unwrap();
assert!(m.is_match("Edit"));
assert!(m.is_match("Write"));
assert!(m.is_match("search_replace")); // Grok equivalent
assert!(m.is_match("hashline_edit")); // second Grok alias
assert!(m.is_match("search_replace")); // Kigi equivalent
assert!(m.is_match("hashline_edit")); // second Kigi alias
assert!(!m.is_match("read_file"));
// The old anchoring bug matched these; the exact-list mode must not.
assert!(!m.is_match("Editorial"));
@@ -178,7 +178,7 @@ mod tests {
}
#[test]
fn claude_read_matches_grok_tool() {
fn claude_read_matches_kigi_tool() {
let m = HookMatcher::new("Read").unwrap();
assert!(m.is_match("Read"));
assert!(m.is_match("read_file"));
@@ -186,8 +186,8 @@ mod tests {
}
#[test]
fn regex_against_claude_alias_matches_grok_tool() {
// A regex written against an external alias still matches the Grok tool
fn regex_against_claude_alias_matches_kigi_tool() {
// A regex written against an external alias still matches the Kigi tool
// (legacy alias-name expansion).
let m = HookMatcher::new("^Bash$").unwrap();
assert!(m.is_match("run_terminal_command"));
@@ -87,7 +87,7 @@ pub async fn run_command_hook(
let mut cmd = if is_shell_command {
// Refuse to spawn when the command interpolates an env var that
// we can't resolve from any of: the runner's always-set vars, the
// per-hook extra_env (plugin vars), or Grok's own process env. The
// per-hook extra_env (plugin vars), or Kigi's own process env. The
// alternative is letting sh expand the var to empty -- which then
// produces a broken command, exits 127, and (for PreToolUse hooks)
// fails closed with an opaque "exit code 127" reason. Catching it
@@ -287,7 +287,7 @@ pub(crate) const RUNNER_ALWAYS_SET_ENV: &[&str] = &[
/// * the runner's always-set env vars (see [`RUNNER_ALWAYS_SET_ENV`]),
/// * the per-hook `extra_env` map (set by the plugin adapter for plugin
/// hooks),
/// * the Grok process's own environment (which is inherited by the child),
/// * the Kigi process's own environment (which is inherited by the child),
/// * local shell assignments inside the command itself (e.g. an
/// `INPUT=$(cat)` earlier in the string defines `INPUT` for the rest of
/// the command).
+6 -6
View File
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
// exist only to migrate prior grants out of the legacy file.
/// Path to the legacy project-hook trust file
/// (`<user_kigi_home>/trusted-hook-projects`), or `None` when no user grok home
/// (`<user_kigi_home>/trusted-hook-projects`), or `None` when no user kigi home
/// resolves. Retained only for the one-time migration into folder-trust.
pub fn legacy_trust_file_path() -> Option<PathBuf> {
Some(kigi_config::user_kigi_home()?.join("trusted-hook-projects"))
@@ -59,7 +59,7 @@ fn is_hook_disabled_with_file(hook_name: &str, file: &Path) -> bool {
/// Disable a hook by name. Adds to .
pub fn disable_hook(hook_name: &str) -> Result<(), String> {
let file = disabled_hooks_file_path()
.ok_or_else(|| "no user grok home (set $KIGI_SHARE_DIR or $HOME)".to_string())?;
.ok_or_else(|| "no user kigi home (set $KIGI_SHARE_DIR or $HOME)".to_string())?;
disable_hook_with_file(hook_name, &file)
}
@@ -122,7 +122,7 @@ fn enable_hook_with_file(hook_name: &str, file: &Path) -> Result<bool, String> {
Ok(true)
}
/// Returns the path to `$KIGI_SHARE_DIR/disabled-hooks`, or `None` when no user grok
/// Returns the path to `$KIGI_SHARE_DIR/disabled-hooks`, or `None` when no user kigi
/// home resolves.
fn disabled_hooks_file_path() -> Option<PathBuf> {
Some(kigi_config::user_kigi_home()?.join("disabled-hooks"))
@@ -134,9 +134,9 @@ mod tests {
/// Each test creates its own legacy file in its own temp dir -- no shared state.
fn trust_file_in(dir: &Path) -> PathBuf {
let grok_dir = dir.join(".kigi");
std::fs::create_dir_all(&grok_dir).unwrap();
grok_dir.join("trusted-hook-projects")
let kigi_dir = dir.join(".kigi");
std::fs::create_dir_all(&kigi_dir).unwrap();
kigi_dir.join("trusted-hook-projects")
}
#[test]
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-http"
version.workspace = true
edition.workspace = true
description = "Shared reqwest HTTP clients and User-Agent construction for the grok CLI."
description = "Shared reqwest HTTP clients and User-Agent construction for the kigi CLI."
[dependencies]
reqwest = { workspace = true, features = ["blocking"] }
+26 -12
View File
@@ -488,7 +488,7 @@ mod tests {
#[test]
fn origin_client_info_from_meta_extracts_identifier_and_version() {
let meta = serde_json::json!({
"clientIdentifier": "grok-desktop",
"clientIdentifier": "kigi-desktop",
"clientVersion": "1.2.3",
})
.as_object()
@@ -497,7 +497,7 @@ mod tests {
assert_eq!(
origin_client_info_from_meta(Some(&meta)),
Some(OriginClientInfo {
product: "grok-desktop".to_string(),
product: "kigi-desktop".to_string(),
version: Some("1.2.3".to_string()),
})
);
@@ -505,8 +505,11 @@ mod tests {
#[test]
fn origin_client_info_from_meta_uses_client_type_when_identifier_absent() {
// `kigi_web` is used (not the pager) because its user-agent label is
// distinct from the Generic default's `kigi` — a clientType parse
// failure would fall back to Generic and this assert would catch it.
let meta = serde_json::json!({
"clientType": "grok_pager",
"clientType": "kigi_web",
"clientVersion": "0.1.2",
})
.as_object()
@@ -515,28 +518,39 @@ mod tests {
assert_eq!(
origin_client_info_from_meta(Some(&meta)),
Some(OriginClientInfo {
product: "grok-pager".to_string(),
product: "kigi-web".to_string(),
version: Some("0.1.2".to_string()),
})
);
// First-party clients collapse to the plain `kigi` product (PRD F3).
let pager_meta = serde_json::json!({ "clientType": "kigi_pager" })
.as_object()
.cloned()
.unwrap();
assert_eq!(
origin_client_info_from_meta(Some(&pager_meta))
.expect("pager clientType resolves")
.product,
"kigi"
);
}
#[test]
fn merge_origin_client_info_preserves_primary_product_and_backfills_version() {
let merged = merge_origin_client_info(
Some(OriginClientInfo {
product: "grok-web".to_string(),
product: "kigi-web".to_string(),
version: None,
}),
Some(OriginClientInfo {
product: "grok-desktop".to_string(),
product: "kigi-desktop".to_string(),
version: Some("1.2.3".to_string()),
}),
);
assert_eq!(
merged,
Some(OriginClientInfo {
product: "grok-web".to_string(),
product: "kigi-web".to_string(),
version: Some("1.2.3".to_string()),
})
);
@@ -545,18 +559,18 @@ mod tests {
#[test]
fn session_user_agent_string_renders_expected_variants() {
let with_version = session_user_agent_string(&OriginClientInfo {
product: "grok-desktop".to_string(),
product: "kigi-desktop".to_string(),
version: Some("1.2.3".to_string()),
});
assert!(with_version.starts_with("grok-desktop/1.2.3 kigi/"));
assert!(with_version.starts_with("kigi-desktop/1.2.3 kigi/"));
assert!(with_version.contains(" ("));
let without_version = session_user_agent_string(&OriginClientInfo {
product: "grok-web".to_string(),
product: "kigi-web".to_string(),
version: None,
});
assert!(without_version.starts_with("grok-web kigi/"));
assert!(!without_version.starts_with("grok-web/"));
assert!(without_version.starts_with("kigi-web kigi/"));
assert!(!without_version.starts_with("kigi-web/"));
}
#[test]
+1 -1
View File
@@ -2,7 +2,7 @@
//!
//! This crate provides:
//! - Actor-based hunk tracking with source attribution (Agent vs External)
//! - Integration with grok-shell sessions
//! - Integration with kigi-shell sessions
//!
//! ## Actor Pattern
//!
+1 -1
View File
@@ -36,7 +36,7 @@ pub(crate) fn non_blocking_file_writer(path: &Path) -> std::io::Result<NonBlocki
}
/// Drop all parked worker guards, flushing their non-blocking writers. Call at
/// process exit so short-lived runs (e.g. headless `grok -p`) don't lose buffered logs.
/// process exit so short-lived runs (e.g. headless `kigi -p`) don't lose buffered logs.
pub(crate) fn flush_file_log_guards() {
if let Some(m) = FILE_LOG_GUARDS.get() {
// Recover from a poisoned mutex so exit-flush still drains the guards.
+21 -21
View File
@@ -28,15 +28,15 @@ use kigi_config::kigi_home;
/// Which env var requested a single-file debug log (drives filter and diagnostics).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DebugSource {
GrokLogFile,
GrokDebugLog,
KigiLogFile,
KigiDebugLog,
}
impl DebugSource {
fn label(self) -> &'static str {
match self {
Self::GrokLogFile => "KIGI_LOG_FILE",
Self::GrokDebugLog => "KIGI_DEBUG_LOG",
Self::KigiLogFile => "KIGI_LOG_FILE",
Self::KigiDebugLog => "KIGI_DEBUG_LOG",
}
}
}
@@ -62,7 +62,7 @@ pub const RMCP_SSE_NOISE_TARGET: &str = "rmcp::transport::common::client_side_ss
// Broad firehose filter for the routing/KIGI_DEBUG_LOG sources: capture our
// crates at debug regardless of a narrowing RUST_LOG, with deps at info so they
// don't flood. Curated first-party allowlist: new grok crates default to `info`
// don't flood. Curated first-party allowlist: new kigi crates default to `info`
// until added here.
const FIREHOSE_BASE_DIRECTIVES: &str = "info,kigi_tui=debug,kigi_shell=debug,kigi_tools=debug,kigi_log=debug,kigi_agent=debug,kigi_mcp=debug,kigi_acp_lib=debug,sampling_log=off";
@@ -382,8 +382,8 @@ where
}
Some(DebugTarget::SingleFile { path, src }) => {
let filter = match src {
DebugSource::GrokLogFile => default_file_filter(),
DebugSource::GrokDebugLog => firehose_filter(),
DebugSource::KigiLogFile => default_file_filter(),
DebugSource::KigiDebugLog => firehose_filter(),
};
match build_file_layer::<S>(&path, filter) {
Ok(layer) => registry.with(layer).init(),
@@ -417,11 +417,11 @@ pub(crate) enum DebugTarget {
///
/// Read via `var_os` (not `var`) so a non-UTF-8 path isn't silently dropped.
pub(crate) fn resolve_debug_target() -> Option<DebugTarget> {
let grok_log_file = std::env::var_os("KIGI_LOG_FILE");
let grok_debug_log = std::env::var_os("KIGI_DEBUG_LOG");
let kigi_log_file = std::env::var_os("KIGI_LOG_FILE");
let kigi_debug_log = std::env::var_os("KIGI_DEBUG_LOG");
resolve_debug_target_inner(
grok_log_file.as_deref(),
grok_debug_log.as_deref(),
kigi_log_file.as_deref(),
kigi_debug_log.as_deref(),
&kigi_home().join("debug"),
)
}
@@ -447,19 +447,19 @@ fn os_path(v: &OsStr) -> PathBuf {
// `OsStr` so non-UTF-8 paths round-trip; only the bool-vs-path discrimination
// needs UTF-8 (a non-UTF-8 value can't be a bool keyword, so it's a path).
fn resolve_debug_target_inner(
grok_log_file: Option<&OsStr>,
grok_debug_log: Option<&OsStr>,
kigi_log_file: Option<&OsStr>,
kigi_debug_log: Option<&OsStr>,
debug_dir: &Path,
) -> Option<DebugTarget> {
if let Some(raw) = grok_log_file
if let Some(raw) = kigi_log_file
&& !is_blank(raw)
{
return Some(DebugTarget::SingleFile {
path: os_path(raw),
src: DebugSource::GrokLogFile,
src: DebugSource::KigiLogFile,
});
}
let raw = grok_debug_log?;
let raw = kigi_debug_log?;
match raw.to_str().map(str::trim) {
Some("" | "0" | "false" | "off" | "no") => None,
Some("1" | "true" | "on" | "yes") => Some(DebugTarget::PerSession {
@@ -468,7 +468,7 @@ fn resolve_debug_target_inner(
// Any other UTF-8 value, or a non-UTF-8 value (`None`), is an explicit path.
_ => Some(DebugTarget::SingleFile {
path: os_path(raw),
src: DebugSource::GrokDebugLog,
src: DebugSource::KigiDebugLog,
}),
}
}
@@ -596,7 +596,7 @@ mod tests {
target,
DebugTarget::SingleFile {
path: PathBuf::from("/tmp/custom.log"),
src: DebugSource::GrokDebugLog,
src: DebugSource::KigiDebugLog,
}
);
}
@@ -613,7 +613,7 @@ mod tests {
target,
DebugTarget::SingleFile {
path: PathBuf::from("/tmp/explicit.log"),
src: DebugSource::GrokLogFile,
src: DebugSource::KigiLogFile,
}
);
}
@@ -651,7 +651,7 @@ mod tests {
let target = resolve_debug_target_inner(None, Some(raw), Path::new("/debug")).unwrap();
match target {
DebugTarget::SingleFile { path, src } => {
assert_eq!(src, DebugSource::GrokDebugLog);
assert_eq!(src, DebugSource::KigiDebugLog);
assert_eq!(path.as_os_str(), raw);
}
other => panic!("expected SingleFile for non-UTF-8 path, got {other:?}"),
@@ -954,7 +954,7 @@ mod tests {
fn prune_old_logs_missing_dir_is_noop() {
// Best-effort: a nonexistent debug dir must not panic.
prune_old_logs(
Path::new("/no/such/grok/debug/dir"),
Path::new("/no/such/kigi/debug/dir"),
std::time::Duration::from_secs(1),
);
}
+3 -3
View File
@@ -12,9 +12,9 @@
//! ## Enabling
//!
//! ```bash
//! KIGI_HOOKS_LOG=1 grok # enable, write to ~/.kigi/logs/hooks.log
//! KIGI_HOOKS_LOG=/tmp/h.log grok # write to custom path
//! KIGI_HOOKS_LOG=0 grok # explicitly disable
//! KIGI_HOOKS_LOG=1 kigi # enable, write to ~/.kigi/logs/hooks.log
//! KIGI_HOOKS_LOG=/tmp/h.log kigi # write to custom path
//! KIGI_HOOKS_LOG=0 kigi # explicitly disable
//! tail -f ~/.kigi/logs/hooks.log # watch in another terminal
//! ```
+1 -1
View File
@@ -13,7 +13,7 @@
//!
//! ```bash
//! # build with memory logging enabled, then:
//! KIGI_MEMORY_LOG=0 grok # disable even when enabled
//! KIGI_MEMORY_LOG=0 kigi # disable even when enabled
//! tail -f ~/.kigi/logs/memory.log # watch in another terminal
//! ```
+13 -13
View File
@@ -1,7 +1,7 @@
//! Centralized unified log for cross-component session observability.
//!
//! Shell writes directly via [`emit()`]. Pager and desktop forward entries
//! over ACP (`x.ai/log` notifications); shell receives them in
//! over ACP (`kigi/log` notifications); shell receives them in
//! [`ingest_client_entries()`] and writes on their behalf.
use std::fs::{self, File, OpenOptions};
@@ -29,7 +29,7 @@ const LOG_FILE: &str = "unified.jsonl";
pub const MAX_SIZE: u64 = 5 * 1024 * 1024; // 5 MB
/// ACP method name for unified log notifications.
pub const LOG_METHOD: &str = "x.ai/log";
pub const LOG_METHOD: &str = "kigi/log";
// ---------------------------------------------------------------------------
// Log entry types
@@ -52,12 +52,12 @@ pub enum LogSource {
#[strum(serialize = "shell")]
#[serde(rename = "shell")]
Shell,
#[strum(serialize = "grok-pager")]
#[serde(rename = "grok-pager")]
GrokPager,
#[strum(serialize = "grok-desktop")]
#[serde(rename = "grok-desktop")]
GrokDesktop,
#[strum(serialize = "kigi-pager")]
#[serde(rename = "kigi-pager")]
KigiPager,
#[strum(serialize = "kigi-desktop")]
#[serde(rename = "kigi-desktop")]
KigiDesktop,
}
/// A single unified log entry, written as one JSONL line.
@@ -95,7 +95,7 @@ pub struct LogEntry {
pub ctx: Option<serde_json::Value>,
}
/// Wire format for the `x.ai/log` ACP notification params.
/// Wire format for the `kigi/log` ACP notification params.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogNotificationParams {
/// Source component identifier.
@@ -254,7 +254,7 @@ pub fn emit(lvl: LogLevel, msg: &str, sid: Option<&str>, ctx: Option<serde_json:
/// Ingest a batch of log entries from a client (pager or desktop).
///
/// Called by the `x.ai/log` notification handler. Entries from
/// Called by the `kigi/log` notification handler. Entries from
/// [`LogSource::Shell`] are rejected to prevent spoofing.
pub fn ingest_client_entries(src: LogSource, entries: &[ClientLogEntry]) {
if matches!(src, LogSource::Shell) || entries.is_empty() {
@@ -381,7 +381,7 @@ mod tests {
fn log_entry_serializes_full() {
let entry = LogEntry {
ts: "2025-07-14T10:30:00.123Z".into(),
src: LogSource::GrokPager,
src: LogSource::KigiPager,
pid: Some(4242),
ver: Some("0.1.211".into()),
lvl: LogLevel::Warn,
@@ -465,7 +465,7 @@ mod tests {
for bad in &[
r#"{"src":"evil","entries":[]}"#,
r#"{"src":"","entries":[]}"#,
r#"{"src":"GROK-PAGER","entries":[]}"#,
r#"{"src":"KIGI-PAGER","entries":[]}"#,
] {
assert!(serde_json::from_str::<LogNotificationParams>(bad).is_err());
}
@@ -474,7 +474,7 @@ mod tests {
#[test]
fn notification_params_round_trip() {
let params = LogNotificationParams {
src: LogSource::GrokPager,
src: LogSource::KigiPager,
entries: vec![
ClientLogEntry {
ts: "2025-07-14T10:30:00.123Z".into(),
+1 -1
View File
@@ -3,7 +3,7 @@ license = "Apache-2.0"
name = "kigi-markdown-core"
version.workspace = true
edition.workspace = true
description = "Headless markdown analysis sharing Grok Build's exact pulldown-cmark config."
description = "Headless markdown analysis sharing Kigi's exact pulldown-cmark config."
[dependencies]
pulldown-cmark = { workspace = true }
+6 -6
View File
@@ -1,19 +1,19 @@
//! Headless markdown analysis sharing Grok Build's exact `pulldown-cmark` config.
//! Headless markdown analysis sharing Kigi's exact `pulldown-cmark` config.
//!
//! This crate is intentionally lean -- it depends only on `pulldown-cmark` -- so it
//! can be used without pulling in the terminal-rendering stack (syntect, ratatui,
//! two-face). [`parser_options`] is the single source of truth for the parser
//! feature set, shared with `kigi-markdown` so analysis matches what Grok
//! feature set, shared with `kigi-markdown` so analysis matches what Kigi
//! Build actually renders 1:1.
//!
//! After parsing, Grok applies [`offset_events`]: only `~~…~~` is strikethrough.
//! After parsing, Kigi applies [`offset_events`]: only `~~…~~` is strikethrough.
//! Single-tilde pairs (`~text~`) are demoted to literal `~` text so LLM output
//! like `~**10%**` is not struck (pulldown treats those pairs as strike; we do not).
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use std::ops::Range;
/// The exact `pulldown-cmark` option set Grok Build uses to render markdown.
/// The exact `pulldown-cmark` option set Kigi uses to render markdown.
///
/// With `ENABLE_STRIKETHROUGH`, pulldown treats both `~~…~~` and single-`~` pairs as
/// strike. Callers must consume events via [`offset_events`] so only double-tilde
@@ -26,7 +26,7 @@ pub fn parser_options() -> Options {
| Options::ENABLE_TABLES
}
/// Offset event stream from Grok's parser, with single-tilde strikethrough demoted.
/// Offset event stream from Kigi's parser, with single-tilde strikethrough demoted.
///
/// Prefer this over `Parser::new_ext(...).into_offset_iter()` so analysis and
/// rendering agree on what counts as strikethrough.
@@ -294,7 +294,7 @@ fn detect_malformed_tables(
}
}
/// Parse `text` with Grok Build's options; count elements and flag structural issues.
/// Parse `text` with Kigi's options; count elements and flag structural issues.
pub fn analyze(text: &str) -> MarkdownAnalysis {
let mut stats = MarkdownStats::default();
let mut issues = Vec::new();
+5 -5
View File
@@ -1,9 +1,9 @@
//! rmcp transport bridge over the ACP reverse channel.
//!
//! In-process SDK MCP servers (the official `grok-agent-sdk`'s `@tool` /
//! In-process SDK MCP servers (the official `kigi-agent-sdk`'s `@tool` /
//! `create_sdk_mcp_server`) run in the SDK-host process, not behind a socket. The
//! agent reaches them by sending each MCP JSON-RPC message to the client as a
//! reverse `x.ai/mcp/sdk_call` request and feeding the response back. This module
//! reverse `kigi/mcp/sdk_call` request and feeding the response back. This module
//! adapts that request/response channel into an rmcp transport so an in-process
//! server reuses the same `RunningService` / tool-dispatch path as HTTP/stdio
//! servers for tool calls.
@@ -28,7 +28,7 @@ use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, DuplexStream};
/// Sends one MCP JSON-RPC message to an in-process server over the ACP reverse
/// channel (`x.ai/mcp/sdk_call`) and returns its JSON-RPC response. The `Err` string is
/// channel (`kigi/mcp/sdk_call`) and returns its JSON-RPC response. The `Err` string is
/// surfaced as a JSON-RPC error to the waiting rmcp request (fail-closed: a missing
/// tool server is a real error, unlike a hook gate).
///
@@ -65,7 +65,7 @@ const INTERNAL_ERROR_CODE: i64 = -32603;
/// Build an rmcp transport that bridges to an in-process MCP server via `invoker`.
///
/// Spawns a pump that forwards each client→server message as a reverse
/// `x.ai/mcp/sdk_call` and writes the server→client response back. The pump exits when
/// `kigi/mcp/sdk_call` and writes the server→client response back. The pump exits when
/// rmcp drops its half of the duplex (service shutdown), so it never leaks.
///
/// `invoke_timeout` is the resolved per-server tool timeout; it bounds every reverse
@@ -159,7 +159,7 @@ async fn read_requests(
}
};
// An id-less message is a notification (no response). The SDK peer rejects reverse
// `x.ai/mcp/sdk_call`s without a JSON-RPC id, so id-less messages (e.g. rmcp's
// `kigi/mcp/sdk_call`s without a JSON-RPC id, so id-less messages (e.g. rmcp's
// `notifications/initialized` on every handshake) are logged and discarded locally
// rather than spawning a doomed round-trip. Safe only because the SDK `Server` is
// lenient about never receiving `initialized` (a documented v1 limit).
+3 -3
View File
@@ -77,7 +77,7 @@ impl McpCredentialStore {
/// Save the credential store to the default path.
pub fn save_default(&self) -> Result<()> {
let path = Self::default_path().ok_or_else(|| {
McpCredentialError::Other("no user grok home (set $KIGI_SHARE_DIR or $HOME)".into())
McpCredentialError::Other("no user kigi home (set $KIGI_SHARE_DIR or $HOME)".into())
})?;
self.save_to(&path)
}
@@ -99,7 +99,7 @@ impl McpCredentialStore {
creds: rmcp::transport::auth::StoredCredentials,
) -> Result<()> {
let path = Self::default_path().ok_or_else(|| {
McpCredentialError::Other("no user grok home (set $KIGI_SHARE_DIR or $HOME)".into())
McpCredentialError::Other("no user kigi home (set $KIGI_SHARE_DIR or $HOME)".into())
})?;
let lock_path = path.with_extension("lock");
@@ -417,7 +417,7 @@ mod tests {
#[test]
fn save_and_load_from_file() {
let dir = std::env::temp_dir().join("grok-mcp-credentials-test");
let dir = std::env::temp_dir().join("kigi-mcp-credentials-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test_creds.json");
+2 -2
View File
@@ -22,7 +22,7 @@ use crate::rmcp::transport::auth::{AuthorizationManager, OAuthClientConfig};
/// Client name advertised to MCP servers during Dynamic Client Registration
/// (RFC 7591). Surfaces as the application name on third-party OAuth consent
/// screens (e.g. Linear, GitHub), so keep this human-recognizable.
const MCP_OAUTH_CLIENT_NAME: &str = "Grok";
const MCP_OAUTH_CLIENT_NAME: &str = "Kigi";
/// How often the interactive OAuth flow polls the credential store to detect
/// a login completed in another window or process.
@@ -31,7 +31,7 @@ const CREDENTIAL_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_
// ---------------------------------------------------------------------------
// Two-layer dedup: prevents duplicate browser tabs both within one process
// (multiple async tasks / sessions) and across separate processes (leader
// mode disabled, multiple `grok` invocations).
// mode disabled, multiple `kigi` invocations).
//
// Layer 1 (cross-process): filesystem lock at $KIGI_SHARE_DIR/mcp_auth_{safe_name}.lock
// Layer 2 (in-process): watch channel so only one task runs the flow
+37 -37
View File
@@ -310,10 +310,10 @@ impl InitProgress {
}
/// One in-process SDK MCP server registration: its tool-namespace name and the
/// SDK-side id echoed back in `x.ai/mcp/sdk_call`. A named struct (rather than a
/// SDK-side id echoed back in `kigi/mcp/sdk_call`. A named struct (rather than a
/// `(String, String)` tuple) so callers can't transpose the two strings.
///
/// `Deserialize`d straight from a `_meta["x.ai/mcp/servers"]` entry, so the
/// `Deserialize`d straight from a `_meta["kigi/mcp/servers"]` entry, so the
/// `serverId` wire field name is declared (and serde-checked) exactly once here.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AcpServerEntry {
@@ -322,7 +322,7 @@ pub struct AcpServerEntry {
pub server_id: String,
}
/// The session's in-process SDK MCP servers (declared via `_meta["x.ai/mcp/servers"]`,
/// The session's in-process SDK MCP servers (declared via `_meta["kigi/mcp/servers"]`,
/// reached over the ACP reverse channel), bundled with the shared reverse-RPC invoker.
/// Held as `McpState::acp_mcp: Option<_>` so the set is one atom — present together or
/// absent, never "servers without an invoker" — and survives `update_configs` clears
@@ -332,7 +332,7 @@ struct AcpMcpRegistry {
/// Registered servers (`name -> serverId`).
servers: Vec<AcpServerEntry>,
/// Shared reverse-RPC invoker all these servers' tools are called through (emits
/// `x.ai/mcp/sdk_call` over the ACP connection).
/// `kigi/mcp/sdk_call` over the ACP connection).
invoker: Arc<dyn crate::acp_transport::AcpReverseInvoker>,
}
@@ -381,7 +381,7 @@ pub struct McpState {
/// task. When `Some`, the state — and every [`McpClient`] reached
/// through [`Self::all_clients`] / [`Self::get_client`] — forwards
/// [`McpClientEvent`]s here for coalescing and fan-out as ACP
/// `x.ai/mcp/server_status` notifications.
/// `kigi/mcp/server_status` notifications.
///
/// Intentionally `None` in subagent-pool / shared-pool snapshots
/// ([`SharedMcpPool`]) where the **parent** session is the
@@ -441,7 +441,7 @@ impl McpState {
/// [`McpClient::set_event_tx`] **before**
/// `get_tool_registrations` (so `ensure_initialized`'s
/// `Ready`/`HandshakeFailed` emit fires with `Some(tx)` and the
/// `GrokClientHandler` cloned during `try_handshake` reads
/// `KigiClientHandler` cloned during `try_handshake` reads
/// through the same Arc).
pub fn set_client_event_tx(
&mut self,
@@ -1219,8 +1219,8 @@ pub struct McpTool {
/// so the LLM can invoke them during a conversation.
/// - **App-visible only** (`["app"]`): not registered in `ToolBridge`, so the LLM
/// never sees them. These are UI-only actions (e.g. refresh buttons) surfaced to
/// the frontend via `x.ai/mcp/tools_changed` notifications and callable via
/// `x.ai/mcp/call`.
/// the frontend via `kigi/mcp/tools_changed` notifications and callable via
/// `kigi/mcp/call`.
pub struct McpToolRegistration {
pub name: String,
pub description: String,
@@ -2201,7 +2201,7 @@ enum PendingTransport {
auth_manager: Arc<tokio::sync::Mutex<rmcp::transport::auth::AuthorizationManager>>,
},
/// In-process SDK MCP server reached over the ACP reverse channel
/// (`x.ai/mcp/sdk_call`). Rebuildable from its `server_id` + invoker, so handshake
/// (`kigi/mcp/sdk_call`). Rebuildable from its `server_id` + invoker, so handshake
/// failures restore like Http (unlike the consumed Stdio child).
Acp {
server_id: String,
@@ -2210,14 +2210,14 @@ enum PendingTransport {
}
/// A connected MCP service (rmcp's RunningService wrapped in Arc).
/// Uses [`GrokClientHandler`] rather than rmcp's default `ClientInfo`
/// Uses [`KigiClientHandler`] rather than rmcp's default `ClientInfo`
/// handler: rmcp 2.1 parameterizes `RunningService` over the handler
/// type, and `ClientInfo` is only a `ClientHandler` impl with no
/// notification routing. The custom handler keeps the same protocol
/// behavior (same `get_info`) while plumbing
/// `tools/list_changed` / `resources/list_changed` notifications
/// through to the session-actor dispatcher.
pub type McpService = Arc<RunningService<RoleClient, GrokClientHandler>>;
pub type McpService = Arc<RunningService<RoleClient, KigiClientHandler>>;
/// MCP client connection state machine.
///
@@ -2290,13 +2290,13 @@ pub enum LivenessCheck {
/// 1. [`crate::liveness::spawn_transport_liveness`] when an `is_healthy`
/// poll observes that the rmcp service loop has shut down its receiver
/// (`TransportClosed`).
/// 2. [`GrokClientHandler`] when the server pushes a notification we
/// 2. [`KigiClientHandler`] when the server pushes a notification we
/// care about — currently `notifications/tools/list_changed` and
/// `notifications/resources/list_changed`.
/// 3. The session/managed-config layer when a server is added, removed,
/// or successfully (re-)initialized.
///
/// Consumers fan these out to ACP `x.ai/mcp/server_status` after 50 ms
/// Consumers fan these out to ACP `kigi/mcp/server_status` after 50 ms
/// of tumbling-window coalescing keyed by `(server, kind)`; see the
/// session-actor `StatusDispatcher`.
#[derive(Debug, Clone)]
@@ -2527,7 +2527,7 @@ pub struct McpClient {
///
/// The slot is `Some` after [`Self::set_event_tx`] is called and
/// `None` otherwise. The `Arc<Mutex<...>>` is **shared with
/// [`GrokClientHandler`]** constructed by
/// [`KigiClientHandler`]** constructed by
/// [`Self::make_client_handler`]: the handler holds a clone of
/// the same Arc and reads through it on every notification.
/// Snapshotting the slot at handshake time instead would mean any
@@ -2569,7 +2569,7 @@ pub struct McpClient {
}
/// Shared sender slot type — the same Arc lives on the [`McpClient`]
/// and the [`GrokClientHandler`] it constructs during
/// and the [`KigiClientHandler`] it constructs during
/// [`McpClient::try_handshake`]. Mutating the slot via
/// [`McpClient::set_event_tx`] is observed by the live rmcp service
/// loop on the next notification, so there's no "snapshot at
@@ -3026,7 +3026,7 @@ impl McpClient {
}
/// Build a client for an in-process SDK MCP server reached over the ACP reverse
/// channel. `server_id` is the id the agent echoes back in `x.ai/mcp/sdk_call`; the
/// channel. `server_id` is the id the agent echoes back in `kigi/mcp/sdk_call`; the
/// `invoker` performs the reverse request. Same downstream path as HTTP/stdio.
pub fn new_acp(
server_name: String,
@@ -3350,7 +3350,7 @@ impl McpClient {
async fn try_handshake(
&self,
pending: PendingTransport,
) -> Result<rmcp::service::RunningService<RoleClient, GrokClientHandler>, McpError> {
) -> Result<rmcp::service::RunningService<RoleClient, KigiClientHandler>, McpError> {
let timeout = std::time::Duration::from_secs(self.startup_timeout_sec);
let name = &self.server_name;
@@ -3431,7 +3431,7 @@ impl McpClient {
})
}
PendingTransport::Acp { server_id, invoker } => {
// Per-reverse-call backstop on `x.ai/mcp/sdk_call`: the larger of the
// Per-reverse-call backstop on `kigi/mcp/sdk_call`: the larger of the
// startup and tool timeouts, so it never undercuts the real outer bound
// (the handshake `initialize` is bounded by the serve `timeout` below;
// tool calls by `tool_timeout_for` in `try_call_tool`). The bridge
@@ -3468,7 +3468,7 @@ impl McpClient {
ClientInfo::new(
capabilities,
Implementation::new(
format!("grok-shell-{server_name}"),
format!("kigi-shell-{server_name}"),
kigi_version::VERSION.to_string(),
),
)
@@ -3478,14 +3478,14 @@ impl McpClient {
.with_protocol_version(rmcp::model::ProtocolVersion::V_2025_06_18)
}
/// Build the [`GrokClientHandler`] that drives `client.serve(...)`.
/// Build the [`KigiClientHandler`] that drives `client.serve(...)`.
///
/// The handler holds a **clone of `Arc<Mutex<Option<Sender>>>`**,
/// not a snapshot — so any subsequent call to
/// [`Self::set_event_tx`] is observed by the live rmcp service
/// loop on its next notification.
fn make_client_handler(&self) -> GrokClientHandler {
GrokClientHandler {
fn make_client_handler(&self) -> KigiClientHandler {
KigiClientHandler {
info: Self::make_client_info(&self.server_name),
server_name: self.server_name.clone(),
notify_tx: Arc::clone(&self.notify_tx),
@@ -3495,7 +3495,7 @@ impl McpClient {
/// Wire a sender for [`McpClientEvent`]s emitted by this client.
///
/// Mutates the shared slot synchronously. All previously-cloned
/// references (the [`GrokClientHandler`] handed to
/// references (the [`KigiClientHandler`] handed to
/// `client.serve`, the [`crate::liveness::spawn_transport_liveness`]
/// task) read through the same Arc, so this is observed
/// session-wide on the next event.
@@ -4027,7 +4027,7 @@ fn ensure_figma_user_agent(headers: &mut reqwest::header::HeaderMap, server_name
}
headers.insert(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static("grok-cli"),
reqwest::header::HeaderValue::from_static("kigi-cli"),
);
}
@@ -4297,7 +4297,7 @@ impl McpClient {
/// Plumbs server-pushed notifications through an
/// [`tokio::sync::mpsc::UnboundedSender<McpClientEvent>`] so the
/// session-actor dispatcher can fan them out as ACP
/// `x.ai/mcp/server_status` events.
/// `kigi/mcp/server_status` events.
///
/// ## RPIT, not `#[async_trait]`
///
@@ -4322,7 +4322,7 @@ impl McpClient {
/// send fails silently; rmcp must not see an error from a
/// notification handler or the service loop tears down.
#[derive(Debug)]
pub struct GrokClientHandler {
pub struct KigiClientHandler {
/// Static `ClientInfo` returned by [`Self::get_info`]; built once
/// at handshake time and stored to avoid re-allocating per call.
info: ClientInfo,
@@ -4337,7 +4337,7 @@ pub struct GrokClientHandler {
notify_tx: SharedEventTx,
}
impl GrokClientHandler {
impl KigiClientHandler {
/// Best-effort event emit. Reads the shared `notify_tx` slot on
/// every call (so the handler picks up any post-handshake wiring
/// done by [`McpClient::set_event_tx`]). Drops the send error: if
@@ -4352,13 +4352,13 @@ impl GrokClientHandler {
}
}
impl ClientHandler for GrokClientHandler {
impl ClientHandler for KigiClientHandler {
// NOTE: `async fn` here is sugar for the trait's
// `-> impl Future<Output = ()> + Send + '_`. We INTENTIONALLY do
// not use `#[async_trait]` — rmcp 2.1's `ClientHandler` declares
// its notification methods as return-position `impl Future`, and
// async_trait would produce a different (incompatible) signature.
// See the [`GrokClientHandler`] doc-comment for the full RPIT
// See the [`KigiClientHandler`] doc-comment for the full RPIT
// contract.
async fn on_tool_list_changed(&self, _context: NotificationContext<RoleClient>) {
self.emit(McpClientEvent::ToolsChanged {
@@ -4510,19 +4510,19 @@ mod tests {
}
#[test]
fn ensure_figma_user_agent_sets_grok_cli_when_missing() {
fn ensure_figma_user_agent_sets_kigi_cli_when_missing() {
let mut headers = reqwest::header::HeaderMap::new();
ensure_figma_user_agent(&mut headers, "figma", "https://mcp.figma.com/mcp");
assert_eq!(
headers.get(reqwest::header::USER_AGENT).unwrap(),
"grok-cli"
"kigi-cli"
);
let mut host_only = reqwest::header::HeaderMap::new();
ensure_figma_user_agent(&mut host_only, "other", "https://mcp.figma.com/mcp");
assert_eq!(
host_only.get(reqwest::header::USER_AGENT).unwrap(),
"grok-cli"
"kigi-cli"
);
}
@@ -6671,7 +6671,7 @@ mod tests {
}
}
});
let handler = GrokClientHandler {
let handler = KigiClientHandler {
info: McpClient::make_client_info("dead"),
server_name: "dead".to_string(),
notify_tx: Arc::new(parking_lot::Mutex::new(None)),
@@ -7395,7 +7395,7 @@ mod tests {
);
}
// -- GrokClientHandler --------------------------------------
// -- KigiClientHandler --------------------------------------
//
// The handler's notification routing is the only behavior worth
// unit-testing here; `get_info` is a literal `info.clone()` and
@@ -7409,7 +7409,7 @@ mod tests {
#[tokio::test]
async fn client_handler_routes_tools_changed() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<McpClientEvent>();
let handler = GrokClientHandler {
let handler = KigiClientHandler {
info: McpClient::make_client_info("test"),
server_name: "test".to_string(),
notify_tx: Arc::new(parking_lot::Mutex::new(Some(tx))),
@@ -7429,7 +7429,7 @@ mod tests {
/// must not panic.
#[tokio::test]
async fn client_handler_no_dispatcher_is_silent() {
let handler = GrokClientHandler {
let handler = KigiClientHandler {
info: McpClient::make_client_info("test"),
server_name: "test".to_string(),
notify_tx: Arc::new(parking_lot::Mutex::new(None)),
@@ -7444,7 +7444,7 @@ mod tests {
#[tokio::test]
async fn client_handler_get_info_round_trips() {
let info = McpClient::make_client_info("test-srv");
let handler = GrokClientHandler {
let handler = KigiClientHandler {
info: info.clone(),
server_name: "test-srv".to_string(),
notify_tx: Arc::new(parking_lot::Mutex::new(None)),
+9 -9
View File
@@ -1,27 +1,27 @@
//! Single source of truth for the `x.ai/mcp/*` ACP wire strings.
//! Single source of truth for the `kigi/mcp/*` ACP wire strings.
//!
//! These method/`_meta` keys are part of the cross-language MCP-over-ACP
//! protocol the SDK speaks (mirrors the SDK's `_mcp_wire.py` / `mcpWire.ts`).
//! Reference these constants instead of re-typing the literals so the agent and
//! SDK can't drift apart.
/// Forward tool-invocation method (client -> agent): `x.ai/mcp/call`.
/// Forward tool-invocation method (client -> agent): `kigi/mcp/call`.
///
/// The pager/client asks the agent to invoke an MCP tool on a server the agent is
/// connected to, outside the LLM loop. See `extensions::mcp::handle_call`.
pub const MCP_CALL: &str = "x.ai/mcp/call";
pub const MCP_CALL: &str = "kigi/mcp/call";
/// Reverse zero-IPC tool-invocation method (agent -> client): `x.ai/mcp/sdk_call`.
/// Reverse zero-IPC tool-invocation method (agent -> client): `kigi/mcp/sdk_call`.
///
/// The agent invokes a tool that lives in the SDK's in-process MCP server by sending
/// the MCP JSON-RPC message back to the client over the ACP reverse channel. Distinct
/// from [`MCP_CALL`] so the two disjoint schemas don't share a method string for
/// metrics/tracing. See the agent-side ACP invoker that handles this method.
pub const MCP_SDK_CALL: &str = "x.ai/mcp/sdk_call";
pub const MCP_SDK_CALL: &str = "kigi/mcp/sdk_call";
/// `session/new` `_meta` key listing in-process SDK MCP servers: `x.ai/mcp/servers`.
pub const MCP_SERVERS: &str = "x.ai/mcp/servers";
/// `session/new` `_meta` key listing in-process SDK MCP servers: `kigi/mcp/servers`.
pub const MCP_SERVERS: &str = "kigi/mcp/servers";
/// `initialize` `_meta` capability flag advertising in-process SDK MCP support
/// (enables the SDK's `transport="acp"`): `x.ai/mcp/sdk`.
pub const MCP_SDK: &str = "x.ai/mcp/sdk";
/// (enables the SDK's `transport="acp"`): `kigi/mcp/sdk`.
pub const MCP_SDK: &str = "kigi/mcp/sdk";
+1 -1
View File
@@ -139,7 +139,7 @@ impl EmbeddingProvider for ApiEmbeddingProvider {
let request = kigi_http::shared_client()
.post(format!("{}/embeddings", self.api_base))
.json(&body_json)
.header("x-grok-client-version", kigi_version::VERSION);
.header("x-kigi-client-version", kigi_version::VERSION);
let req = match request.build() {
Ok(r) => r,
+10 -10
View File
@@ -472,7 +472,7 @@ impl MemoryIndex {
///
/// An empty string means no claim is active. A non-empty claim means
/// a session currently owns the reindex lock (or a crashed session left
/// a stale one). Used by `grok memory doctor` to detect stuck states.
/// a stale one). Used by `kigi memory doctor` to detect stuck states.
pub fn get_reindex_claim(&self) -> String {
self.db
.query_row(
@@ -485,7 +485,7 @@ impl MemoryIndex {
/// Return all distinct file paths that have at least one indexed chunk.
///
/// Used by `grok memory doctor` to detect orphaned chunks (chunks whose
/// Used by `kigi memory doctor` to detect orphaned chunks (chunks whose
/// source file has since been deleted).
pub fn all_indexed_paths(&self) -> Result<Vec<String>, rusqlite::Error> {
let mut stmt = self
@@ -1179,13 +1179,13 @@ mod tests {
///
/// 1. Index a file.
/// 2. Delete the file from disk (simulates a user removing a session log).
/// 3. Run the same orphan-removal logic as `grok memory reindex`:
/// 3. Run the same orphan-removal logic as `kigi memory reindex`:
/// compare `all_indexed_paths()` against current files and call
/// `delete_path()` for paths that no longer exist.
/// 4. Verify the stale chunks are gone and are no longer searchable.
///
/// This proves that `grok memory reindex`'s Phase 1 actually fixes the
/// state that `grok memory doctor` warns about.
/// This proves that `kigi memory reindex`'s Phase 1 actually fixes the
/// state that `kigi memory doctor` warns about.
#[test]
fn test_reindex_maintenance_removes_orphaned_chunks() {
let tmp = TempDir::new().unwrap();
@@ -1206,7 +1206,7 @@ mod tests {
// Delete the file — now it is orphaned in the index.
std::fs::remove_file(&file).unwrap();
// Simulate `grok memory reindex` Phase 1: compare indexed vs current.
// Simulate `kigi memory reindex` Phase 1: compare indexed vs current.
let current: std::collections::BTreeSet<String> = vec![].into_iter().collect(); // empty = no files
let indexed = idx.all_indexed_paths().unwrap();
for path in &indexed {
@@ -1225,7 +1225,7 @@ mod tests {
/// A fresh (non-stale) reindex claim blocks `try_claim_reindex`.
///
/// Verifies that `grok memory reindex` Phase 0 correctly bails when a
/// Verifies that `kigi memory reindex` Phase 0 correctly bails when a
/// live session holds a fresh claim — i.e., the CLI cannot steal a live
/// session's lock and then mutate the index concurrently.
#[test]
@@ -1256,10 +1256,10 @@ mod tests {
idx.release_claim();
}
/// `grok memory reindex` Phase 3 resets the stale reindex claim.
/// `kigi memory reindex` Phase 3 resets the stale reindex claim.
///
/// Verifies that `release_claim()` clears `meta.reindex_claim` so that
/// `grok memory doctor` no longer reports a stale lock after reindex runs.
/// `kigi memory doctor` no longer reports a stale lock after reindex runs.
#[test]
fn test_reindex_maintenance_resets_stale_claim() {
let tmp = TempDir::new().unwrap();
@@ -1274,7 +1274,7 @@ mod tests {
"claim must be set before Phase 3"
);
// Simulate `grok memory reindex` Phase 3: release the claim.
// Simulate `kigi memory reindex` Phase 3: release the claim.
idx.release_claim();
assert_eq!(
+1 -1
View File
@@ -1,7 +1,7 @@
//! Memory system for cross-session knowledge persistence.
//!
//! This crate provides a markdown-based memory storage layer that allows
//! Grok to persist important information across sessions. Memory files are
//! Kigi to persist important information across sessions. Memory files are
//! stored under `~/.kigi/memory/` with workspace-scoped subdirectories
//! keyed by a blake3 hash of the workspace path.
//!
+1 -1
View File
@@ -1076,7 +1076,7 @@ mod tests {
/// newline. Kept in sync with that source.
const GLOBAL_STUB: &str = "# Global Memory\n\
\n\
> This file is automatically managed by Grok's memory system.\n\
> This file is automatically managed by Kigi's memory system.\n\
> You can also edit it manually changes will be indexed on next session.\n\
\n\
## Preferences\n\
+1 -1
View File
@@ -366,7 +366,7 @@ impl MemoryStorage {
&global_file,
"# Global Memory\n\
\n\
> This file is automatically managed by Grok's memory system.\n\
> This file is automatically managed by Kigi's memory system.\n\
> You can also edit it manually changes will be indexed on next session.\n\
\n\
## Preferences\n\
@@ -1,7 +1,7 @@
Roboto-Regular.ttf
Copyright 2011 Google Inc. All Rights Reserved.
This font is bundled (via include_bytes!) into the distributed Grok CLI binary
This font is bundled (via include_bytes!) into the distributed Kigi CLI binary
and is redistributed under the Apache License, Version 2.0 (below). It is used as
a deterministic fallback face so diagram text metrics do not depend on system
fonts. Retain this notice alongside the font when redistributing.
+2 -2
View File
@@ -64,10 +64,10 @@ use std::sync::Arc;
/// relevant to diagram rendering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MermaidTheme {
/// Light surfaces with dark text (e.g. `GrokDay`).
/// Light surfaces with dark text (e.g. `KigiDay`).
#[default]
Light,
/// Dark surfaces with light text (e.g. `GrokNight`, `TokyoNight`).
/// Dark surfaces with light text (e.g. `KigiNight`, `TokyoNight`).
Dark,
}
+2 -2
View File
@@ -608,7 +608,7 @@ mod tests {
// Aux models fall back to the default (no dedicated entries).
assert_eq!(default_image_description_model(), "kimi-for-coding");
assert_eq!(default_session_summary_model(), "kimi-for-coding");
// No grok remnants in the embedded fallback.
assert!(!DEFAULT_MODELS_JSON.contains("grok"));
// No kigi remnants in the embedded fallback.
assert!(!DEFAULT_MODELS_JSON.contains("kigi"));
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ kigi-tui = { path = "../kigi-tui" }
# Rendering primitives (must match the pager's versions).
ratatui = { workspace = true, features = ["crossterm", "unstable-widget-ref"] }
crossterm = { workspace = true, features = ["event-stream", "bracketed-paste"] }
# Transcript temp-file names (`grok-transcript-<uuid>.ansi`).
# Transcript temp-file names (`kigi-transcript-<uuid>.ansi`).
uuid = { workspace = true, features = ["v4"] }
tracing = { workspace = true }
@@ -133,7 +133,7 @@ fn finish_transcript(app: &mut AppView, id: kigi_tui::app::agent::AgentId, out:
}
return;
}
let path = std::env::temp_dir().join(format!("grok-transcript-{}.ansi", uuid::Uuid::new_v4()));
let path = std::env::temp_dir().join(format!("kigi-transcript-{}.ansi", uuid::Uuid::new_v4()));
match std::fs::write(&path, out) {
Ok(()) => {
app.pending_pager_path = Some(path);
+1 -1
View File
@@ -1,4 +1,4 @@
//! Minimal (scrollback-native) render mode — `grok --minimal`.
//! Minimal (scrollback-native) render mode — `kigi --minimal`.
//!
//! In this mode finalized conversation blocks are printed once into the
//! terminal's *native* scrollback (via `kigi_ratatui_inline::Terminal::insert_before`,
@@ -72,7 +72,7 @@ pub fn maybe_commit_welcome(app: &mut AppView, terminal: &mut PagerTerminal) {
let mut info: Vec<Line<'static>> = Vec::new();
info.push(Line::from(vec![
Span::styled(
"Grok Build",
"Kigi",
Style::default()
.fg(theme.accent_user)
.add_modifier(Modifier::BOLD),
@@ -26,7 +26,7 @@
//!
//! # Compare an old release artifact, text mode only, JSON to a file:
//! cargo bench -p kigi-pager-pty-harness --bench paste_latency -- \
//! --binary ~/Downloads/grok-old --mode text --json /tmp/paste-old.json
//! --binary ~/Downloads/kigi-old --mode text --json /tmp/paste-old.json
//! ```
use std::path::{Path, PathBuf};
@@ -21,7 +21,7 @@
//!
//! Run every scenario in CI and fail on >15% p99 regression:
//! ```bash
//! PAGER_BINARY=./artifacts/grok-${VERSION}-linux-x86_64 \
//! PAGER_BINARY=./artifacts/kigi-${VERSION}-linux-x86_64 \
//! cargo bench -p kigi-pager-pty-harness \
//! --bench pty_bench -- --all \
//! --baseline benches/pty_baselines/linux-x86_64.json
@@ -20,7 +20,7 @@ struct Cli {
#[arg(long, value_name = "PATH")]
scenario: PathBuf,
/// Pager binary. Defaults to PAGER_BINARY, CARGO_BIN_EXE_kigi-tui,
/// Pager binary. Defaults to PAGER_BINARY, CARGO_BIN_EXE_kigi,
/// or a locally-built debug binary.
#[arg(long, value_name = "PATH")]
binary: Option<PathBuf>,
@@ -53,7 +53,7 @@ struct Cli {
#[arg(long, value_name = "DIR", default_value = "target/scroll-matrix")]
artifacts: PathBuf,
/// Pager binary. Defaults to PAGER_BINARY, CARGO_BIN_EXE_kigi-tui,
/// Pager binary. Defaults to PAGER_BINARY, CARGO_BIN_EXE_kigi,
/// or a locally-built debug binary.
#[arg(long, value_name = "PATH")]
binary: Option<PathBuf>,

Some files were not shown because too many files have changed in this diff Show More