§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
@@ -23,7 +23,7 @@ const DATA_FILENAME: &str = "active_sessions.json";
const LOCK_FILENAME: &str = "active_sessions.lock";
const TMP_FILENAME: &str = "active_sessions.json.tmp";
// -- Public API (delegates to `_in` variants with default grok home) --------
// -- Public API (delegates to `_in` variants with default kigi home) --------
/// Register a session as active (idempotent by session_id).
pub fn register(session: ActiveSession) -> io::Result<()> {
@@ -3,7 +3,7 @@
//! that cannot read the `!Send` `MvpAgent` state on the `LocalSet`).
//!
//! The leader's `agent_busy` flag only counts IPC (Unix-socket) requests;
//! relay (grok.com WebSocket) traffic is bridged straight into the agent's
//! relay (kimi.com WebSocket) traffic is bridged straight into the agent's
//! ACP stdin and never sets it, so a relay-driven leader (devbox / remote)
//! always looked idle and got restarted mid-turn on every update —
//! surfacing as "Subagent result channel dropped".
+18 -18
View File
@@ -83,7 +83,7 @@ const MAX_AUTO_UPDATE_BUSY_DEFERRALS: u32 = 24;
///
/// Idle means BOTH `agent_busy` is false (no IPC client request in flight)
/// AND `activity.is_busy()` is false (no running turn, parked interaction,
/// or live subagent). The second signal covers relay-driven (grok.com
/// or live subagent). The second signal covers relay-driven (kimi.com
/// WebSocket) leaders, whose traffic bypasses the IPC server and never sets
/// `agent_busy`.
///
@@ -217,10 +217,10 @@ fn spawn_agent_local(
}
/// Build a newline-terminated JSON-RPC request line for an internal
/// `x.ai/...` extension method, for injection into the agent's inbound ACP
/// `kigi/...` extension method, for injection into the agent's inbound ACP
/// stream by the leader's own watcher tasks (config hot-reload, skills).
///
/// The wire method is written **`_`-prefixed** (`_x.ai/internal/...`):
/// The wire method is written **`_`-prefixed** (`_kigi/internal/...`):
/// `agent-client-protocol`'s inbound decoder routes a non-built-in method to
/// `ext_method` only when it carries the `_` extension prefix and rejects
/// bare custom methods with `-32601 method_not_found`. These injections were
@@ -239,7 +239,7 @@ fn internal_reload_request_line(id: &str, method: &str, params: serde_json::Valu
format!("{}\n", msg)
}
/// Start a skills file watcher and wire it to inject `x.ai/internal/reload_skills`
/// Start a skills file watcher and wire it to inject `kigi/internal/reload_skills`
/// messages into the shared ACP incoming stream when SKILL.md files change on disk.
///
/// Returns the watcher guard (must be kept alive for the lifetime of the session)
@@ -264,7 +264,7 @@ where
info!("Skill directory changed on disk, reloading skills for all sessions");
let line = internal_reload_request_line(
"skills-reload",
"x.ai/internal/reload_skills",
"kigi/internal/reload_skills",
serde_json::json!({}),
);
let mut tx = skills_tx.lock().await;
@@ -297,7 +297,7 @@ pub async fn run_stdio_agent(
// are identifiable by version in diagnostic logs.
kigi_log::unified_log::set_version(kigi_version::VERSION);
// Log the client that launched us (set by grok-desktop when spawning `grok agent stdio`).
// Log the client that launched us (set by kigi-desktop when spawning `kigi agent stdio`).
// This appears early in unified.jsonl and is extremely useful for auth diagnostics.
if let Ok(version) = std::env::var("KIGI_CLIENT_VERSION") {
crate::unified_log::info(
@@ -369,8 +369,8 @@ pub async fn run_stdio_agent(
auth_manager.start_proactive_refresh(tokio_util::sync::CancellationToken::new());
// Pause refreshes across system sleep so an OIDC refresh can't straddle a
// suspend (which can revoke the refresh token and force re-login).
// `grok agent stdio` is a local/interactive entrypoint (spawned by
// grok-desktop), so it needs the gate like the leader and pager paths;
// `kigi agent stdio` is a local/interactive entrypoint (spawned by
// kigi-desktop), so it needs the gate like the leader and pager paths;
// no-op where the OS listener is unavailable.
auth_manager.start_system_power_listener();
@@ -907,7 +907,7 @@ pub async fn run_leader(
models_manager_for_config.on_auth_changed().await;
let line = internal_reload_request_line(
"config-auth-reloaded",
"x.ai/internal/reload_all_mcp_servers",
"kigi/internal/reload_all_mcp_servers",
serde_json::json!({}),
);
let mut tx = acp_tx_for_config.lock().await;
@@ -929,7 +929,7 @@ pub async fn run_leader(
info!("MCP server config change detected — reloading active sessions");
let line = internal_reload_request_line(
"config-reload-mcp",
"x.ai/internal/reload_all_mcp_servers",
"kigi/internal/reload_all_mcp_servers",
serde_json::json!({}),
);
let mut tx = acp_tx_for_config.lock().await;
@@ -952,7 +952,7 @@ pub async fn run_leader(
);
let line = internal_reload_request_line(
"config-reload-project-mcp",
"x.ai/internal/reload_project_mcp_servers",
"kigi/internal/reload_project_mcp_servers",
serde_json::json!({ "cwd": cwd.to_string_lossy() }),
);
let mut tx = acp_tx_for_config.lock().await;
@@ -967,7 +967,7 @@ pub async fn run_leader(
info!("Model config change detected — reloading agent model list");
let line = internal_reload_request_line(
"config-reload-models",
"x.ai/internal/reload_models",
"kigi/internal/reload_models",
serde_json::json!({}),
);
let mut tx = acp_tx_for_config.lock().await;
@@ -977,7 +977,7 @@ pub async fn run_leader(
}
ConfigUpdate::ModelsCacheChanged => {
// External write to ~/.kigi/models_cache.json
// (another grok process fetched a fresher /v1/models
// (another kigi process fetched a fresher /v1/models
// catalog). Injected into the agent's ACP stream —
// NOT applied directly on the manager — so it is
// serialized behind any `reload_models` from the
@@ -993,7 +993,7 @@ pub async fn run_leader(
info!("Models cache change detected — reloading agent model catalog");
let line = internal_reload_request_line(
"config-reload-models-cache",
"x.ai/internal/reload_models_cache",
"kigi/internal/reload_models_cache",
serde_json::json!({}),
);
let mut tx = acp_tx_for_config.lock().await;
@@ -1030,7 +1030,7 @@ pub async fn run_leader(
info!("UI config change detected by watcher");
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": "x.ai/config_changed",
"method": "kigi/config_changed",
"params": {
"section": "ui",
"changes": {
@@ -1114,13 +1114,13 @@ mod tests {
fn internal_reload_request_line_uses_wire_ext_prefix() {
let line = internal_reload_request_line(
"config-reload-models",
"x.ai/internal/reload_models",
"kigi/internal/reload_models",
serde_json::json!({}),
);
assert!(line.ends_with('\n'), "must be a newline-terminated line");
let msg: serde_json::Value = serde_json::from_str(line.trim_end()).unwrap();
assert_eq!(
msg["method"], "_x.ai/internal/reload_models",
msg["method"], "_kigi/internal/reload_models",
"wire method must carry the `_` ext prefix or the ACP decoder \
rejects it with method_not_found"
);
@@ -1130,7 +1130,7 @@ mod tests {
// Params must pass through verbatim (project-MCP reload carries cwd).
let line = internal_reload_request_line(
"config-reload-project-mcp",
"x.ai/internal/reload_project_mcp_servers",
"kigi/internal/reload_project_mcp_servers",
serde_json::json!({ "cwd": "/repo/x" }),
);
let msg: serde_json::Value = serde_json::from_str(line.trim_end()).unwrap();
@@ -101,7 +101,7 @@ pub struct BuiltAuthMethods {
/// Ordering (when each method is enabled):
/// 1. `xai.api_key` (if `has_external_api_key`)
/// 2. `cached_token` (if `has_cached_token`)
/// 3. `grok.com` (the Kimi Code device login)
/// 3. `kimi-code` (the Kimi Code device login)
///
/// `default_auth_method_id`:
/// - `cached_token` if `has_cached_token`
@@ -153,7 +153,7 @@ pub fn build_auth_methods(inputs: AuthMethodsBuildInputs<'_>) -> BuiltAuthMethod
pub enum AuthMethodKind {
XaiApiKey,
CachedToken,
GrokCom,
KimiCode,
Unknown,
}
@@ -162,7 +162,7 @@ impl AuthMethodKind {
match id.0.as_ref() {
XAI_API_KEY_METHOD_ID => Self::XaiApiKey,
CACHED_TOKEN_AUTH_METHOD_ID => Self::CachedToken,
KIGI_COM_METHOD_ID => Self::GrokCom,
KIMI_CODE_METHOD_ID => Self::KimiCode,
_ => Self::Unknown,
}
}
@@ -174,12 +174,12 @@ impl AuthMethodKind {
/// `true` for session-based methods (cached_token, interactive login).
pub fn is_session_based(self) -> bool {
matches!(self, Self::CachedToken | Self::GrokCom)
matches!(self, Self::CachedToken | Self::KimiCode)
}
/// Requires user interaction (device-code login in the browser).
pub fn needs_interactive_login(self) -> bool {
matches!(self, Self::GrokCom)
matches!(self, Self::KimiCode)
}
pub fn auth_error_message(self) -> &'static str {
@@ -259,7 +259,7 @@ pub fn method_id_after_cached_token_unavailable(has_external_api_key: bool) -> &
if has_external_api_key {
XAI_API_KEY_METHOD_ID
} else {
KIGI_COM_METHOD_ID
KIMI_CODE_METHOD_ID
}
}
@@ -287,17 +287,20 @@ pub fn cached_token_auth_method() -> acp::AuthMethod {
)
}
/// Interactive login method id. The literal `"grok.com"` is kept for ACP
/// wire-compat with the in-repo pager (renaming is a cross-crate wire change
/// deferred to the command-surface milestone).
pub const KIGI_COM_METHOD_ID: &str = "grok.com";
/// Interactive login method id, advertised over ACP by this agent and
/// selected by the in-repo pager. Both sides of the ACP boundary live in
/// this repo, so the id is renamed in lockstep everywhere.
pub const KIMI_CODE_METHOD_ID: &str = "kimi-code";
/// The Kimi Code device-code login.
pub fn kimi_code_auth_method(label: Option<&str>) -> acp::AuthMethod {
let name = label.unwrap_or("Kimi Code");
acp::AuthMethod::Agent(
acp::AuthMethodAgent::new(acp::AuthMethodId::new(KIGI_COM_METHOD_ID), name.to_string())
.description(Some(format!("Sign in with {name}"))),
acp::AuthMethodAgent::new(
acp::AuthMethodId::new(KIMI_CODE_METHOD_ID),
name.to_string(),
)
.description(Some(format!("Sign in with {name}"))),
)
}
@@ -323,14 +326,14 @@ mod tests {
fn after_cached_token_unavailable_falls_to_interactive_login() {
assert_eq!(
method_id_after_cached_token_unavailable(false),
KIGI_COM_METHOD_ID,
KIMI_CODE_METHOD_ID,
);
}
/// Classifier matrix for all auth method variants.
#[test]
fn auth_method_kind_classifier_matrix() {
let session_methods = [CACHED_TOKEN_AUTH_METHOD_ID, KIGI_COM_METHOD_ID];
let session_methods = [CACHED_TOKEN_AUTH_METHOD_ID, KIMI_CODE_METHOD_ID];
for id in session_methods {
let kind = AuthMethodKind::from_id(&acp::AuthMethodId::new(id));
assert!(kind.is_session_based(), "{id} must be session-based");
@@ -345,7 +348,7 @@ mod tests {
assert!(!unknown.is_session_based());
// Only the interactive login needs a browser.
assert!(
AuthMethodKind::from_id(&acp::AuthMethodId::new(KIGI_COM_METHOD_ID))
AuthMethodKind::from_id(&acp::AuthMethodId::new(KIMI_CODE_METHOD_ID))
.needs_interactive_login()
);
assert!(
@@ -428,7 +431,7 @@ mod tests {
});
assert_eq!(
method_ids(&built),
vec![XAI_API_KEY_METHOD_ID, KIGI_COM_METHOD_ID]
vec![XAI_API_KEY_METHOD_ID, KIMI_CODE_METHOD_ID]
);
assert_eq!(default_id(&built), Some(XAI_API_KEY_METHOD_ID));
assert!(
@@ -451,7 +454,7 @@ mod tests {
vec![
XAI_API_KEY_METHOD_ID,
CACHED_TOKEN_AUTH_METHOD_ID,
KIGI_COM_METHOD_ID
KIMI_CODE_METHOD_ID
]
);
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
@@ -466,7 +469,7 @@ mod tests {
});
assert_eq!(
method_ids(&built),
vec![CACHED_TOKEN_AUTH_METHOD_ID, KIGI_COM_METHOD_ID]
vec![CACHED_TOKEN_AUTH_METHOD_ID, KIMI_CODE_METHOD_ID]
);
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
assert_eq!(
@@ -480,7 +483,7 @@ mod tests {
#[test]
fn fresh_user_only_advertises_interactive_login() {
let built = build_auth_methods(default_inputs());
assert_eq!(method_ids(&built), vec![KIGI_COM_METHOD_ID]);
assert_eq!(method_ids(&built), vec![KIMI_CODE_METHOD_ID]);
assert_eq!(default_id(&built), None);
}
@@ -1,7 +1,7 @@
//! Legacy `--chat` gateway gate.
//!
//! The grok.com chat-product model picker (`/rest/modes`, `ChatModesManager`)
//! was removed with the xAI proxy: those "modes" came from a grok backend with
//! The kimi.com chat-product model picker (`/rest/modes`, `ChatModesManager`)
//! was removed with the xAI proxy: those "modes" came from a kigi backend with
//! no Kimi counterpart. Only the process-mode gate survives so the `--chat`
//! frontend path stays a compile-time-off no-op across crates without a
//! cross-crate churn to delete every reference.
@@ -10,7 +10,7 @@
pub const KIGI_CHAT_MODE_ENV: &str = "KIGI_CHAT_MODE";
/// True when the process is a gateway light-frontend (`--chat`) agent.
/// Hard-off: the grok chat-modes backend is gone, so this is always `false`.
/// Hard-off: the kigi chat-modes backend is gone, so this is always `false`.
pub fn process_chat_mode_enabled() -> bool {
false
}
+149 -140
View File
@@ -37,13 +37,11 @@ pub enum AgentMode {
Generic,
}
/// Default agent type when the server or user config doesn't specify one.
pub const DEFAULT_AGENT_TYPE: &str = "grok-build-plan";
pub const DEFAULT_AGENT_TYPE: &str = "kigi-plan";
/// Serde default for `ModelInfo.agent_type` and `ModelEntryConfig.agent_type`.
pub fn default_agent_type() -> String {
DEFAULT_AGENT_TYPE.to_owned()
}
/// Default base URL for the public xAI API.
pub const API_BASE_URL_DEFAULT: &str = "https://api.x.ai/v1";
/// One or more environment variable names that may hold a model API key.
///
/// Serde `untagged`: accepts a string or an array in TOML/JSON.
@@ -143,9 +141,14 @@ pub struct EndpointsConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub coding_api_base_url: Option<String>,
/// Base URL for direct (BYOK / external-API-key) API calls.
///
/// There is NO built-in default endpoint: a BYOK/custom endpoint must be
/// explicitly configured via `[endpoints] api_base_url` in config.toml,
/// the `KIGI_API_BASE_URL` env var, `--api-base-url`, or a managed
/// requirements pin. `None` = not configured.
/// Accepts the legacy `xai_api_base_url` config key.
#[serde(alias = "xai_api_base_url")]
pub api_base_url: String,
#[serde(alias = "xai_api_base_url", skip_serializing_if = "Option::is_none")]
pub api_base_url: Option<String>,
/// Optional extra access-header value (applied only with the optional
/// non-production feature, and only for matching first-party hosts).
#[serde(skip_serializing_if = "Option::is_none")]
@@ -225,7 +228,7 @@ impl EndpointsConfig {
pub fn resolve_feedback_base_url(&self) -> String {
blank_as_unset(&self.feedback_base_url).unwrap_or_else(|| self.proxy_url())
}
/// Managed deployment-config URL (`grok setup`): explicit `managed_config_url`,
/// Managed deployment-config URL (`kigi setup`): explicit `managed_config_url`,
/// else `proxy_url` + `/deployment/config`. Never `api_base_url`, so the
/// deployment key reaches the proxy, not the inference host.
pub fn resolve_managed_config_url(&self) -> String {
@@ -252,8 +255,7 @@ impl Default for EndpointsConfig {
fn default() -> Self {
Self {
coding_api_base_url: std::env::var("KIGI_CODE_BASE_URL").ok(),
api_base_url: std::env::var("KIGI_API_BASE_URL")
.unwrap_or_else(|_| API_BASE_URL_DEFAULT.to_owned()),
api_base_url: env_string("KIGI_API_BASE_URL"),
alpha_test_key: None,
models_base_url: env_string("KIGI_MODELS_BASE_URL"),
models_list_url: env_string("KIGI_MODELS_LIST_URL"),
@@ -1000,7 +1002,7 @@ pub struct Config {
/// `[model.*]` overrides from config.toml. Resolve via `resolve_model_list()`.
#[serde(skip)]
pub config_models: IndexMap<String, ConfigModelOverride>,
/// Warnings from `[model.*]` parsing; surfaced by `grok inspect`.
/// Warnings from `[model.*]` parsing; surfaced by `kigi inspect`.
#[serde(skip)]
pub model_override_warnings: Vec<super::config_model_override_parse::ModelOverrideWarning>,
pub kimi_code_config: KimiCodeConfig,
@@ -1076,7 +1078,7 @@ pub struct Config {
/// Typed as `KimiCodeConfig` (same schema) so sub-field typos are caught.
#[serde(default, skip_serializing)]
pub auth: Option<KimiCodeConfig>,
/// `[desktop]` section — owned by grok-desktop (Electron app), opaque to the CLI agent.
/// `[desktop]` section — owned by kigi-desktop (Electron app), opaque to the CLI agent.
#[serde(default, skip_serializing)]
pub desktop: Option<toml::Value>,
/// `[tips]` section — consumed by `merge_tips`.
@@ -1311,13 +1313,13 @@ pub use kigi_shared::ui_config::{ContextualHints, UiConfig};
/// 2. CLI `--agent-profile` flag
/// 3. `[agent]` config.toml section (this config)
/// 4. `KIGI_AGENT` env var
/// 5. Default `grok-build` agent
/// 5. Default `kigi` agent
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentSelectionConfig {
/// Name of a built-in or discovered agent definition.
/// Looked up via `kigi_agent::discovery::by_name_in_cwd()`.
/// Examples: "grok-build", "browser-use", or a custom agent name.
/// Examples: "kigi", "browser-use", or a custom agent name.
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Path to an agent definition file (.md with YAML frontmatter).
@@ -1718,7 +1720,7 @@ impl Config {
.default(false)
.resolve()
}
/// Server-side doom-loop check policy (the `x-grok-doom-loop-check`
/// Server-side doom-loop check policy (the `x-kigi-doom-loop-check`
/// header, trigger parsing, and confident-signal resampling, all
/// applied by the sampler). Merged
/// PER-FIELD across the `[doom_loop_recovery]` TOML table and the
@@ -2146,7 +2148,7 @@ impl Config {
resolve_mcp_auto_restart(None, None, self.features.mcp_auto_restart, None, None)
}
/// Resolve whether the pager subscribes to the per-server
/// `x.ai/mcp/server_status` push.
/// `kigi/mcp/server_status` push.
///
/// Thin delegate to the canonical
/// [`resolve_mcp_push_server_status`] free function — mirrors the
@@ -2251,7 +2253,7 @@ pub fn resolve_mcp_auto_restart(
/// the precedence is single-sourced.
///
/// The default is `true` — the pager's subscription to
/// `x.ai/mcp/server_status` is wired default-on, with this
/// `kigi/mcp/server_status` is wired default-on, with this
/// flag existing primarily as a kill switch.
pub fn resolve_mcp_push_server_status(
requirement: Option<bool>,
@@ -2347,7 +2349,7 @@ impl SyncBoolFlag {
self.disable_env = Some(name);
self
}
/// Either-direction env resolver (typically `GROK_*`). Returns
/// Either-direction env resolver (typically `KIGI_*`). Returns
/// `Some(enabled)` for an explicit signal, `None` to fall through.
pub const fn enable_env(mut self, resolver: fn() -> Option<bool>) -> Self {
self.enable_env = Some(resolver);
@@ -2830,7 +2832,7 @@ pub struct ModelEntryConfig {
pub id: Option<String>,
/// The routing slug sent in API requests.
pub model: String,
/// The base URL of the model. e.g. "https://api.x.ai/v1"
/// The base URL of the model. e.g. "https://byok.example/v1"
pub base_url: String,
/// Human-readable display name of the model.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -2899,7 +2901,7 @@ pub struct ModelEntryConfig {
#[serde(default, skip_serializing_if = "is_false")]
pub use_concise: bool,
/// The type of system prompt to use for this model.
/// e.g. "grok-build", "codex".
/// e.g. "kigi", "codex".
#[serde(default = "default_agent_type")]
pub agent_type: String,
/// Maximum seconds to wait between SSE chunks during inference streaming.
@@ -3143,7 +3145,7 @@ pub struct ModelInfo {
/// concise tool output, concise user message prefix, reduced toolset).
pub use_concise: bool,
/// The type of agent configuration to use for this model.
/// Always has a value; defaults to `"grok-build-plan"` when the server
/// Always has a value; defaults to `"kigi-plan"` when the server
/// or user config doesn't specify one.
#[serde(default = "default_agent_type")]
pub agent_type: String,
@@ -3523,9 +3525,9 @@ pub struct Features {
/// Default: true (index any git repo). Patterns can explicitly match non-git directories.
#[serde(default)]
pub codebase_indexing: CodebaseIndexingSetting,
/// Show a blocking warning when Grok starts outside a Git repository.
/// Show a blocking warning when Kigi starts outside a Git repository.
/// Default: false. Used as the local fallback when the `non_git_warning` remote settings
/// flag in `grok_build_settings` is absent. When the remote flag is present it takes
/// flag in `kigi_settings` is absent. When the remote flag is present it takes
/// precedence — `Some(false)` from remote settings overrides `true` here.
#[serde(default)]
pub non_git_warning: bool,
@@ -3600,7 +3602,7 @@ pub struct Features {
///
/// When `true` (default), each successfully-handshaken MCP
/// client gets a poller that detects rmcp service-loop
/// termination and pushes `x.ai/mcp/server_status` updates to
/// termination and pushes `kigi/mcp/server_status` updates to
/// the client. When `false`, neither watchers nor the
/// dispatcher are spawned — useful as an emergency kill switch
/// for the rollout. `None` = defer to env / default (true).
@@ -3622,13 +3624,13 @@ pub struct Features {
/// Resolved via [`Config::resolve_mcp_auto_restart`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp_auto_restart: Option<bool>,
/// Pager-side subscription to the `x.ai/mcp/server_status` push.
/// Pager-side subscription to the `kigi/mcp/server_status` push.
///
/// When `true` (default), the pager subscribes to the per-server
/// status delta the shell emits via the dispatcher and
/// patches the MCP servers modal in-place (no re-fetch round
/// trip). When `false`, the pager ignores the push and falls
/// back to the legacy `x.ai/mcp/tools_changed` debounced refetch
/// back to the legacy `kigi/mcp/tools_changed` debounced refetch
/// path. `None` = defer to env / default (true).
///
/// The pager-side gate
@@ -4169,7 +4171,7 @@ mod tests {
tools: Some(vec!["read_file".into()]),
..Default::default()
};
let mut cases = vec![(AgentDefinition::default_grok_build(), true)];
let mut cases = vec![(AgentDefinition::default_kigi(), true)];
for (mut definition, expected_injection) in cases {
overrides.apply_to_definition(&mut definition);
assert_eq!(definition.tools, vec!["read_file".to_string()]);
@@ -4185,13 +4187,13 @@ mod tests {
let toml_src = r#"
enabled = true
prompt_type = "no_user_tool_prefix"
classifier_model = "grok-4.5"
classifier_model = "kigi-4.5"
reasoning_effort = "low"
"#;
let from_toml: AutoModeConfig = toml::from_str(toml_src).unwrap();
let json = serde_json::json!(
{ "enabled" : true, "prompt_type" : "no_user_tool_prefix", "classifier_model"
: "grok-4.5", "reasoning_effort" : "low" }
: "kigi-4.5", "reasoning_effort" : "low" }
);
let from_json: AutoModeConfig = serde_json::from_value(json).unwrap();
for cfg in [&from_toml, &from_json] {
@@ -4200,7 +4202,7 @@ reasoning_effort = "low"
cfg.prompt_type,
Some(ClassifierPromptType::NoUserToolPrefix)
);
assert_eq!(cfg.classifier_model.as_deref(), Some("grok-4.5"));
assert_eq!(cfg.classifier_model.as_deref(), Some("kigi-4.5"));
assert_eq!(cfg.reasoning_effort, Some(ReasoningEffort::Low));
}
let empty: AutoModeConfig = toml::from_str("").unwrap();
@@ -4450,7 +4452,7 @@ reasoning_effort = "low"
let (model, cfg) = finalize_image_describe_sampler_config(None, &active, Some(3));
assert_eq!(model, "composer-session-model");
assert_eq!(cfg.model, "composer-session-model");
assert_ne!(cfg.model, "grok-build");
assert_ne!(cfg.model, "kigi");
}
#[test]
fn finalize_image_describe_sampler_some_stamps_session_fields() {
@@ -4459,20 +4461,20 @@ reasoning_effort = "low"
..Default::default()
};
let aux = SamplerConfig {
model: "grok-build".into(),
model: "kigi".into(),
..Default::default()
};
let (model, cfg) = finalize_image_describe_sampler_config(Some(aux), &active, Some(7));
assert_eq!(model, "grok-build");
assert_eq!(cfg.model, "grok-build");
assert_eq!(model, "kigi");
assert_eq!(cfg.model, "kigi");
assert_eq!(cfg.max_retries, Some(7));
}
#[test]
fn resolve_aux_model_honors_grok_build_override() {
fn resolve_aux_model_honors_kigi_override() {
let endpoints = EndpointsConfig::default();
let mut catalog = IndexMap::new();
catalog.insert(
"grok-build".to_string(),
"kigi".to_string(),
test_model_entry(
"v9m-rl-learnability-tp8",
"https://vendor.example/v1",
@@ -4481,9 +4483,8 @@ reasoning_effort = "low"
None,
),
);
let resolved =
resolve_aux_model_sampling_config("grok-build", &catalog, &endpoints, None, None)
.expect("override entry has an API key, so resolution succeeds");
let resolved = resolve_aux_model_sampling_config("kigi", &catalog, &endpoints, None, None)
.expect("override entry has an API key, so resolution succeeds");
assert_eq!(resolved.model, "v9m-rl-learnability-tp8");
assert_eq!(resolved.base_url, "https://vendor.example/v1");
assert_eq!(resolved.api_key.as_deref(), Some("vendor-key"));
@@ -4493,7 +4494,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-custom-model]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
context_window = 200000
api_key = "sk-test-key-12345"
@@ -4503,7 +4504,7 @@ reasoning_effort = "low"
let cfg = Config::new_from_toml_cfg(&raw_config).expect("config should parse");
let resolved = resolve_model_list(&cfg, None);
let model = resolved.get("my-custom-model").expect("model should exist");
assert_eq!(model.info.model, "grok-4.5");
assert_eq!(model.info.model, "kigi-4.5");
assert_eq!(model.info.base_url, "https://api.example.com/v1");
assert_eq!(model.api_key, Some("sk-test-key-12345".to_string()));
}
@@ -4633,8 +4634,9 @@ reasoning_effort = "low"
auth_scheme: AuthScheme::Bearer,
};
assert_eq!(
api_key_creds.base_url, endpoints.api_base_url,
"{model_id}: ExternalApiKey must route to api.x.ai"
Some(api_key_creds.base_url.as_str()),
endpoints.api_base_url.as_deref(),
"{model_id}: ExternalApiKey must route to the configured BYOK endpoint"
);
}
}
@@ -4874,7 +4876,7 @@ reasoning_effort = "low"
#[test]
fn proxy_messages_models_use_bearer_auth_scheme() {
let mut model = test_model_entry(
"grok-4.5",
"kigi-4.5",
kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url,
None,
None,
@@ -4937,7 +4939,7 @@ reasoning_effort = "low"
#[test]
fn auth_scheme_defaults_to_bearer_when_not_set_in_config() {
let model = test_model_entry(
"grok-4.5",
"kigi-4.5",
"https://api.example.com/v1",
Some("sk-openai-test"),
None,
@@ -4994,7 +4996,7 @@ reasoning_effort = "low"
byok_from_lookup(&ModelLookup::Loaded(Some(&byok))),
ModelByok::Byok,
);
let session = test_model_entry("m", "https://api.x.ai/v1", None, None, None);
let session = test_model_entry("m", "https://byok.example/v1", None, None, None);
assert_eq!(
byok_from_lookup(&ModelLookup::Loaded(Some(&session))),
ModelByok::NotByok,
@@ -5257,10 +5259,10 @@ reasoning_effort = "low"
}
#[test]
fn sampling_config_context_window_from_entry_or_default() {
let model = test_model_entry("any-model", "https://api.x.ai/v1", None, None, None);
let model = test_model_entry("any-model", "https://byok.example/v1", None, None, None);
let config = sampling_config_for_model(&model, resolve_credentials(&model, None), None);
assert_eq!(config.context_window, 200_000);
let mut model = test_model_entry("any-model", "https://api.x.ai/v1", None, None, None);
let mut model = test_model_entry("any-model", "https://byok.example/v1", None, None, None);
model.info.context_window = NonZeroU64::new(256_000).unwrap();
let config = sampling_config_for_model(&model, resolve_credentials(&model, None), None);
assert_eq!(config.context_window, 256_000);
@@ -5270,7 +5272,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-responses-model]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
context_window = 200000
api_backend = "responses"
@@ -5289,7 +5291,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-chat-model]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
context_window = 200000
api_backend = "chat_completions"
@@ -5309,7 +5311,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-claude]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://messages.example.com"
context_window = 200000
api_backend = "messages"
@@ -5331,7 +5333,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-claude]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://messages.example.com"
context_window = 200000
api_backend = "messages"
@@ -5354,7 +5356,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-openai]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
context_window = 200000
api_backend = "chat_completions"
@@ -5374,7 +5376,7 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-model]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
context_window = 200000
"#,
@@ -5574,7 +5576,7 @@ reasoning_effort = "low"
assert_eq!(model.info.agent_type, "codex");
}
#[test]
fn model_agent_type_defaults_to_grok_build() {
fn model_agent_type_defaults_to_kigi() {
let raw_config: toml::Value = toml::from_str(
r#"
[model.my-model]
@@ -5834,12 +5836,12 @@ reasoning_effort = "low"
r#"
[model.visible-model]
model = "visible-model"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
[model.hidden-model]
model = "hidden-model"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
hidden = true
"#,
@@ -5874,7 +5876,7 @@ reasoning_effort = "low"
disabled_models = ["to-disable"]
[model.to-disable]
model = "to-disable"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
"#,
)
@@ -5891,7 +5893,7 @@ reasoning_effort = "low"
hidden_models = ["to-hide"]
[model.to-hide]
model = "to-hide"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
"#,
)
@@ -5911,15 +5913,15 @@ reasoning_effort = "low"
allowed_models = ["keep-*", "explicit-key", "explicit-model-id"]
[model.to-drop]
model = "to-drop"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
[model.keep-one]
model = "keep-one"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
[model.explicit-key]
model = "explicit-model-id"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
"#,
)
@@ -5944,7 +5946,7 @@ reasoning_effort = "low"
allowed_models = []
[model.foo]
model = "foo"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
"#,
)
@@ -5958,11 +5960,11 @@ reasoning_effort = "low"
#[test]
fn invalid_glob_is_rejected_by_validation() {
use crate::agent::models::ModelGlobSet;
assert!(ModelGlobSet::compile(Some(&vec!["grok[".to_string()])).is_err());
assert!(ModelGlobSet::compile(Some(&vec!["kigi[".to_string()])).is_err());
let raw: toml::Value = toml::from_str(
r#"
[models]
allowed_models = ["grok["]
allowed_models = ["kigi["]
"#,
)
.unwrap();
@@ -5982,13 +5984,13 @@ reasoning_effort = "low"
r#"
[model.oauth-only-model]
model = "oauth-only-model"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
supported_in_api = false
[model.public-model]
model = "public-model"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 200000
"#,
)
@@ -6013,8 +6015,8 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.slow-model]
model = "grok-4.5"
base_url = "https://api.x.ai/v1"
model = "kigi-4.5"
base_url = "https://byok.example/v1"
context_window = 200000
inference_idle_timeout_secs = 600
"#,
@@ -6030,8 +6032,8 @@ reasoning_effort = "low"
let raw_config: toml::Value = toml::from_str(
r#"
[model.default-model]
model = "grok-fast"
base_url = "https://api.x.ai/v1"
model = "kigi-fast"
base_url = "https://byok.example/v1"
context_window = 200000
"#,
)
@@ -6136,7 +6138,7 @@ reasoning_effort = "low"
);
assert_eq!(
sampling.base_url, "https://inference.example.com/v1",
"should route to the user's custom endpoint, not api.x.ai"
"should route to the user's custom endpoint, not the BYOK endpoint"
);
unsafe { std::env::remove_var("ENTERPRISE_AUTH_TOKEN") };
}
@@ -6229,8 +6231,8 @@ reasoning_effort = "low"
fn config_models_default_custom_model_is_in_resolved_model_list() {
let (_, models) = resolve_models_from_toml(
r#"
[model.acme-grok]
model = "grok-4.5"
[model.acme-kigi]
model = "kigi-4.5"
base_url = "https://inference.example.com/v1"
context_window = 256000
env_key = "ENTERPRISE_AUTH_TOKEN"
@@ -6238,11 +6240,11 @@ reasoning_effort = "low"
None,
);
assert!(
models.contains_key("acme-grok"),
models.contains_key("acme-kigi"),
"user-defined model must be in the resolved model list"
);
let model = models.get("acme-grok").unwrap();
assert_eq!(model.info.model, "grok-4.5");
let model = models.get("acme-kigi").unwrap();
assert_eq!(model.info.model, "kigi-4.5");
assert_eq!(model.info.base_url, "https://inference.example.com/v1");
}
#[test]
@@ -6342,7 +6344,7 @@ reasoning_effort = "low"
"https://proxy.api/v1",
None,
None,
Some("https://api.x.ai/v1"),
Some("https://byok.example/v1"),
);
let sampling = resolve_sampling(&model_no_key, Some("session-key"));
assert_eq!(
@@ -6361,7 +6363,7 @@ reasoning_effort = "low"
"env key should be used when no session and no model credentials"
);
assert_eq!(
sampling.base_url, "https://api.x.ai/v1",
sampling.base_url, "https://byok.example/v1",
"env key should route to api_base_url"
);
unsafe { std::env::remove_var("XAI_API_KEY") };
@@ -6447,17 +6449,17 @@ reasoning_effort = "low"
fn e2e_acp_model_info_no_dedup_on_model_field() {
let mut models = IndexMap::new();
models.insert(
"default-grok".to_string(),
"default-kigi".to_string(),
test_model_entry(
crate::models::default_model(),
"https://api.kimi.com/coding/v1",
None,
None,
Some("https://api.x.ai/v1"),
Some("https://byok.example/v1"),
),
);
models.insert(
"acme-grok".to_string(),
"acme-kigi".to_string(),
test_model_entry(
crate::models::default_model(),
"https://inference.example.com/v1",
@@ -6473,11 +6475,11 @@ reasoning_effort = "low"
"both entries should survive in ACP model list"
);
assert!(
acp_models.contains_key(&acp::ModelId::new("default-grok")),
acp_models.contains_key(&acp::ModelId::new("default-kigi")),
"default entry should be addressable by map key"
);
assert!(
acp_models.contains_key(&acp::ModelId::new("acme-grok")),
acp_models.contains_key(&acp::ModelId::new("acme-kigi")),
"user entry should be addressable by map key"
);
}
@@ -6550,6 +6552,16 @@ reasoning_effort = "low"
unsafe { std::env::remove_var(k) };
}
}
/// PRD §9: there is no built-in BYOK endpoint default — `api_base_url`
/// stays unset unless explicitly configured (config key, env var, CLI
/// flag, or requirements pin).
#[test]
#[serial]
fn api_base_url_has_no_default() {
unset_endpoint_env_vars();
assert_eq!(EndpointsConfig::default().api_base_url, None);
}
/// INVARIANT: auxiliary-service resolvers resolve to the cli-chat-proxy, never
/// `api_base_url` — overriding ONLY inference keeps every aux endpoint on
/// the proxy; explicit per-service overrides win verbatim.
@@ -6559,7 +6571,7 @@ reasoning_effort = "low"
unset_endpoint_env_vars();
let inference = "https://inference.acme-corp.example/xai/v1";
let cfg = EndpointsConfig {
api_base_url: inference.to_string(),
api_base_url: Some(inference.to_string()),
coding_api_base_url: None,
..Default::default()
};
@@ -6572,7 +6584,7 @@ reasoning_effort = "low"
format!("{proxy}/deployment/config")
);
assert_eq!(cfg.resolve_feedback_base_url(), proxy);
assert_eq!(cfg.api_base_url, inference);
assert_eq!(cfg.api_base_url.as_deref(), Some(inference));
let overridden = EndpointsConfig {
coding_api_base_url: Some("https://proxy.enterprise.example/v1".to_string()),
managed_config_url: Some(
@@ -7495,13 +7507,13 @@ reverify_after = 6
}
fn planner_pair() -> crate::util::config::GoalRoleModel {
crate::util::config::GoalRoleModel {
model: "grok-4".to_string(),
model: "kigi-4".to_string(),
agent_type: "general-purpose".to_string(),
}
}
fn strategist_pair() -> crate::util::config::GoalRoleModel {
crate::util::config::GoalRoleModel {
model: "grok-4.5".to_string(),
model: "kigi-4.5".to_string(),
agent_type: "cursor".to_string(),
}
}
@@ -7701,29 +7713,29 @@ reverify_after = 6
let toml_str = r#"
[goal]
enabled = true
planner_model = { model = "grok-build", agent_type = "grok-build-plan" }
planner_model = { model = "kigi", agent_type = "kigi-plan" }
[goal.strategist_model]
model = "grok-composer-2.5-fast"
model = "kigi-composer-2.5-fast"
agent_type = "cursor"
[[goal.skeptic_models]]
model = "grok-build"
agent_type = "grok-build-plan"
model = "kigi"
agent_type = "kigi-plan"
[[goal.skeptic_models]]
model = "grok-composer-2.5-fast"
model = "kigi-composer-2.5-fast"
agent_type = "cursor"
"#;
let raw: toml::Value = toml::from_str(toml_str).unwrap();
let cfg = Config::new_from_toml_cfg(&raw).unwrap();
assert_eq!(cfg.goal.planner_model.as_ref().unwrap().model, "grok-build");
assert_eq!(cfg.goal.planner_model.as_ref().unwrap().model, "kigi");
assert_eq!(
cfg.goal.strategist_model.as_ref().unwrap().agent_type,
"cursor"
);
assert_eq!(cfg.goal.skeptic_models.len(), 2);
assert_eq!(cfg.goal.skeptic_models[0].model, "grok-build");
assert_eq!(cfg.goal.skeptic_models[0].model, "kigi");
assert_eq!(
cfg.resolve_goal_planner_model(false).source,
ConfigSource::Config
@@ -7737,7 +7749,7 @@ agent_type = "cursor"
[goal]
enabled = true
classifier_max_runs = 6
planner_model = { agent_type = "grok-build-plan" }
planner_model = { agent_type = "kigi-plan" }
"#;
let raw: toml::Value = toml::from_str(toml_str).unwrap();
let cfg = Config::new_from_toml_cfg(&raw)
@@ -7752,21 +7764,21 @@ planner_model = { agent_type = "grok-build-plan" }
enabled = true
[[goal.skeptic_models]]
model = "grok-build"
agent_type = "grok-build-plan"
model = "kigi"
agent_type = "kigi-plan"
[[goal.skeptic_models]]
agent_type = "cursor"
[[goal.skeptic_models]]
model = "grok-composer-2.5-fast"
model = "kigi-composer-2.5-fast"
agent_type = "cursor"
"#;
let raw: toml::Value = toml::from_str(toml_str).unwrap();
let cfg = Config::new_from_toml_cfg(&raw).unwrap();
assert_eq!(cfg.goal.skeptic_models.len(), 2);
assert_eq!(cfg.goal.skeptic_models[0].model, "grok-build");
assert_eq!(cfg.goal.skeptic_models[1].model, "grok-composer-2.5-fast");
assert_eq!(cfg.goal.skeptic_models[0].model, "kigi");
assert_eq!(cfg.goal.skeptic_models[1].model, "kigi-composer-2.5-fast");
}
/// Acceptance test: a full managed-config `[goal]` block resolves end-to-end,
/// every value sourced from config (not remote/default).
@@ -7783,26 +7795,26 @@ classifier_enabled = true
planner_enabled = true
verifier_count = 3
classifier_max_runs = 6
planner_model = { model = "grok-build", agent_type = "grok-build-plan" }
strategist_model = { model = "grok-composer-2.5-fast", agent_type = "cursor" }
planner_model = { model = "kigi", agent_type = "kigi-plan" }
strategist_model = { model = "kigi-composer-2.5-fast", agent_type = "cursor" }
[[goal.skeptic_models]]
model = "grok-build"
agent_type = "grok-build-plan"
model = "kigi"
agent_type = "kigi-plan"
[[goal.skeptic_models]]
model = "grok-composer-2.5-fast"
model = "kigi-composer-2.5-fast"
agent_type = "cursor"
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("[goal] config must parse");
let grok_build = crate::util::config::GoalRoleModel {
model: "grok-build".into(),
agent_type: "grok-build-plan".into(),
let kigi = crate::util::config::GoalRoleModel {
model: "kigi".into(),
agent_type: "kigi-plan".into(),
};
let composer = crate::util::config::GoalRoleModel {
model: "grok-composer-2.5-fast".into(),
model: "kigi-composer-2.5-fast".into(),
agent_type: "cursor".into(),
};
let goal_enabled = cfg.resolve_goal().value;
@@ -7814,10 +7826,7 @@ agent_type = "cursor"
let use_current = cfg.resolve_goal_use_current_model_only().value;
assert!(!use_current);
let planner = cfg.resolve_goal_planner_model(use_current);
assert_eq!(
planner.value,
GoalRoleModelChoice::Explicit(grok_build.clone())
);
assert_eq!(planner.value, GoalRoleModelChoice::Explicit(kigi.clone()));
assert_eq!(planner.source, ConfigSource::Config);
assert_eq!(
cfg.resolve_goal_strategist_model(use_current).value,
@@ -7826,7 +7835,7 @@ agent_type = "cursor"
assert_eq!(
cfg.resolve_goal_skeptic_models(use_current).value,
vec![
GoalRoleModelChoice::Explicit(grok_build),
GoalRoleModelChoice::Explicit(kigi),
GoalRoleModelChoice::Explicit(composer),
]
);
@@ -7910,7 +7919,7 @@ agent_type = "cursor"
management_api_key = "mgmt-key"
gcs_service_account_key = "gcs-key"
[models]
default = "grok-3"
default = "kigi-3"
[ui]
yolo = true
theme = "dark"
@@ -8532,25 +8541,25 @@ hooks = true
let mut value: toml::Value = toml::from_str(
r#"
[models]
default = "grok-build"
default = "kigi"
[[version_overrides]]
minimum_version = "1.8.0"
[version_overrides.models]
default = "grok-4.5"
default = "kigi-4.5"
"#,
)
.unwrap();
let v = semver::Version::parse("1.8.0").unwrap();
kigi_config::apply_version_overrides(&mut value, &v).unwrap();
let cfg = Config::new_from_toml_cfg(&value).unwrap();
assert_eq!(cfg.models.default.as_deref(), Some("grok-4.5"));
assert_eq!(cfg.models.default.as_deref(), Some("kigi-4.5"));
}
/// Reproduce the enterprise managed config bug: [model.kigi-build] sets
/// context_window=500k for model="grok-4.5", but
/// [models].default="grok-4.5" resolves to the bare
/// context_window=500k for model="kigi-4.5", but
/// [models].default="kigi-4.5" resolves to the bare
/// prefetched entry (256k) because Layer 3 only overrides key
/// "kigi-build", not key "grok-4.5".
/// "kigi-build", not key "kigi-4.5".
///
/// After the Layer 4 slug propagation fix, both keys should have 500k.
#[test]
@@ -8559,10 +8568,10 @@ default = "grok-4.5"
let raw: toml::Value = toml::from_str(
r#"
[models]
default = "grok-4.5"
default = "kigi-4.5"
[model.kigi-build]
model = "grok-4.5"
model = "kigi-4.5"
context_window = 500000
base_url = "https://inference.example.com/v1"
api_backend = "responses"
@@ -8572,25 +8581,25 @@ default = "grok-4.5"
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let mut prefetched = IndexMap::new();
let mut entry = test_model_entry(
"grok-4.5",
"kigi-4.5",
"https://inference.example.com/v1",
None,
None,
None,
);
entry.info.context_window = NonZeroU64::new(default_cw).unwrap();
prefetched.insert("grok-4.5".to_owned(), entry);
prefetched.insert("kigi-4.5".to_owned(), entry);
let resolved = resolve_model_list(&cfg, Some(prefetched));
let by_key = resolved
.get("kigi-build")
.expect("kigi-build key must exist");
assert_eq!(by_key.info.context_window.get(), 500_000);
assert_eq!(by_key.info.model, "grok-4.5");
let by_latest = resolved.get("grok-4.5").expect("grok-4.5 key must exist");
assert_eq!(by_key.info.model, "kigi-4.5");
let by_latest = resolved.get("kigi-4.5").expect("kigi-4.5 key must exist");
assert_eq!(
by_latest.info.context_window.get(),
500_000,
"BUG: prefetched 'grok-4.5' should inherit 500k from \
"BUG: prefetched 'kigi-4.5' should inherit 500k from \
sibling 'kigi-build' (same model slug), not stay at {default_cw}"
);
}
@@ -8601,24 +8610,24 @@ default = "grok-4.5"
let raw: toml::Value = toml::from_str(
r#"
[model.kigi-build]
model = "grok-4.5"
model = "kigi-4.5"
context_window = 500000
base_url = "https://test.example.com/v1"
api_backend = "responses"
agent_type = "grok-build"
agent_type = "kigi"
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let mut prefetched = IndexMap::new();
let mut entry =
test_model_entry("grok-4.5", "https://test.example.com/v1", None, None, None);
test_model_entry("kigi-4.5", "https://test.example.com/v1", None, None, None);
entry.info.context_window = NonZeroU64::new(default_cw).unwrap();
entry.info.agent_type = default_agent_type();
entry.info.api_backend = ApiBackend::default();
prefetched.insert("grok-4.5".to_owned(), entry);
prefetched.insert("kigi-4.5".to_owned(), entry);
let resolved = resolve_model_list(&cfg, Some(prefetched));
let latest = resolved.get("grok-4.5").unwrap();
let latest = resolved.get("kigi-4.5").unwrap();
assert_eq!(
latest.info.agent_type,
default_agent_type(),
@@ -8637,7 +8646,7 @@ default = "grok-4.5"
let raw: toml::Value = toml::from_str(
r#"
[model.kigi-build]
model = "grok-4.5"
model = "kigi-4.5"
context_window = 500000
base_url = "https://test.example.com/v1"
"#,
@@ -8646,11 +8655,11 @@ default = "grok-4.5"
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let mut prefetched = IndexMap::new();
let mut entry =
test_model_entry("grok-4.5", "https://test.example.com/v1", None, None, None);
test_model_entry("kigi-4.5", "https://test.example.com/v1", None, None, None);
entry.info.context_window = NonZeroU64::new(65_536).unwrap();
prefetched.insert("grok-4.5".to_owned(), entry);
prefetched.insert("kigi-4.5".to_owned(), entry);
let resolved = resolve_model_list(&cfg, Some(prefetched));
let latest = resolved.get("grok-4.5").unwrap();
let latest = resolved.get("kigi-4.5").unwrap();
assert_eq!(
latest.info.context_window.get(),
65_536,
@@ -9017,13 +9026,13 @@ default = "grok-4.5"
fn resolve_model_list_inherits_agent_type_and_api_backend() {
let cfg = Config::default();
let default_cw = DEFAULT_CONTEXT_WINDOW;
let entry = prefetch_model_entry("grok-build", default_cw, ApiBackend::default());
let entry = prefetch_model_entry("kigi", default_cw, ApiBackend::default());
let mut prefetched = IndexMap::new();
prefetched.insert("grok-build".to_owned(), entry);
prefetched.insert("kigi".to_owned(), entry);
let resolved = resolve_model_list(&cfg, Some(prefetched));
let entry = resolved.get("grok-build").expect("model must exist");
let entry = resolved.get("kigi").expect("model must exist");
let defaults = default_model_entries(&EndpointsConfig::default());
if let Some(default) = defaults.get("grok-build") {
if let Some(default) = defaults.get("kigi") {
if default.info.agent_type != DEFAULT_AGENT_TYPE {
assert_eq!(
entry.info.agent_type, default.info.agent_type,
@@ -10,7 +10,7 @@
//! parsed again. Non-table values are dropped with a warning.
//!
//! Warnings are retained on `Config::model_override_warnings` and surfaced by
//! `grok inspect`.
//! `kigi inspect`.
use indexmap::IndexMap;
use serde::Serialize;
@@ -125,7 +125,7 @@ pub(crate) fn log_model_override_warnings(warnings: &[ModelOverrideWarning]) {
if !warnings.is_empty() {
tracing::warn!(
warnings = warnings.len(),
"model_override: parsed with warnings; run `grok inspect` for details"
"model_override: parsed with warnings; run `kigi inspect` for details"
);
}
}
@@ -292,8 +292,8 @@ mod tests {
fn duplicate_compactions_keys_keeps_model() {
let cfg = parse_cfg(
r#"
[model."grok-4.5"]
model = "grok-4.5"
[model."kigi-4.5"]
model = "kigi-4.5"
env_key = "ANTHROPIC_AUTH_TOKEN"
compactions_remaining = 1
send_compactions_remaining = true
@@ -301,8 +301,8 @@ mod tests {
);
let model = cfg
.config_models
.get("grok-4.5")
.expect("grok-4.5 must remain in catalog");
.get("kigi-4.5")
.expect("kigi-4.5 must remain in catalog");
assert_eq!(
model.compactions_remaining,
Some(CompactionsRemaining::Fixed(1))
@@ -312,19 +312,19 @@ mod tests {
&& w.field.as_deref() == Some("send_compactions_remaining")
}));
let resolved = crate::agent::config::resolve_model_list(&cfg, None);
assert!(resolved.contains_key("grok-4.5"));
assert!(resolved.contains_key("kigi-4.5"));
}
#[test]
fn legacy_alias_alone_parses_without_warning() {
let cfg = parse_cfg(
r#"
[model."grok-4.5"]
model = "grok-4.5"
[model."kigi-4.5"]
model = "kigi-4.5"
send_compactions_remaining = 2
"#,
);
let model = cfg.config_models.get("grok-4.5").unwrap();
let model = cfg.config_models.get("kigi-4.5").unwrap();
assert_eq!(
model.compactions_remaining,
Some(CompactionsRemaining::Fixed(2))
@@ -336,17 +336,17 @@ mod tests {
fn invalid_reasoning_effort_skips_field_keeps_model() {
let cfg = parse_cfg(
r#"
[model."grok-4.5"]
model = "grok-4.5"
[model."kigi-4.5"]
model = "kigi-4.5"
env_key = "ANTHROPIC_AUTH_TOKEN"
reasoning_effort = "not-a-level"
"#,
);
let model = cfg
.config_models
.get("grok-4.5")
.expect("grok-4.5 must remain in catalog");
assert_eq!(model.model.as_deref(), Some("grok-4.5"));
.get("kigi-4.5")
.expect("kigi-4.5 must remain in catalog");
assert_eq!(model.model.as_deref(), Some("kigi-4.5"));
assert!(model.reasoning_effort.is_none());
assert!(cfg.model_override_warnings.iter().any(|w| {
w.kind == ModelOverrideWarningKind::InvalidValue
@@ -358,14 +358,14 @@ mod tests {
fn unknown_field_warns_but_keeps_known_fields() {
let (models, warnings) = parse_raw(
r#"
[model."grok-4.5"]
model = "grok-4.5"
[model."kigi-4.5"]
model = "kigi-4.5"
env_key = "TOKEN"
future_field = 1
"#,
);
let entry = models.get("grok-4.5").unwrap();
assert_eq!(entry.model.as_deref(), Some("grok-4.5"));
let entry = models.get("kigi-4.5").unwrap();
assert_eq!(entry.model.as_deref(), Some("kigi-4.5"));
assert_eq!(
entry.env_key.as_ref().and_then(|k| k.primary()),
Some("TOKEN")
@@ -373,7 +373,7 @@ mod tests {
assert_eq!(
warnings,
vec![ModelOverrideWarning {
model_key: Some("grok-4.5".to_owned()),
model_key: Some("kigi-4.5".to_owned()),
field: Some("future_field".to_owned()),
kind: ModelOverrideWarningKind::UnknownField,
reason: "unknown field".to_owned(),
@@ -472,7 +472,7 @@ mod tests {
#[test]
fn non_table_model_section_warns_and_is_ignored() {
let (models, warnings) = parse_raw(r#"model = "grok-4""#);
let (models, warnings) = parse_raw(r#"model = "kigi-4""#);
assert!(models.is_empty());
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].kind, ModelOverrideWarningKind::NotATable);
@@ -5,7 +5,7 @@
use crate::session::SessionCommand;
/// Parse a `x.ai/queue/{remove,reorder,clear,edit,interject}` ext-notification's
/// Parse a `kigi/queue/{remove,reorder,clear,edit,interject}` ext-notification's
/// params into the corresponding [`SessionCommand`].
/// `owner` is the resolved attribution (params `owner`/`clientIdentifier`) used
/// to scope remove/clear to the requesting client's own items, and recorded as
@@ -17,7 +17,7 @@ pub(super) fn parse_queue_edit_command(
owner: Option<String>,
) -> Option<SessionCommand> {
match method {
"x.ai/queue/remove" => {
"kigi/queue/remove" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
// The client supplies the version it last saw; the handler removes
// only on an exact match (stale = benign no-op + rebroadcast).
@@ -32,7 +32,7 @@ pub(super) fn parse_queue_edit_command(
owner,
})
}
"x.ai/queue/reorder" => {
"kigi/queue/reorder" => {
let ordered_ids = params
.get("orderedIds")
.and_then(|v| v.as_array())
@@ -44,8 +44,8 @@ pub(super) fn parse_queue_edit_command(
.unwrap_or_default();
Some(SessionCommand::ReorderQueue { ordered_ids })
}
"x.ai/queue/clear" => Some(SessionCommand::ClearQueue { owner }),
"x.ai/queue/interject" => {
"kigi/queue/clear" => Some(SessionCommand::ClearQueue { owner }),
"kigi/queue/interject" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
// The client supplies the version it last saw; the handler acts
// only on an exact match (stale = benign no-op + rebroadcast).
@@ -68,7 +68,7 @@ pub(super) fn parse_queue_edit_command(
new_text,
})
}
"x.ai/queue/edit" => {
"kigi/queue/edit" => {
let id = params.get("id").and_then(|v| v.as_str())?.to_string();
let new_text = params.get("newText").and_then(|v| v.as_str())?.to_string();
// `owner` is the resolved attribution; for edit it represents the
@@ -88,7 +88,7 @@ pub(super) fn parse_queue_edit_command(
mod tests {
use super::*;
/// Each `x.ai/queue/*` ext-notification maps to the
/// Each `kigi/queue/*` ext-notification maps to the
/// correct versioned/idempotent `SessionCommand`.
#[test]
fn parse_queue_edit_command_maps_each_method() {
@@ -96,7 +96,7 @@ mod tests {
let p = serde_json::json!({
"sessionId": "s1", "id": "p7", "expectedVersion": 3
});
match parse_queue_edit_command("x.ai/queue/remove", &p, Some("grok-tui".into())) {
match parse_queue_edit_command("kigi/queue/remove", &p, Some("kigi-tui".into())) {
Some(SessionCommand::RemoveQueuedPrompt {
id,
expected_version,
@@ -104,14 +104,14 @@ mod tests {
}) => {
assert_eq!(id, "p7");
assert_eq!(expected_version, 3);
assert_eq!(owner.as_deref(), Some("grok-tui"));
assert_eq!(owner.as_deref(), Some("kigi-tui"));
}
_ => panic!("expected RemoveQueuedPrompt"),
}
// remove without expectedVersion defaults to 0.
let p = serde_json::json!({ "sessionId": "s1", "id": "p8" });
match parse_queue_edit_command("x.ai/queue/remove", &p, None) {
match parse_queue_edit_command("kigi/queue/remove", &p, None) {
Some(SessionCommand::RemoveQueuedPrompt {
expected_version, ..
}) => assert_eq!(expected_version, 0),
@@ -120,7 +120,7 @@ mod tests {
// reorder: orderedIds array.
let p = serde_json::json!({ "sessionId": "s1", "orderedIds": ["a", "b", "c"] });
match parse_queue_edit_command("x.ai/queue/reorder", &p, None) {
match parse_queue_edit_command("kigi/queue/reorder", &p, None) {
Some(SessionCommand::ReorderQueue { ordered_ids }) => {
assert_eq!(ordered_ids, vec!["a", "b", "c"]);
}
@@ -129,12 +129,12 @@ mod tests {
// clear: owner-scoped.
match parse_queue_edit_command(
"x.ai/queue/clear",
"kigi/queue/clear",
&serde_json::json!({ "sessionId": "s1" }),
Some("grok-tui".into()),
Some("kigi-tui".into()),
) {
Some(SessionCommand::ClearQueue { owner }) => {
assert_eq!(owner.as_deref(), Some("grok-tui"));
assert_eq!(owner.as_deref(), Some("kigi-tui"));
}
_ => panic!("expected ClearQueue"),
}
@@ -143,7 +143,7 @@ mod tests {
let p = serde_json::json!({
"sessionId": "s1", "id": "p9", "newText": "replacement text"
});
match parse_queue_edit_command("x.ai/queue/edit", &p, Some("grok-vscode".into())) {
match parse_queue_edit_command("kigi/queue/edit", &p, Some("kigi-vscode".into())) {
Some(SessionCommand::EditQueuedPrompt {
id,
new_text,
@@ -151,14 +151,14 @@ mod tests {
}) => {
assert_eq!(id, "p9");
assert_eq!(new_text, "replacement text");
assert_eq!(editor.as_deref(), Some("grok-vscode"));
assert_eq!(editor.as_deref(), Some("kigi-vscode"));
}
_ => panic!("expected EditQueuedPrompt"),
}
// edit without editor (no owner/clientIdentifier) → editor: None.
match parse_queue_edit_command(
"x.ai/queue/edit",
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "id": "p9", "newText": "x" }),
None,
) {
@@ -171,7 +171,7 @@ mod tests {
// edit without newText → None (can't replace text we don't have).
assert!(
parse_queue_edit_command(
"x.ai/queue/edit",
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "id": "p9" }),
None,
)
@@ -181,7 +181,7 @@ mod tests {
// edit without id → None (can't target an entry).
assert!(
parse_queue_edit_command(
"x.ai/queue/edit",
"kigi/queue/edit",
&serde_json::json!({ "sessionId": "s1", "newText": "x" }),
None,
)
@@ -192,7 +192,7 @@ mod tests {
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2
});
match parse_queue_edit_command("x.ai/queue/interject", &p, Some("grok-tui".into())) {
match parse_queue_edit_command("kigi/queue/interject", &p, Some("kigi-tui".into())) {
Some(SessionCommand::InterjectQueuedPrompt {
id,
expected_version,
@@ -201,7 +201,7 @@ mod tests {
}) => {
assert_eq!(id, "p10");
assert_eq!(expected_version, 2);
assert_eq!(owner.as_deref(), Some("grok-tui"));
assert_eq!(owner.as_deref(), Some("kigi-tui"));
assert_eq!(new_text, None, "newText absent → None");
}
_ => panic!("expected InterjectQueuedPrompt"),
@@ -211,7 +211,7 @@ mod tests {
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2, "newText": "edited"
});
match parse_queue_edit_command("x.ai/queue/interject", &p, None) {
match parse_queue_edit_command("kigi/queue/interject", &p, None) {
Some(SessionCommand::InterjectQueuedPrompt { new_text, .. }) => {
assert_eq!(new_text.as_deref(), Some("edited"));
}
@@ -222,7 +222,7 @@ mod tests {
let p = serde_json::json!({
"sessionId": "s1", "id": "p10", "expectedVersion": 2, "newText": " "
});
match parse_queue_edit_command("x.ai/queue/interject", &p, None) {
match parse_queue_edit_command("kigi/queue/interject", &p, None) {
Some(SessionCommand::InterjectQueuedPrompt { new_text, .. }) => {
assert_eq!(new_text, None, "blank override must be dropped");
}
@@ -231,7 +231,7 @@ mod tests {
// interject without expectedVersion defaults to 0.
match parse_queue_edit_command(
"x.ai/queue/interject",
"kigi/queue/interject",
&serde_json::json!({ "sessionId": "s1", "id": "p11" }),
None,
) {
@@ -243,17 +243,17 @@ mod tests {
// interject without id → None (can't target an entry).
assert!(
parse_queue_edit_command("x.ai/queue/interject", &serde_json::json!({}), None)
parse_queue_edit_command("kigi/queue/interject", &serde_json::json!({}), None)
.is_none()
);
// unknown method → None.
assert!(
parse_queue_edit_command("x.ai/queue/bogus", &serde_json::json!({}), None).is_none()
parse_queue_edit_command("kigi/queue/bogus", &serde_json::json!({}), None).is_none()
);
// remove without id → None (can't target an entry).
assert!(
parse_queue_edit_command("x.ai/queue/remove", &serde_json::json!({}), None).is_none()
parse_queue_edit_command("kigi/queue/remove", &serde_json::json!({}), None).is_none()
);
}
}
@@ -148,7 +148,7 @@ pub fn project_scope_allowed(cwd: &Path) -> bool {
/// is on, the workspace is NOT store-trusted, and repo-local code-exec configs
/// are present (something to gate). Interactivity is forced `true` because the
/// caller already confirmed the client can prompt (it advertised
/// `x.ai/folderTrust.interactive`); the TTY-based [`decide_inputs`] default is
/// `kigi/folderTrust.interactive`); the TTY-based [`decide_inputs`] default is
/// false under the ACP stdio transport. Mirrors the [`decide`] precedence so it
/// cannot drift from the gate: feature-off (kill-switch / opt-out) / store-trusted
/// / no-configs all collapse to a non-`Prompt` verdict and return false.
@@ -226,7 +226,7 @@ pub(crate) fn record_for_test(cwd: &Path, allowed: bool) {
/// `allow_prompt` must be `true` ONLY where a blocking stdin y/N read is safe —
/// i.e. agent `initialize` for the launch directory, before the TUI takes over
/// the terminal. Every other call site (per-session cwd, leader-served sessions
/// whose cwd differs from the launch dir, `grok mcp doctor`) passes `false`, so
/// whose cwd differs from the launch dir, `kigi mcp doctor`) passes `false`, so
/// an unresolved interactive-but-untrusted workspace resolves **fail-closed**
/// (untrusted, no prompt) — only the launch dir is ever prompted for.
pub fn resolve_and_record(cwd: &Path, remote: Option<&RemoteSettings>, allow_prompt: bool) -> bool {
@@ -290,7 +290,7 @@ pub fn resolve_launch_dir_trust(cwd: &Path, remote: Option<&RemoteSettings>) ->
/// - A cached **grant** (`Some(true)`) is durable and short-circuits — neither
/// `store_trusted` nor `recompute` runs.
/// - A cached **untrusted** verdict (`Some(false)`) is re-checked via
/// `store_trusted`: a `grok --trust` grant issued AFTER this workspace was
/// `store_trusted`: a `kigi --trust` grant issued AFTER this workspace was
/// first resolved writes the store, so honor it on the next session without a
/// restart. Without this re-read a long-lived leader would mask the grant.
/// - An **unrecorded** key (`None`) does a full `recompute`, which reports
@@ -389,7 +389,7 @@ fn compute_from_inputs(
/// merged server list when the workspace is untrusted.
///
/// SINGLE SOURCE OF TRUTH for "project-scoped MCP names" across ALL gate sites
/// (session merge, the session-less agent pool, `grok mcp doctor`). It MUST
/// (session merge, the session-less agent pool, `kigi mcp doctor`). It MUST
/// enumerate every project MCP source the loaders read; adding a new repo-local
/// MCP source without extending this fn silently re-opens the gate (guarded by
/// `project_scoped_mcp_names_cover_every_source`).
@@ -1190,9 +1190,9 @@ mod tests {
// can distinguish it from user/plugin servers. Asserts on the specific
// key, so any real `~/.kigi/lsp.json` on the test host is irrelevant.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("lsp.json"), r#"{"projlsp": {"command": "true"}}"#).unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("lsp.json"), r#"{"projlsp": {"command": "true"}}"#).unwrap();
let sourced = load_servers_with_plugins_sourced(tmp.path(), &[], &[], &[], &[]);
let (_, source) = sourced.get("projlsp").expect("project server present");
@@ -1209,9 +1209,9 @@ mod tests {
// End-to-end of the load-site gate (Sites A/B): a project server loaded
// from `<cwd>/.kigi/lsp.json` is dropped once the workspace is untrusted.
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("lsp.json"), r#"{"projlsp": {"command": "true"}}"#).unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("lsp.json"), r#"{"projlsp": {"command": "true"}}"#).unwrap();
let sourced = load_servers_with_plugins_sourced(tmp.path(), &[], &[], &[], &[]);
assert!(
@@ -1243,10 +1243,10 @@ mod tests {
r#"{"mcpServers": {"projjson": {"url": "https://proj.example.com/mcp"}}}"#,
)
.unwrap();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(
grok.join("config.toml"),
kigi.join("config.toml"),
"[mcp_servers.projtoml]\nurl = \"https://projtoml.example.com/mcp\"\n",
)
.unwrap();
@@ -1264,10 +1264,10 @@ mod tests {
#[test]
fn project_scoped_mcp_names_cover_every_source() {
let tmp = repo_tmp();
let grok = tmp.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
let kigi = tmp.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(
grok.join("config.toml"),
kigi.join("config.toml"),
"[mcp_servers.cfgsrv]\nurl = \"https://cfg.example.com/mcp\"\n",
)
.unwrap();
@@ -1339,7 +1339,7 @@ mod tests {
let key = workspace_key(tmp.path());
record(&key, false);
assert!(!project_scope_allowed(tmp.path()));
// Simulate a `grok --trust` grant landing in the store after the
// Simulate a `kigi --trust` grant landing in the store after the
// untrusted verdict was cached: the re-read sees trusted, so the next
// resolve upgrades the cache without a process restart.
let allowed = resolve_and_record_inner(
@@ -226,7 +226,7 @@ fn broadcast_model_changed(
agent
.gateway
.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/session_notification",
"kigi/session_notification",
params.into(),
));
}
@@ -28,27 +28,27 @@ fn backfill_session_summary(summary: &mut Summary) {
}
}
/// Router for x.ai/session/* and x.ai/session_summaries/* methods.
/// Router for kigi/session/* and kigi/session_summaries/* methods.
pub async fn handle(
agent: &MvpAgent,
args: &acp::ExtRequest,
) -> Result<acp::ExtResponse, acp::Error> {
match args.method.as_ref() {
"x.ai/session/info" => handle_session_info(agent, args).await,
"x.ai/session/close" => handle_session_close(agent, args).await,
"x.ai/session/list" => handle_session_list(agent, args).await,
"x.ai/sessions/list" => handle_roster_list(agent, args).await,
m if m.starts_with("x.ai/session_summaries/") => {
"kigi/session/info" => handle_session_info(agent, args).await,
"kigi/session/close" => handle_session_close(agent, args).await,
"kigi/session/list" => handle_session_list(agent, args).await,
"kigi/sessions/list" => handle_roster_list(agent, args).await,
m if m.starts_with("kigi/session_summaries/") => {
handle_session_summaries(agent, args).await
}
_ => Err(acp::Error::method_not_found()),
}
}
/// `x.ai/sessions/list` — the FleetView roster. Returns every
/// `kigi/sessions/list` — the FleetView roster. Returns every
/// resident session plus recently-touched on-disk `Dormant` sessions. Clients
/// poll this while the dashboard is open and reconcile against the
/// `x.ai/sessions/changed` broadcast.
/// `kigi/sessions/changed` broadcast.
async fn handle_roster_list(
agent: &MvpAgent,
_args: &acp::ExtRequest,
@@ -166,7 +166,7 @@ async fn handle_session_close(
// (see `MvpAgent::handle_evict_sessions` / `close_session_explicit`).
agent.request_session_shutdown(&sid);
agent.close_session_explicit(&sid);
tracing::info!(session_id = %req.session_id, "session closed via x.ai/session/close");
tracing::info!(session_id = %req.session_id, "session closed via kigi/session/close");
} else {
tracing::debug!(session_id = %req.session_id, "session/close: session not found (already closed)");
}
@@ -181,7 +181,7 @@ async fn handle_session_summaries(
args: &acp::ExtRequest,
) -> Result<acp::ExtResponse, acp::Error> {
match args.method.as_ref() {
"x.ai/session_summaries/session_list" => {
"kigi/session_summaries/session_list" => {
let req = serde_json::from_str::<SessionListRequest>(args.params.get())?;
let cwd = req.workspace_directory.to_string_lossy().to_string();
@@ -203,7 +203,7 @@ async fn handle_session_summaries(
Ok(acp::ExtResponse::new(value))
}
"x.ai/session_summaries/workspace_list" => {
"kigi/session_summaries/workspace_list" => {
tracing::debug!("xai/session_summaries/workspace_list is working");
let _req = serde_json::from_str::<AllSessionOverviewRequest>(args.params.get())?;
@@ -215,7 +215,7 @@ async fn handle_session_summaries(
summaries_to_overview_response(summaries)
}
"x.ai/session_summaries/workspace_list_recent" => {
"kigi/session_summaries/workspace_list_recent" => {
let req = serde_json::from_str::<RecentSessionsRequest>(args.params.get())?;
let _timer = crate::instrumentation_timer!("session.list_sessions_recent");
+2 -2
View File
@@ -76,7 +76,7 @@ fn resolve_config(cfg: &AgentConfig, auth_manager: &AuthManager) -> AgentConfig
crate::util::config::sync_campaign_fields(&mut cfg);
crate::agent::config::apply_remote_settings_side_effects(cfg.remote_settings.as_ref());
// env var > remote settings > Local. Skip remote settings for Generic (grok -p, subagents).
// env var > remote settings > Local. Skip remote settings for Generic (kigi -p, subagents).
if cfg.storage_mode == StorageMode::Local
&& cfg.mode != crate::agent::config::AgentMode::Generic
{
@@ -86,7 +86,7 @@ fn resolve_config(cfg: &AgentConfig, auth_manager: &AuthManager) -> AgentConfig
if cfg.storage_mode == StorageMode::Writeback
&& !auth_manager.current().is_some_and(|a| a.is_session_auth())
{
tracing::info!("Writeback is disabled: requires auth with grok.com");
tracing::info!("Writeback is disabled: requires auth with kimi.com");
cfg.storage_mode = StorageMode::Local;
}
+141 -163
View File
@@ -405,7 +405,7 @@ impl ModelsManager {
self.reselect_current_model_if_missing(&new_config);
}
// Push the new catalog to connected clients (`x.ai/models/update`).
// Push the new catalog to connected clients (`kigi/models/update`).
// Without this, a long-running agent (leader mode) correctly swaps
// its in-memory catalog on a config.toml `[model.*]`/`[models]` edit,
// but already-connected clients keep rendering the stale model list
@@ -592,10 +592,10 @@ impl ModelsManager {
/// Catalog opt-in to display the served-checkpoint fingerprint for this model.
///
/// `model_id` may be a routing slug (`config.model`, e.g. `grok-4.5`)
/// `model_id` may be a routing slug (`config.model`, e.g. `kigi-4.5`)
/// OR a catalog key; the catalog map is keyed by the config key, which can
/// differ from the slug for custom/enterprise ids (e.g. key `enterprise-grok-build`
/// → slug `grok-4.5`). Resolve to the catalog key first so a slug
/// differ from the slug for custom/enterprise ids (e.g. key `enterprise-kigi`
/// → slug `kigi-4.5`). Resolve to the catalog key first so a slug
/// caller still finds the opted-in entry.
pub fn model_show_model_fingerprint(&self, model_id: &str) -> bool {
let models = self.inner.models.read();
@@ -737,7 +737,7 @@ impl ModelsManager {
acp::SessionModelState::new(current, available.values().cloned().collect());
if let Ok(params) = serde_json::value::to_raw_value(&model_state) {
gw.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/models/update",
"kigi/models/update",
params.into(),
));
}
@@ -749,8 +749,8 @@ impl ModelsManager {
///
/// A long-running leader otherwise only refreshes its catalog from its
/// *own* fetch paths (startup prefetch, auth change, response-header etag).
/// When another grok process sharing `~/.kigi` (a `--no-leader` run, a
/// newer client, grok-desktop) fetches a fresher `/v1/models` catalog and
/// When another kigi process sharing `~/.kigi` (a `--no-leader` run, a
/// newer client, kigi-desktop) fetches a fresher `/v1/models` catalog and
/// persists it, this picks it up without a network round-trip.
///
/// Guards, in order:
@@ -934,7 +934,7 @@ impl ModelsManager {
/// delivering events on macOS after resume from sleep. On each
/// notification the catalog is re-fetched from the server; if the
/// fetch succeeds and the catalog changed, clients are notified
/// via `x.ai/models/update`.
/// via `kigi/models/update`.
pub fn start_auth_refresh_watcher(&self, notify: Arc<tokio::sync::Notify>) {
let mgr = self.clone();
let had_catalog_at_start = *self.inner.has_fetched_real_catalog.read();
@@ -1687,7 +1687,7 @@ struct PrefetchEnv {
fn resolve_prefetch_env_with_auth(auth: Option<KimiAuth>) -> Option<PrefetchEnv> {
let _timer = crate::instrumentation_timer!("startup.early_prefetch_launch");
// Config-aware (not env-only) so the prefetch can't leak the bearer to api.x.ai.
// Config-aware (not env-only) so the prefetch can't leak the bearer to the BYOK endpoint.
let mut endpoints = config::EndpointsConfig::from_effective_config();
if endpoints.deployment_key.is_none() {
@@ -1796,8 +1796,8 @@ fn spawn_prefetch_thread(env: PrefetchEnv) -> EarlyPrefetchHandle {
/// Map a model id (catalog key or routing slug) to its catalog key.
///
/// Sessions persist the routing slug (`[model.X].model`, e.g. `grok-4.5`);
/// the catalog and `/model` picker use config keys (e.g. `enterprise-grok-build`).
/// Sessions persist the routing slug (`[model.X].model`, e.g. `kigi-4.5`);
/// the catalog and `/model` picker use config keys (e.g. `enterprise-kigi`).
/// Last slug match wins so user overrides beat defaults (matches `MvpAgent::resolve_model_id`).
pub(crate) fn resolve_catalog_key(
models: &IndexMap<String, ModelEntry>,
@@ -2070,7 +2070,7 @@ pub fn resolve_model_catalog(
// Skip non-reasoning models so we don't send the field to providers that reject it.
// Also skip models whose effort menu does not include the override (e.g. `--effort none`
// must not stamp `none` onto grok-4.5, which only offers low/medium/high).
// must not stamp `none` onto kigi-4.5, which only offers low/medium/high).
if let Some(effort) = cfg.reasoning_effort_override {
for entry in catalog.values_mut() {
if model_offers_reasoning_effort(&entry.info, effort) {
@@ -2188,7 +2188,7 @@ mod tests {
.try_init();
// Use a temp dir so AuthManager finds no credentials — ensures
// refresh_async bails at the auth check without needing a tokio runtime.
let tmp = std::env::temp_dir().join("grok-test-models-manager");
let tmp = std::env::temp_dir().join("kigi-test-models-manager");
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
ModelsManager::new(
None,
@@ -2263,11 +2263,11 @@ mod tests {
allowed_models = ["keep-*"]
[model.zzz-first]
model = "zzz-first"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
[model.keep-one]
model = "keep-one"
base_url = "https://api.x.ai/v1"
base_url = "https://byok.example/v1"
context_window = 256000
"#,
);
@@ -2286,15 +2286,15 @@ mod tests {
let excluded = config_from_toml(
r#"
[models]
default = "grok-3"
allowed_models = ["grok-4*"]
default = "kigi-3"
allowed_models = ["kigi-4*"]
[model.kigi-3]
model = "grok-3"
base_url = "https://api.x.ai/v1"
model = "kigi-3"
base_url = "https://byok.example/v1"
context_window = 256000
[model.kigi-4]
model = "grok-4"
base_url = "https://api.x.ai/v1"
model = "kigi-4"
base_url = "https://byok.example/v1"
context_window = 256000
"#,
);
@@ -2302,7 +2302,7 @@ mod tests {
assert!(
validate_selectable(&excluded, &catalog)
.unwrap_err()
.contains("grok-3")
.contains("kigi-3")
);
// Matches nothing → error.
@@ -2311,8 +2311,8 @@ mod tests {
[models]
allowed_models = ["nomatch-*"]
[model.kigi-4]
model = "grok-4"
base_url = "https://api.x.ai/v1"
model = "kigi-4"
base_url = "https://byok.example/v1"
context_window = 256000
"#,
);
@@ -2362,7 +2362,7 @@ mod tests {
);
// Real switch: both subscribers see the change.
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
tokio::time::timeout(std::time::Duration::from_millis(100), rx_a.changed())
.await
.expect("rx_a saw the switch")
@@ -2380,13 +2380,13 @@ mod tests {
async fn model_switch_generation_snapshot_reflects_current_state() {
let mgr = test_manager();
let start = mgr.model_switch_generation();
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
assert_eq!(mgr.model_switch_generation(), start + 1);
// Idempotent: same id → no bump.
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
assert_eq!(mgr.model_switch_generation(), start + 1);
// Another real change: another bump.
mgr.set_current_model_id(acp::ModelId::new("grok-3"));
mgr.set_current_model_id(acp::ModelId::new("kigi-3"));
assert_eq!(mgr.model_switch_generation(), start + 2);
}
@@ -2430,7 +2430,7 @@ mod tests {
#[test]
fn current_reasoning_effort_seeded_from_config() {
let tmp = std::env::temp_dir().join("grok-test-models-manager-seed");
let tmp = std::env::temp_dir().join("kigi-test-models-manager-seed");
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
let mut cfg = config::Config::default();
cfg.models.default_reasoning_effort = Some(ReasoningEffort::Xhigh);
@@ -2504,7 +2504,7 @@ mod tests {
let mut prefetched = IndexMap::new();
// 4.5-style: supports effort, menu is high only (no none).
let mut no_none = ModelEntry {
info: config::ModelInfo::fallback("grok-4.5"),
info: config::ModelInfo::fallback("kigi-4.5"),
api_key: None,
env_key: None,
api_base_url: None,
@@ -2518,7 +2518,7 @@ mod tests {
default: true,
}];
no_none.info.reasoning_effort = Some(ReasoningEffort::High);
prefetched.insert("grok-4.5".to_string(), no_none);
prefetched.insert("kigi-4.5".to_string(), no_none);
// Model that explicitly offers none.
let mut with_none = ModelEntry {
@@ -2539,7 +2539,7 @@ mod tests {
let catalog = resolve_model_catalog(&cfg, Some(prefetched));
assert_eq!(
catalog["grok-4.5"].info.reasoning_effort,
catalog["kigi-4.5"].info.reasoning_effort,
Some(ReasoningEffort::High),
"--effort none must not stamp onto models that do not offer none"
);
@@ -2598,7 +2598,7 @@ mod tests {
assert_eq!(catalog["plain"].info.reasoning_effort, None);
// The internal getters read those derived fields.
let tmp = std::env::temp_dir().join("grok-test-models-manager-menu-only");
let tmp = std::env::temp_dir().join("kigi-test-models-manager-menu-only");
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
let mgr = ModelsManager::new(
None,
@@ -2698,37 +2698,37 @@ mod tests {
fn first_apply_refresh_reselects_default_model() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
assert!(!mgr.has_fetched_real_catalog());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert!(mgr.has_fetched_real_catalog());
assert_eq!(mgr.current_model_id().0.as_ref(), "grok-3");
assert_eq!(mgr.current_model_id().0.as_ref(), "kigi-3");
}
#[test]
fn subsequent_apply_refresh_preserves_user_model() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Simulate on_auth_changed clearing prefetched + etag.
*mgr.inner.prefetched.write() = None;
*mgr.inner.etag.write() = None;
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-4",
"kigi-4",
"user's model selection must survive auth-change refresh"
);
}
@@ -2737,19 +2737,19 @@ mod tests {
fn subsequent_refresh_reselects_when_model_removed() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Second refresh with grok-4 removed.
let prefetched = make_prefetched(&["grok-3", "grok-4.5"]);
// Second refresh with kigi-4 removed.
let prefetched = make_prefetched(&["kigi-3", "kigi-4.5"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-3",
"kigi-3",
"should fall back to config default when current is removed"
);
}
@@ -2773,11 +2773,11 @@ mod tests {
fn apply_config_honors_new_preferred_model() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Simulate stale inner cfg (no default) from a racing auth refresh.
let mut stale_cfg = config::Config::default();
@@ -2785,12 +2785,12 @@ mod tests {
*mgr.inner.cfg.write() = stale_cfg;
let mut new_cfg = config::Config::default();
new_cfg.models.default = Some("grok-3".to_string());
new_cfg.models.default = Some("kigi-3".to_string());
mgr.apply_config(new_cfg);
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-3",
"kigi-3",
"apply_config must honor updated preferred model from config"
);
}
@@ -2800,10 +2800,10 @@ mod tests {
let mgr = test_manager();
let cfg = config::Config::default();
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Unrelated config change — preferred model unchanged.
let new_cfg = config::Config::default();
@@ -2811,7 +2811,7 @@ mod tests {
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-4",
"kigi-4",
"apply_config must not reset model when preferred hasn't changed"
);
}
@@ -2820,16 +2820,16 @@ mod tests {
fn apply_config_falls_back_when_preferred_not_in_catalog() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Preferred model not in catalog — falls back to first entry.
let mut new_cfg = config::Config::default();
new_cfg.models.default = Some("grok-nonexistent".to_string());
new_cfg.models.default = Some("kigi-nonexistent".to_string());
mgr.apply_config(new_cfg);
let current = mgr.current_model_id();
@@ -2845,15 +2845,15 @@ mod tests {
fn apply_config_both_none_preferred_preserves_current() {
let mgr = test_manager();
let cfg = config::Config::default();
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
let new_cfg = config::Config::default();
mgr.apply_config(new_cfg);
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-4",
"kigi-4",
"both-None preferred must preserve user's runtime model"
);
}
@@ -2862,13 +2862,13 @@ mod tests {
fn apply_config_old_some_new_none_preserves_current() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert_eq!(mgr.current_model_id().0.as_ref(), "grok-3");
assert_eq!(mgr.current_model_id().0.as_ref(), "kigi-3");
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// [models] default removed — is_some() guard prevents reset.
let new_cfg = config::Config::default();
@@ -2876,7 +2876,7 @@ mod tests {
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-4",
"kigi-4",
"old=Some new=None must not reset model (is_some guard)"
);
}
@@ -2887,29 +2887,29 @@ mod tests {
fn auth_refresh_then_config_reload_preserves_user_model() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
// Initial fetch.
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
// User runs /model grok-4.
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
// User runs /model kigi-4.
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
// Auth refresh races — clears prefetched/etag.
*mgr.inner.prefetched.write() = None;
*mgr.inner.etag.write() = None;
// Second fetch must preserve user's model.
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert_eq!(mgr.current_model_id().0.as_ref(), "grok-4");
assert_eq!(mgr.current_model_id().0.as_ref(), "kigi-4");
// Config reload with persisted preference.
let mut new_cfg = config::Config::default();
new_cfg.models.default = Some("grok-4".to_string());
new_cfg.models.default = Some("kigi-4".to_string());
mgr.apply_config(new_cfg);
assert_eq!(mgr.current_model_id().0.as_ref(), "grok-4");
assert_eq!(mgr.current_model_id().0.as_ref(), "kigi-4");
}
// ── disk-cache hot-reload (external models_cache.json writes) ────
@@ -2931,7 +2931,7 @@ mod tests {
let auth_method = mgr.inner.fetch_auth.read().cache_auth_method();
cache.persist(
&make_prefetched(&["grok-4.5", "grok-4.3"]),
&make_prefetched(&["kigi-4.5", "kigi-4.3"]),
Some("etag-ext"),
auth_method,
&mgr.cache_origin(),
@@ -2940,8 +2940,8 @@ mod tests {
mgr.reload_from_cache_manager(&cache);
assert!(mgr.has_fetched_real_catalog());
assert!(mgr.models().contains_key("grok-4.5"));
assert!(mgr.models().contains_key("grok-4.3"));
assert!(mgr.models().contains_key("kigi-4.5"));
assert!(mgr.models().contains_key("kigi-4.3"));
assert_eq!(mgr.inner.etag.read().as_deref(), Some("etag-ext"));
}
@@ -3025,9 +3025,9 @@ mod tests {
fn reload_from_disk_cache_skips_identical_catalog_and_adopts_etag() {
let mgr = test_manager();
let cfg = config::Config::default();
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched.clone()), Some("etag-a".into()));
mgr.set_current_model_id(acp::ModelId::new("grok-4"));
mgr.set_current_model_id(acp::ModelId::new("kigi-4"));
let tmp = tempfile::TempDir::new().unwrap();
let cache = test_cache_manager(tmp.path());
@@ -3043,7 +3043,7 @@ mod tests {
assert_eq!(
mgr.current_model_id().0.as_ref(),
"grok-4",
"kigi-4",
"identical catalog must not disturb the user's model"
);
assert_eq!(
@@ -3068,13 +3068,13 @@ mod tests {
auth_method: Some(auth_method),
origin: Some(mgr.cache_origin()),
etag: Some("etag-stale".into()),
models: make_prefetched(&["grok-stale"]),
models: make_prefetched(&["kigi-stale"]),
};
cache.atomic_write(&stale);
mgr.reload_from_cache_manager(&cache);
assert!(!mgr.models().contains_key("grok-stale"));
assert!(!mgr.models().contains_key("kigi-stale"));
assert!(mgr.inner.etag.read().is_none());
}
@@ -3093,7 +3093,7 @@ mod tests {
CacheAuthMethod::Platforms
};
cache.persist(
&make_prefetched(&["grok-other-auth"]),
&make_prefetched(&["kigi-other-auth"]),
Some("etag-x"),
other,
&mgr.cache_origin(),
@@ -3101,7 +3101,7 @@ mod tests {
mgr.reload_from_cache_manager(&cache);
assert!(!mgr.models().contains_key("grok-other-auth"));
assert!(!mgr.models().contains_key("kigi-other-auth"));
}
/// A cache persisted by a process pointed at a *different backend* (env
@@ -3117,7 +3117,7 @@ mod tests {
let cache = test_cache_manager(tmp.path());
let auth_method = mgr.inner.fetch_auth.read().cache_auth_method();
cache.persist(
&make_prefetched(&["grok-other-origin"]),
&make_prefetched(&["kigi-other-origin"]),
Some("etag-y"),
auth_method,
"http://127.0.0.1:49953/v1/models",
@@ -3125,7 +3125,7 @@ mod tests {
mgr.reload_from_cache_manager(&cache);
assert!(!mgr.models().contains_key("grok-other-origin"));
assert!(!mgr.models().contains_key("kigi-other-origin"));
assert!(mgr.inner.etag.read().is_none());
}
@@ -3144,13 +3144,13 @@ mod tests {
auth_method: Some(auth_method),
origin: None,
etag: Some("etag-legacy".into()),
models: make_prefetched(&["grok-legacy"]),
models: make_prefetched(&["kigi-legacy"]),
};
cache.atomic_write(&legacy);
mgr.reload_from_cache_manager(&cache);
assert!(!mgr.models().contains_key("grok-legacy"));
assert!(!mgr.models().contains_key("kigi-legacy"));
}
// ── clear() resets has_fetched_real_catalog ──────────────────────
@@ -3159,9 +3159,9 @@ mod tests {
fn clear_resets_has_fetched_real_catalog() {
let mgr = test_manager();
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
let prefetched = make_prefetched(&["grok-3", "grok-4"]);
let prefetched = make_prefetched(&["kigi-3", "kigi-4"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
assert!(mgr.has_fetched_real_catalog());
@@ -3169,7 +3169,7 @@ mod tests {
assert!(!mgr.has_fetched_real_catalog());
// New identity fetch — resolves default via reselect_default_model.
let prefetched = make_prefetched(&["grok-4.5", "grok-4.3"]);
let prefetched = make_prefetched(&["kigi-4.5", "kigi-4.3"]);
mgr.apply_refresh_result(&cfg, Some(prefetched), None);
let first_available = mgr.available().keys().next().unwrap().clone();
assert_eq!(
@@ -3610,25 +3610,25 @@ mod tests {
#[test]
fn build_prefetched_map_distinct_ids_same_slug() {
let entries = vec![
make_entry_config_with_id(Some("auto"), "grok-build", Some("Auto")),
make_entry_config_with_id(Some("grok-build"), "grok-build", Some("Grok Build")),
make_entry_config_with_id(Some("auto"), "kigi", Some("Auto")),
make_entry_config_with_id(Some("kigi"), "kigi", Some("Kigi")),
make_entry_config_with_id(
Some("grok-composer-2.5-fast"),
"grok-composer-2.5-fast",
Some("Grok Fast"),
Some("kigi-composer-2.5-fast"),
"kigi-composer-2.5-fast",
Some("Kigi Fast"),
),
];
let map = build_prefetched_map(entries);
assert_eq!(map.len(), 3, "all three entries should survive");
assert!(map.contains_key("auto"));
assert!(map.contains_key("grok-build"));
assert!(map.contains_key("grok-composer-2.5-fast"));
assert!(map.contains_key("kigi"));
assert!(map.contains_key("kigi-composer-2.5-fast"));
assert_eq!(
map["auto"].info.model, "grok-build",
"auto entry should still route to grok-build"
map["auto"].info.model, "kigi",
"auto entry should still route to kigi"
);
assert_eq!(map["grok-build"].info.model, "grok-build");
assert_eq!(map["kigi"].info.model, "kigi");
}
/// No id field — falls back to model slug as key.
@@ -3649,13 +3649,13 @@ mod tests {
#[test]
fn build_prefetched_map_duplicate_id_overwrites() {
let entries = vec![
make_entry_config_with_id(Some("grok-build"), "grok-build", Some("First")),
make_entry_config_with_id(Some("grok-build"), "grok-build", Some("Second")),
make_entry_config_with_id(Some("kigi"), "kigi", Some("First")),
make_entry_config_with_id(Some("kigi"), "kigi", Some("Second")),
];
let map = build_prefetched_map(entries);
assert_eq!(map.len(), 1, "duplicate id: second overwrites first");
assert_eq!(map["grok-build"].info.name.as_deref(), Some("Second"));
assert_eq!(map["kigi"].info.name.as_deref(), Some("Second"));
}
/// Regression: resolve_default_model must match by id before scanning
@@ -3664,31 +3664,24 @@ mod tests {
#[test]
fn resolve_default_model_prefers_id_over_model_slug() {
let mut catalog: IndexMap<String, ModelEntry> = IndexMap::new();
catalog.insert(
"auto-grok-build".to_string(),
make_model_entry("grok-build"),
);
catalog.insert("grok-build".to_string(), make_model_entry("grok-build"));
catalog.insert("auto-kigi".to_string(), make_model_entry("kigi"));
catalog.insert("kigi".to_string(), make_model_entry("kigi"));
let mut cfg = config::Config::default();
cfg.models.default = Some("grok-build".to_string());
cfg.models.default = Some("kigi".to_string());
let (key, _, _) = resolve_default_model(&cfg, &catalog, true);
assert_eq!(key, "grok-build", "must match id, not first slug hit");
assert_eq!(key, "kigi", "must match id, not first slug hit");
}
/// No id field — falls back to slug as key.
#[test]
fn build_prefetched_map_none_id_falls_back_to_slug() {
let entries = vec![make_entry_config_with_id(
None,
"grok-build",
Some("Grok Build"),
)];
let entries = vec![make_entry_config_with_id(None, "kigi", Some("Kigi"))];
let map = build_prefetched_map(entries);
assert_eq!(map.len(), 1);
assert!(map.contains_key("grok-build"));
assert!(map.contains_key("kigi"));
}
// ── persisted model id → catalog key (session resume) ─────────────
@@ -3696,94 +3689,79 @@ mod tests {
#[test]
fn resolve_catalog_key_maps_routing_slug_to_config_key() {
let mut models = IndexMap::new();
models.insert(
"enterprise-grok-build".to_string(),
make_model_entry("grok-4.5"),
);
models.insert("grok-4.3".to_string(), make_model_entry("grok-4.3"));
models.insert("enterprise-kigi".to_string(), make_model_entry("kigi-4.5"));
models.insert("kigi-4.3".to_string(), make_model_entry("kigi-4.3"));
let persisted = acp::ModelId::new("grok-4.5");
let persisted = acp::ModelId::new("kigi-4.5");
let key = resolve_catalog_key(&models, &persisted).expect("slug must resolve");
assert_eq!(key.0.as_ref(), "enterprise-grok-build");
assert_eq!(key.0.as_ref(), "enterprise-kigi");
}
#[test]
fn resolve_catalog_key_prefers_exact_key_match() {
let mut models = IndexMap::new();
models.insert("grok-4.5".to_string(), make_model_entry("grok-4.5"));
models.insert("kigi-4.5".to_string(), make_model_entry("kigi-4.5"));
let persisted = acp::ModelId::new("grok-4.5");
let persisted = acp::ModelId::new("kigi-4.5");
let key = resolve_catalog_key(&models, &persisted).expect("exact key must resolve");
assert_eq!(key.0.as_ref(), "grok-4.5");
assert_eq!(key.0.as_ref(), "kigi-4.5");
}
#[test]
fn resolve_catalog_key_last_slug_match_wins() {
let mut models = IndexMap::new();
models.insert(
"default-grok-build".to_string(),
make_model_entry("grok-4.5"),
);
models.insert("user-grok-build".to_string(), make_model_entry("grok-4.5"));
models.insert("default-kigi".to_string(), make_model_entry("kigi-4.5"));
models.insert("user-kigi".to_string(), make_model_entry("kigi-4.5"));
let persisted = acp::ModelId::new("grok-4.5");
let persisted = acp::ModelId::new("kigi-4.5");
let key = resolve_catalog_key(&models, &persisted).expect("slug must resolve");
assert_eq!(key.0.as_ref(), "user-grok-build");
assert_eq!(key.0.as_ref(), "user-kigi");
}
#[test]
fn selectable_catalog_key_for_persisted_none_when_resolved_not_available() {
let mut models = IndexMap::new();
models.insert(
"enterprise-grok-build".to_string(),
make_model_entry("grok-4.5"),
);
models.insert("enterprise-kigi".to_string(), make_model_entry("kigi-4.5"));
let available: IndexMap<_, _> = IndexMap::new();
let persisted = acp::ModelId::new("grok-4.5");
let persisted = acp::ModelId::new("kigi-4.5");
assert!(selectable_catalog_key_for_persisted(&models, &available, &persisted).is_none());
}
#[test]
fn selectable_prefers_available_identity_over_non_selectable_exact_key() {
let mut models = IndexMap::new();
models.insert("grok-build".to_string(), make_model_entry("grok-build"));
models.insert(
"enterprise-grok-build".to_string(),
make_model_entry("grok-build"),
);
models.insert("grok-4.3".to_string(), make_model_entry("grok-4.3"));
models.insert("kigi".to_string(), make_model_entry("kigi"));
models.insert("enterprise-kigi".to_string(), make_model_entry("kigi"));
models.insert("kigi-4.3".to_string(), make_model_entry("kigi-4.3"));
let available = test_available_keys(&["enterprise-grok-build", "grok-4.3"]);
let available = test_available_keys(&["enterprise-kigi", "kigi-4.3"]);
let persisted = acp::ModelId::new("grok-build");
let persisted = acp::ModelId::new("kigi");
assert_eq!(
resolve_catalog_key(&models, &persisted)
.expect("exact key exists")
.0
.as_ref(),
"grok-build"
"kigi"
);
let key = selectable_catalog_key_for_persisted(&models, &available, &persisted)
.expect("must resolve to selectable section");
assert_eq!(key.0.as_ref(), "enterprise-grok-build");
assert_eq!(key.0.as_ref(), "enterprise-kigi");
}
#[test]
fn selectable_matches_routing_slug_when_no_exact_key() {
let mut models = IndexMap::new();
models.insert(
"enterprise-grok-build".to_string(),
make_model_entry("grok-build"),
);
models.insert("grok-4.3".to_string(), make_model_entry("grok-4.3"));
models.insert("enterprise-kigi".to_string(), make_model_entry("kigi"));
models.insert("kigi-4.3".to_string(), make_model_entry("kigi-4.3"));
let available = test_available_keys(&["enterprise-grok-build", "grok-4.3"]);
let available = test_available_keys(&["enterprise-kigi", "kigi-4.3"]);
let persisted = acp::ModelId::new("grok-build");
let persisted = acp::ModelId::new("kigi");
let key = selectable_catalog_key_for_persisted(&models, &available, &persisted)
.expect("slug must resolve to selectable key");
assert_eq!(key.0.as_ref(), "enterprise-grok-build");
assert_eq!(key.0.as_ref(), "enterprise-kigi");
}
/// A persisted *selectable* catalog key binds to itself even when a later
@@ -3791,15 +3769,15 @@ mod tests {
#[test]
fn selectable_prefers_exact_key_over_later_slug_match() {
let mut models = IndexMap::new();
models.insert("grok-build".to_string(), make_model_entry("grok-4.5"));
models.insert("other".to_string(), make_model_entry("grok-build"));
models.insert("kigi".to_string(), make_model_entry("kigi-4.5"));
models.insert("other".to_string(), make_model_entry("kigi"));
let available = test_available_keys(&["grok-build", "other"]);
let available = test_available_keys(&["kigi", "other"]);
let persisted = acp::ModelId::new("grok-build");
let persisted = acp::ModelId::new("kigi");
let key = selectable_catalog_key_for_persisted(&models, &available, &persisted)
.expect("exact selectable key must win");
assert_eq!(key.0.as_ref(), "grok-build");
assert_eq!(key.0.as_ref(), "kigi");
}
fn test_available_keys(keys: &[&str]) -> IndexMap<acp::ModelId, acp::ModelInfo> {
@@ -657,13 +657,13 @@ mod tests {
#[test]
fn parse_openai_format_uses_id_field() {
let value = serde_json::json!(
{ "id" : "grok-3", "object" : "model", "owned_by" : "xai", "context_window" :
{ "id" : "kigi-3", "object" : "model", "owned_by" : "xai", "context_window" :
131072 }
);
let result = parse_remote_model_value(&value, "https://api.x.ai/v1").unwrap();
assert_eq!(result.model, "grok-3");
assert_eq!(result.base_url, "https://api.x.ai/v1");
assert_eq!(result.name.as_deref(), Some("grok-3"));
let result = parse_remote_model_value(&value, "https://byok.example/v1").unwrap();
assert_eq!(result.model, "kigi-3");
assert_eq!(result.base_url, "https://byok.example/v1");
assert_eq!(result.name.as_deref(), Some("kigi-3"));
}
#[test]
fn parse_model_field_takes_priority_over_id() {
@@ -753,14 +753,14 @@ mod tests {
fn parse_reads_reasoning_effort_fields() {
use kigi_sampling_types::ReasoningEffort;
let value = serde_json::json!(
{ "model" : "grok-4.5", "context_window" : 1_000_000,
{ "model" : "kigi-4.5", "context_window" : 1_000_000,
"supports_reasoning_effort" : true, "reasoning_effort" : "high" }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
assert!(result.supports_reasoning_effort);
assert_eq!(result.reasoning_effort, Some(ReasoningEffort::High));
let value = serde_json::json!(
{ "model" : "grok-4.5", "contextWindow" : 1_000_000,
{ "model" : "kigi-4.5", "contextWindow" : 1_000_000,
"supportsReasoningEffort" : true, "reasoningEffort" : "xhigh" }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -775,7 +775,7 @@ mod tests {
fn parse_reads_reasoning_efforts_list() {
use kigi_sampling_types::ReasoningEffort;
let value = serde_json::json!(
{ "model" : "grok-4.5", "context_window" : 1_000_000, "reasoning_efforts" :
{ "model" : "kigi-4.5", "context_window" : 1_000_000, "reasoning_efforts" :
[{ "id" : "deep", "value" : "xhigh", "label" : "Deep" }, { "value" :
"quantum" }, "low",] }
);
@@ -819,7 +819,7 @@ mod tests {
#[test]
fn parse_remote_model_value_no_laziness_detector_block_yields_default() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, }
{ "model" : "kigi-4", "context_window" : 256_000, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
assert_eq!(
@@ -830,7 +830,7 @@ mod tests {
#[test]
fn parse_remote_model_value_parses_camelcase_key() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "max_nudges_per_session" : 2, "idle_threshold_ms" : 12_000,
"min_confidence" : 0.75, }, }
);
@@ -847,7 +847,7 @@ mod tests {
#[test]
fn parse_remote_model_value_parses_snake_case_laziness_detector() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "laziness_detector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "laziness_detector" : {
"enabled" : true, "max_nudges_per_session" : 3, "idle_threshold_ms" : 8_000,
"min_confidence" : 0.6, }, }
);
@@ -864,7 +864,7 @@ mod tests {
#[test]
fn parse_remote_model_value_parses_meta_laziness_detector() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "_meta" : {
{ "model" : "kigi-4", "context_window" : 256_000, "_meta" : {
"lazinessDetector" : { "enabled" : true, "max_nudges_per_session" : 1,
"idle_threshold_ms" : 15_000, "min_confidence" : 0.9, }, }, }
);
@@ -881,7 +881,7 @@ mod tests {
#[test]
fn parse_remote_model_value_partial_block_uses_field_defaults() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, }, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -897,7 +897,7 @@ mod tests {
#[test]
fn parse_remote_model_value_malformed_block_falls_back_to_default() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "max_nudges_per_session" : "abc", }, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -909,7 +909,7 @@ mod tests {
#[test]
fn parse_remote_model_value_non_object_value_falls_back_to_default() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" :
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" :
"not-an-object", }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -921,7 +921,7 @@ mod tests {
#[test]
fn parse_remote_model_value_top_level_camelcase_wins_over_snake_case() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "max_nudges_per_session" : 7, }, "laziness_detector" : {
"enabled" : false, "max_nudges_per_session" : 99, }, }
);
@@ -942,7 +942,7 @@ mod tests {
#[test]
fn parse_remote_model_value_parses_include_reasoning_under_camelcase_wrapper() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "include_reasoning" : false, }, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -951,7 +951,7 @@ mod tests {
#[test]
fn parse_remote_model_value_parses_include_reasoning_under_snake_case_wrapper() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "laziness_detector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "laziness_detector" : {
"enabled" : true, "include_reasoning" : true, }, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -960,7 +960,7 @@ mod tests {
#[test]
fn parse_remote_model_value_omitted_include_reasoning_defaults_to_none() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "max_nudges_per_session" : 2, }, }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -972,7 +972,7 @@ mod tests {
#[test]
fn parse_remote_model_value_top_level_wins_over_meta() {
let value = serde_json::json!(
{ "model" : "grok-4", "context_window" : 256_000, "lazinessDetector" : {
{ "model" : "kigi-4", "context_window" : 256_000, "lazinessDetector" : {
"enabled" : true, "max_nudges_per_session" : 5, }, "_meta" : {
"lazinessDetector" : { "enabled" : false, "max_nudges_per_session" : 99, },
}, }
@@ -990,19 +990,19 @@ mod tests {
#[test]
fn parse_reads_show_model_fingerprint_field() {
let value = serde_json::json!(
{ "model" : "grok-build", "context_window" : 256_000,
{ "model" : "kigi", "context_window" : 256_000,
"show_model_fingerprint" : true }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
assert!(result.show_model_fingerprint);
let value = serde_json::json!(
{ "model" : "grok-build", "contextWindow" : 256_000, "showModelFingerprint" :
{ "model" : "kigi", "contextWindow" : 256_000, "showModelFingerprint" :
true }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
assert!(result.show_model_fingerprint);
let value = serde_json::json!(
{ "model" : "grok-build", "context_window" : 256_000, "_meta" : {
{ "model" : "kigi", "context_window" : 256_000, "_meta" : {
"showModelFingerprint" : true } }
);
let result = parse_remote_model_value(&value, "https://default.url").unwrap();
@@ -1087,10 +1087,13 @@ mod tests {
fn list_url_derived_from_base_url() {
let ep = endpoints(
"https://proxy.kigi.com/v1",
Some("https://api.x.ai/v1"),
Some("https://byok.example/v1"),
None,
);
assert_eq!(ep.resolve_models_list_url(), "https://api.x.ai/v1/models");
assert_eq!(
ep.resolve_models_list_url(),
"https://byok.example/v1/models"
);
}
#[test]
fn list_url_explicit_overrides_derivation() {
@@ -65,10 +65,10 @@ impl acp::Agent for MvpAgent {
}
if client_type == ClientType::Generic {
match client_identifier.as_deref() {
Some("grok-web") => client_type = ClientType::GrokWeb,
Some("kigi-web") => client_type = ClientType::KigiWeb,
Some("nebula") => client_type = ClientType::Nebula,
Some("grok-code-extension") => client_type = ClientType::Extension,
Some("grok-desktop") => client_type = ClientType::Desktop,
Some("kigi-code-extension") => client_type = ClientType::Extension,
Some("kigi-desktop") => client_type = ClientType::Desktop,
_ => {}
}
}
@@ -80,7 +80,7 @@ impl acp::Agent for MvpAgent {
code_nav_enabled, client_type = ? client_type, event =
"code_nav_capability_parsed",
"code-nav capability initialized from initialize request; \
index will start lazily on first x.ai/code/* request if eligible"
index will start lazily on first kigi/code/* request if eligible"
);
let interactive_trust_client = Self::parse_interactive_trust_capability(
&arguments,
@@ -279,7 +279,7 @@ impl acp::Agent for MvpAgent {
.load_session(true)
.meta(
serde_json::json!(
{ "x.ai/fs_notify" : true, "x.ai/hooks" : { "blockingEvents"
{ "kigi/fs_notify" : true, "kigi/hooks" : { "blockingEvents"
: [kigi_hooks::event::HookEventName::PreToolUse],
"decisions" : ["deny"], }, }
)
@@ -297,7 +297,7 @@ impl acp::Agent for MvpAgent {
.meta({
let metadata = parse_json_object_env("KIGI_AGENT_METADATA");
serde_json::json!(
{ "grokShell" : true, "defaultAuthMethodId" :
{ "kigiShell" : true, "defaultAuthMethodId" :
default_auth_method_id_wire, (kigi_mcp::wire::MCP_SDK) :
true, (SESSION_PLUGIN_DIRS_CAPABILITY_KEY) : true,
"currentWorkingDirectory" : current_working_directory
@@ -372,7 +372,7 @@ impl acp::Agent for MvpAgent {
return self
.authenticate(
acp::AuthenticateRequest::new(
acp::AuthMethodId::new(auth_method::KIGI_COM_METHOD_ID),
acp::AuthMethodId::new(auth_method::KIMI_CODE_METHOD_ID),
)
.meta(arguments.meta),
)
@@ -425,7 +425,7 @@ impl acp::Agent for MvpAgent {
emit_login_span(true, "cached_token", uid.as_deref(), None);
Ok(self.auth_response_with_meta())
}
auth_method::KIGI_COM_METHOD_ID => {
auth_method::KIMI_CODE_METHOD_ID => {
let kimi_ctx = self.auth_manager.kimi_code_config().clone();
let auth_meta = AuthRequestMeta::from_json(arguments.meta.as_ref());
tracing::info!(
@@ -488,10 +488,10 @@ impl acp::Agent for MvpAgent {
let mut sampling_config = self.sampling_config.borrow_mut();
sampling_config.api_key = Some(auth.key.clone());
tracing::debug!(
"auth: grok.com/oidc handler set api_key (SessionToken)"
"auth: kimi.com/oidc handler set api_key (SessionToken)"
);
kigi_log::unified_log::debug(
"auth: grok.com/oidc handler set api_key (SessionToken)",
"auth: kimi.com/oidc handler set api_key (SessionToken)",
None,
None,
);
@@ -827,7 +827,7 @@ impl acp::Agent for MvpAgent {
Some(serde_json::json!({ "cwd" : cwd.as_str() })),
);
let models = if is_chat_kind {
// The grok.com chat-mode model picker was removed with the xAI
// The kimi.com chat-mode model picker was removed with the xAI
// proxy; a chat-kind session has no managed catalog to offer.
chat_new_session_model_state(
acp::SessionModelState::new(acp::ModelId::from(String::new()), Vec::new()),
@@ -846,8 +846,8 @@ impl acp::Agent for MvpAgent {
feedback_enabled, }
);
if let Some(obj) = meta.as_object_mut() {
obj.insert("x.ai/sessionConfig".to_string(), session_config_value);
obj.insert("x.ai/sessionDetail".to_string(), session_detail_value);
obj.insert("kigi/sessionConfig".to_string(), session_config_value);
obj.insert("kigi/sessionDetail".to_string(), session_detail_value);
}
Ok(
acp::NewSessionResponse::new(session_id)
@@ -874,12 +874,12 @@ impl acp::Agent for MvpAgent {
let persist_data = arguments
.meta
.as_ref()
.and_then(|m| m.get("x.ai/persist"))
.and_then(|m| m.get("kigi/persist"))
.cloned();
let target_client_id = arguments
.meta
.as_ref()
.and_then(|m| m.get("x.ai/leaderClientId"))
.and_then(|m| m.get("kigi/leaderClientId"))
.cloned();
let acp::LoadSessionRequest {
session_id,
@@ -1010,7 +1010,7 @@ impl acp::Agent for MvpAgent {
);
let restore_code_requested = request_meta
.as_ref()
.and_then(|m| m.get("x.ai/restore_code"))
.and_then(|m| m.get("kigi/restore_code"))
.and_then(|v| v.as_bool())
.unwrap_or(self.restore_code);
let registry_client_for_restore = self.session_registry_client();
@@ -1030,7 +1030,7 @@ impl acp::Agent for MvpAgent {
target : kigi_workspace::session::git::RESTORE_CODE_LOG, session_id =
% session_id.0, supplied_cwd = % cwd.as_str(), persisted_cwd = % summary
.info.cwd, target_sha = % target_sha,
"restore_code: skipping session HEAD checkout — supplied cwd is neither a grok worktree nor the session's persisted cwd (refusing to detach the source repo)"
"restore_code: skipping session HEAD checkout — supplied cwd is neither a kigi worktree nor the session's persisted cwd (refusing to detach the source repo)"
);
kigi_log::unified_log::warn(
"restore_code: skipped session HEAD checkout (unsafe cwd)",
@@ -1075,7 +1075,7 @@ impl acp::Agent for MvpAgent {
let load_envrc = {
let skip_envrc = request_meta
.as_ref()
.and_then(|m| m.get("x.ai/skip_envrc"))
.and_then(|m| m.get("kigi/skip_envrc"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if skip_envrc {
@@ -1156,7 +1156,7 @@ impl acp::Agent for MvpAgent {
);
let prompt_display_cwd = request_meta
.as_ref()
.and_then(|m| m.get("x.ai/display_cwd"))
.and_then(|m| m.get("kigi/display_cwd"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| summary.prompt_display_cwd.clone());
@@ -1462,7 +1462,7 @@ impl acp::Agent for MvpAgent {
let mut response_meta_map = serde_json::Map::new();
response_meta_map.insert("sessionId".to_string(), serde_json::json!(session_id));
if let Some(persist) = persist_data {
response_meta_map.insert("x.ai/persist".to_string(), persist);
response_meta_map.insert("kigi/persist".to_string(), persist);
}
let session_cwd = self
.sessions
@@ -1517,7 +1517,7 @@ impl acp::Agent for MvpAgent {
{
response_meta_map
.insert(
"x.ai/runningPromptId".to_string(),
"kigi/runningPromptId".to_string(),
serde_json::json!(running_prompt_id),
);
}
@@ -1529,8 +1529,8 @@ impl acp::Agent for MvpAgent {
summary.display_title_opt(),
&model_state,
);
response_meta_map.insert("x.ai/sessionConfig".to_string(), session_config_value);
response_meta_map.insert("x.ai/sessionDetail".to_string(), session_detail_value);
response_meta_map.insert("kigi/sessionConfig".to_string(), session_config_value);
response_meta_map.insert("kigi/sessionDetail".to_string(), session_detail_value);
let response_meta = serde_json::Value::Object(response_meta_map);
kigi_log::unified_log::info(
"session loaded",
@@ -1846,7 +1846,7 @@ impl acp::Agent for MvpAgent {
self.gateway
.forward_fire_and_forget(
acp::ExtNotification::new(
"x.ai/session/prompt_complete",
"kigi/session/prompt_complete",
params.into(),
),
);
@@ -2045,116 +2045,116 @@ impl acp::Agent for MvpAgent {
let mut backend_no_bridge_err: Option<acp::Error> = None;
let method = args.method.clone();
let result = match method.as_ref() {
"x.ai/getApiKey" | "x.ai/setApiKey" => {
"kigi/getApiKey" | "kigi/setApiKey" => {
crate::extensions::auth::handle(self, &args).await
}
"x.ai/session/info" | "x.ai/session/close" | "x.ai/session/list"
| "x.ai/sessions/list" => {
"kigi/session/info" | "kigi/session/close" | "kigi/session/list"
| "kigi/sessions/list" => {
crate::agent::handlers::session::handle(self, &args).await
}
"x.ai/session/updates" => {
"kigi/session/updates" => {
crate::extensions::session_updates::handle(&args, &self.gateway).await
}
"x.ai/session/load_history" => {
"kigi/session/load_history" => {
crate::extensions::chat_conversation_history::handle(self, &args).await
}
"x.ai/session/search" => {
"kigi/session/search" => {
crate::extensions::session_search::handle(&args).await
}
"x.ai/session/resolve_local_for_worktree_resume"
| "x.ai/session/rehydrate" => {
"kigi/session/resolve_local_for_worktree_resume"
| "kigi/session/rehydrate" => {
let ops = self.resolve_workspace_ops()?;
crate::extensions::worktree::handle(self, &ops, &args).await
}
"x.ai/session/rename" | "x.ai/session/delete"
| "x.ai/session/update_mcp_servers" | "x.ai/session/fork"
| "x.ai/internal/reload_all_mcp_servers"
| "x.ai/internal/reload_project_mcp_servers" | "x.ai/internal/reload_skills"
| "x.ai/internal/reload_models" | "x.ai/internal/reload_models_cache"
| "x.ai/plugins/reload"
| "x.ai/commands/list" => {
"kigi/session/rename" | "kigi/session/delete"
| "kigi/session/update_mcp_servers" | "kigi/session/fork"
| "kigi/internal/reload_all_mcp_servers"
| "kigi/internal/reload_project_mcp_servers" | "kigi/internal/reload_skills"
| "kigi/internal/reload_models" | "kigi/internal/reload_models_cache"
| "kigi/plugins/reload"
| "kigi/commands/list" => {
crate::extensions::session_admin::handle(self, &args).await
}
"x.ai/session/repair" => crate::extensions::repair::handle(self, &args).await,
"x.ai/billing" => crate::extensions::billing::handle(self, &args).await,
"x.ai/memory/flush" | "x.ai/memory/rewrite" => {
"kigi/session/repair" => crate::extensions::repair::handle(self, &args).await,
"kigi/billing" => crate::extensions::billing::handle(self, &args).await,
"kigi/memory/flush" | "kigi/memory/rewrite" => {
crate::extensions::memory::handle(self, &args).await
}
"x.ai/skills/refresh-baseline" => {
"kigi/skills/refresh-baseline" => {
self.refresh_skill_baseline_for_all_sessions();
crate::extensions::to_ext_response(
Ok(serde_json::json!({ "ok" : true })),
)
}
"x.ai/interject" => crate::extensions::interject::handle(self, &args).await,
"x.ai/feedback" | "x.ai/feedback/dismiss" | "x.ai/btw" => {
"kigi/interject" => crate::extensions::interject::handle(self, &args).await,
"kigi/feedback" | "kigi/feedback/dismiss" | "kigi/btw" => {
crate::extensions::feedback::handle(self, &args).await
}
"x.ai/recap" => crate::extensions::recap::handle(self, &args).await,
"x.ai/rollout/survey" => {
"kigi/recap" => crate::extensions::recap::handle(self, &args).await,
"kigi/rollout/survey" => {
crate::extensions::rollout::handle(self, &args).await
}
"x.ai/prompt_history" => {
"kigi/prompt_history" => {
crate::extensions::prompt_history::handle(self, &args).await
}
"x.ai/suggest" => crate::extensions::suggest::handle(self, &args).await,
"x.ai/suggestPrompt" => crate::extensions::suggest::handle(self, &args).await,
s if s.starts_with("x.ai/auth/") => {
"kigi/suggest" => crate::extensions::suggest::handle(self, &args).await,
"kigi/suggestPrompt" => crate::extensions::suggest::handle(self, &args).await,
s if s.starts_with("kigi/auth/") => {
crate::extensions::auth::handle(self, &args).await
}
s if s.starts_with("x.ai/session_summaries/") => {
s if s.starts_with("kigi/session_summaries/") => {
crate::agent::handlers::session::handle(self, &args).await
}
s if s.starts_with("x.ai/git/worktree/") => {
s if s.starts_with("kigi/git/worktree/") => {
let ops = self.resolve_workspace_ops()?;
crate::extensions::worktree::handle(self, &ops, &args).await
}
s if s.starts_with("x.ai/git/") => {
s if s.starts_with("kigi/git/") => {
let ops = self.resolve_workspace_ops()?;
crate::extensions::git::handle(self, &ops, &args).await
}
s if s.starts_with("x.ai/compact_conversation") => {
s if s.starts_with("kigi/compact_conversation") => {
crate::extensions::memory::handle(self, &args).await
}
s if s.starts_with("x.ai/plugins/") => {
s if s.starts_with("kigi/plugins/") => {
crate::extensions::plugins::handle(self, &args).await
}
s if s.starts_with("x.ai/hooks/") => {
s if s.starts_with("kigi/hooks/") => {
crate::extensions::hooks::handle(self, &args).await
}
s if s.starts_with("x.ai/hunk-tracker/") => {
s if s.starts_with("kigi/hunk-tracker/") => {
let ops = self.resolve_workspace_ops()?;
crate::extensions::hunk_tracker::handle(self, &ops, &args).await
}
s if s.starts_with("x.ai/pr/") => {
s if s.starts_with("kigi/pr/") => {
crate::extensions::pr::handle(self, &args).await
}
s if s.starts_with(crate::extensions::mcp::mcp_methods::PREFIX) => {
crate::extensions::mcp::handle(self, &args).await
}
s if s.starts_with("x.ai/task/") => {
s if s.starts_with("kigi/task/") => {
crate::extensions::task::handle(self, &args).await
}
s if s.starts_with("x.ai/scheduler/") => {
s if s.starts_with("kigi/scheduler/") => {
crate::extensions::task::handle_scheduler(self, &args).await
}
s if s.starts_with("x.ai/subagent/") => {
s if s.starts_with("kigi/subagent/") => {
crate::extensions::task::handle_subagent(self, &args).await
}
s if s.starts_with("x.ai/terminal/") => {
s if s.starts_with("kigi/terminal/") => {
crate::extensions::terminal::handle(self, &args).await
}
s if crate::extensions::fs::is_fs_method(s) => {
crate::extensions::fs::handle(self, &args).await
}
s if s.starts_with("x.ai/search/") => {
s if s.starts_with("kigi/search/") => {
crate::extensions::search::handle(self, &args).await
}
s if s.starts_with("x.ai/code/") => {
s if s.starts_with("kigi/code/") => {
let ops = self.resolve_workspace_ops()?;
crate::extensions::code_nav::handle(self, &ops, &args).await
}
s if s.starts_with("x.ai/skills/") => {
s if s.starts_with("kigi/skills/") => {
let compat = self.cfg.borrow().compat_resolved;
crate::extensions::skills::handle(
&args,
@@ -2163,13 +2163,13 @@ impl acp::Agent for MvpAgent {
)
.await
}
s if s.starts_with("x.ai/review") => {
s if s.starts_with("kigi/review") => {
crate::extensions::feedback::handle(self, &args).await
}
s if s.starts_with("x.ai/debug/") => {
s if s.starts_with("kigi/debug/") => {
crate::extensions::debug::handle(self, &args).await
}
s if s.starts_with("x.ai/rewind") => {
s if s.starts_with("kigi/rewind") => {
crate::extensions::rewind::handle(self, &args).await
}
other => {
@@ -2193,7 +2193,7 @@ impl acp::Agent for MvpAgent {
args: acp::ExtNotification,
) -> Result<(), acp::Error> {
tracing::info!("Received extension notification: method={}", args.method);
if args.method.as_ref() == "x.ai/yolo_mode_changed"
if args.method.as_ref() == "kigi/yolo_mode_changed"
&& let Ok(params) = serde_json::from_str::<
serde_json::Value,
>(args.params.get())
@@ -2257,7 +2257,7 @@ impl acp::Agent for MvpAgent {
);
}
}
if args.method.as_ref() == "x.ai/permissions/reset" {
if args.method.as_ref() == "kigi/permissions/reset" {
let sessions = self.sessions.borrow();
let updated = sessions
.values()
@@ -2273,10 +2273,10 @@ impl acp::Agent for MvpAgent {
"Permission state reset for matching sessions"
);
}
if args.method.as_ref() == "x.ai/internal/evict_sessions" {
if args.method.as_ref() == "kigi/internal/evict_sessions" {
self.handle_evict_sessions(&args.params).await;
}
if args.method.as_ref() == "x.ai/toggle_plan_mode"
if args.method.as_ref() == "kigi/toggle_plan_mode"
&& let Ok(params) = serde_json::from_str::<
serde_json::Value,
>(args.params.get())
@@ -2317,8 +2317,8 @@ impl acp::Agent for MvpAgent {
}
}
if matches!(
args.method.as_ref(), "x.ai/queue/remove" | "x.ai/queue/reorder" |
"x.ai/queue/clear" | "x.ai/queue/edit" | "x.ai/queue/interject"
args.method.as_ref(), "kigi/queue/remove" | "kigi/queue/reorder" |
"kigi/queue/clear" | "kigi/queue/edit" | "kigi/queue/interject"
)
&& let Ok(params) = serde_json::from_str::<
serde_json::Value,
@@ -2358,14 +2358,14 @@ impl acp::Agent for MvpAgent {
);
}
}
if args.method.as_ref() == "x.ai/terminal/pty/input"
if args.method.as_ref() == "kigi/terminal/pty/input"
&& let Ok(params) = serde_json::from_str::<
serde_json::Value,
>(args.params.get())
{
crate::extensions::terminal::handle_pty_input(&params).await;
}
if args.method.as_ref() == "_x.ai/session/update" {
if args.method.as_ref() == "_kigi/session/update" {
if let Ok(notification) = serde_json::from_str::<
SessionNotification,
>(args.params.get()) {
@@ -2393,7 +2393,7 @@ impl acp::Agent for MvpAgent {
tracing::warn!("Failed to parse xAI session notification params");
}
}
if args.method.as_ref() == "x.ai/telemetry/non_git_decision" {
if args.method.as_ref() == "kigi/telemetry/non_git_decision" {
#[derive(serde::Deserialize)]
struct NonGitDecisionParams {
decision: String,
@@ -2412,7 +2412,7 @@ impl acp::Agent for MvpAgent {
tracing::warn!("Failed to parse non_git_decision telemetry params");
}
}
if args.method.as_ref() == "x.ai/telemetry/multi_agent_followup" {
if args.method.as_ref() == "kigi/telemetry/multi_agent_followup" {
#[derive(serde::Deserialize)]
struct MultiAgentFollowupParams {
preferred_agent_label: char,
@@ -2433,7 +2433,7 @@ impl acp::Agent for MvpAgent {
tracing::warn!("Failed to parse multi-agent followup telemetry params");
}
}
if args.method.as_ref() == "x.ai/telemetry/multi_agent_apply" {
if args.method.as_ref() == "kigi/telemetry/multi_agent_apply" {
#[derive(serde::Deserialize)]
struct MultiAgentApplyParams {
applied_agent_label: char,
@@ -2454,7 +2454,7 @@ impl acp::Agent for MvpAgent {
tracing::warn!("Failed to parse multi-agent apply telemetry params");
}
}
if args.method.as_ref() == "x.ai/telemetry/multi_agent_discard" {
if args.method.as_ref() == "kigi/telemetry/multi_agent_discard" {
#[derive(serde::Deserialize)]
struct MultiAgentDiscardParams {
/// (label, session_id, model_id)
@@ -108,7 +108,7 @@ impl MvpAgent {
}
/// Resolve folder trust and load launch-dir MCP configs after `initialize`
/// returns. The walks are synchronous and expensive in large monorepos; they
/// must not block the ACP response (grok-desktop sends `initialize` immediately).
/// must not block the ACP response (kigi-desktop sends `initialize` immediately).
pub(super) fn spawn_initialize_launch_mcp_setup(&self) {
let cwd = self.launch_cwd.clone();
let compat = self.cfg.borrow().compat_resolved;
@@ -151,7 +151,7 @@ impl MvpAgent {
/// Build the launch-dir plugin registry snapshot on first use.
///
/// Boot-time discovery was deferred past ACP `initialize` (the cwd→git-root
/// plus user/marketplace walks stalled grok-desktop's first `initialize`),
/// plus user/marketplace walks stalled kigi-desktop's first `initialize`),
/// leaving `plugin_registry_handle` empty. That shared snapshot still backs
/// the launch-dir plugin MCP/LSP merges read in `resolve_mcp_servers` and
/// the session LSP build, so populate it lazily — off the `initialize`
@@ -292,7 +292,7 @@ impl MvpAgent {
}
/// Pre-session command availability snapshot.
///
/// Used by the `x.ai/commands/list` ext method and the
/// Used by the `kigi/commands/list` ext method and the
/// `InitializeResponse._meta` path (`builtin_commands()`), both of
/// which fire before any session exists. The eventual agent's toolset
/// is unknown (depends on the model the user picks), so we fail-closed
@@ -346,7 +346,7 @@ impl MvpAgent {
///
/// Deferred past ACP wiring so `initialize` can respond before folder-trust
/// scans and `WorkspaceHandle::new_minimal` run (same boot stall as plugin
/// discovery on grok-desktop Windows).
/// discovery on kigi-desktop Windows).
fn ensure_local_workspace_ops(
&self,
) -> Result<kigi_workspace::WorkspaceOps, acp::Error> {
@@ -404,7 +404,7 @@ impl MvpAgent {
/// Returns `SessionToken` when EITHER:
/// - `auth_manager` currently has a live (non-expired) credential, OR
/// - the active auth method is session-based (`cached_token`,
/// `grok.com`, `oidc`) -- even if the in-memory token is currently
/// `kimi-code`, `oidc`) -- even if the in-memory token is currently
/// expired or missing.
///
/// Returns `ApiKey` only when the auth method is BYOK (`xai.api_key`) or
@@ -493,7 +493,7 @@ impl MvpAgent {
let _ = self
.gateway
.ext_notification(
acp::ExtNotification::new("x.ai/session_notification", params.into()),
acp::ExtNotification::new("kigi/session_notification", params.into()),
)
.await;
}
@@ -621,8 +621,8 @@ impl MvpAgent {
/// Build deploy-service config. The tool talks directly to the deployer service.
pub(super) fn prepare_app_builder_deployer_config(
&self,
) -> kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig {
use kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig;
) -> kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig {
use kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig;
AppBuilderDeployerConfig::Disabled
}
/// Web search config (PRD F5). The Kimi search service exists only on
@@ -677,8 +677,8 @@ impl MvpAgent {
/// OAuth sessions (PRD F5) > None (local pipeline only)
pub(super) fn prepare_web_fetch_config(
&self,
) -> kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig {
use kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig;
) -> kigi_tools::implementations::kigi::web_fetch::WebFetchConfig {
use kigi_tools::implementations::kigi::web_fetch::WebFetchConfig;
let cfg = self.cfg.borrow();
if cfg.disable_web_search {
return WebFetchConfig::Disabled;
@@ -816,7 +816,7 @@ impl MvpAgent {
subagent_event_tx,
subagent_event_rx: RefCell::new(Some(subagent_event_rx)),
subagent_coordinator: RefCell::new(subagent_coordinator),
monitor_event_buffer: kigi_tools::implementations::grok_build::task::types::MonitorEventBuffer::default(),
monitor_event_buffer: kigi_tools::implementations::kigi::task::types::MonitorEventBuffer::default(),
workspace_ops: RefCell::new(None),
require_gateway_sessions: Rc::new(
RefCell::new(std::collections::HashSet::new()),
@@ -833,7 +833,7 @@ impl MvpAgent {
instance.auth_manager.configure_refresher();
instance
}
/// Handle `x.ai/internal/evict_sessions` — the leader server tells us a
/// Handle `kigi/internal/evict_sessions` — the leader server tells us a
/// client disconnected and these sessions lost their IPC owner.
///
/// **This is the no-evict keystone.** A disconnect must
@@ -1082,12 +1082,12 @@ impl MvpAgent {
}
}
/// Cancel a subagent by id, returning a typed outcome that backs the pager's
/// `x.ai/subagent/cancel`. Active/pending → cancelled (a finish follows);
/// `kigi/subagent/cancel`. Active/pending → cancelled (a finish follows);
/// already-finished → its terminal status; unknown id → `NotFound`.
pub fn cancel_subagent(
&self,
subagent_id: &str,
) -> kigi_tools::implementations::grok_build::task::types::SubagentCancelOutcome {
) -> kigi_tools::implementations::kigi::task::types::SubagentCancelOutcome {
self.subagent_coordinator.borrow_mut().cancel_with_outcome(subagent_id)
}
/// List running subagent seeds for a given parent session.
@@ -1183,7 +1183,7 @@ impl MvpAgent {
let sessions = self.sessions.borrow();
sessions.get(session_id).cloned()
}
/// Get hooks list for a session (for `x.ai/hooks/list` extension).
/// Get hooks list for a session (for `kigi/hooks/list` extension).
pub async fn list_hooks(
&self,
session_id: &acp::SessionId,
@@ -1191,7 +1191,7 @@ impl MvpAgent {
let handle = self.get_session_handle(session_id)?;
handle.get_hooks_list().await
}
/// Execute a hooks management action (for `x.ai/hooks/action`).
/// Execute a hooks management action (for `kigi/hooks/action`).
pub async fn execute_hooks_action(
&self,
session_id: &acp::SessionId,
@@ -1207,7 +1207,7 @@ impl MvpAgent {
let handle = self.get_session_handle(session_id)?;
handle.execute_hooks_action(action).await
}
/// Execute a plugins management action (for `x.ai/plugins/action`).
/// Execute a plugins management action (for `kigi/plugins/action`).
pub async fn execute_plugins_action(
&self,
session_id: &acp::SessionId,
@@ -1225,7 +1225,7 @@ impl MvpAgent {
}
outcome
}
/// Get a snapshot of the shared plugin registry (for `x.ai/plugins/list`).
/// Get a snapshot of the shared plugin registry (for `kigi/plugins/list`).
pub fn plugin_registry_snapshot(
&self,
) -> Option<std::sync::Arc<kigi_agent::plugins::PluginRegistry>> {
@@ -1352,7 +1352,7 @@ impl MvpAgent {
current_effort,
)
}
/// Build the `x.ai/sessionConfig` and `x.ai/sessionDetail` `_meta` values
/// Build the `kigi/sessionConfig` and `kigi/sessionDetail` `_meta` values
/// shared by `new_session` and `load_session`, returned as
/// `(sessionConfig, sessionDetail)`. Keeping both response paths on this one
/// builder stops them drifting.
@@ -1364,7 +1364,7 @@ impl MvpAgent {
model_state: &acp::SessionModelState,
) -> (serde_json::Value, serde_json::Value) {
let config_options = self.session_config_options(Some(session_id), model_state);
let detail = session_config::GrokSessionDetail::build(
let detail = session_config::KigiSessionDetail::build(
session_id.0.to_string(),
cwd,
model_state.current_model_id.0.to_string(),
@@ -1455,13 +1455,13 @@ impl MvpAgent {
model_agent_type: Option<&str>,
) -> kigi_agent::AgentDefinition {
use kigi_agent::AgentDefinition;
let grok_agent_env_set = std::env::var("KIGI_AGENT")
let kigi_agent_env_set = std::env::var("KIGI_AGENT")
.ok()
.is_some_and(|s| !s.trim().is_empty());
let config_agent_explicitly_set = agent_config.name.is_some();
let model_requires_strict_harness = model_agent_type
.is_some_and(kigi_agent::config::is_strict_harness_agent_type);
if !grok_agent_env_set && !config_agent_explicitly_set
if !kigi_agent_env_set && !config_agent_explicitly_set
&& model_requires_strict_harness && let Some(required) = model_agent_type
&& let Some(def) = kigi_agent::discovery::by_name_in_cwd(required, cwd)
{
@@ -1527,8 +1527,8 @@ impl MvpAgent {
let agent_name = std::env::var("KIGI_AGENT").ok();
let resolved = match agent_name.as_deref() {
Some("browser-use") | Some("browser_use") => AgentDefinition::browser_use(),
Some("grok-build-concise") | Some("grok_build_concise") => {
AgentDefinition::grok_build_concise()
Some("kigi-concise") | Some("kigi_concise") => {
AgentDefinition::kigi_concise()
}
Some(path) if std::path::Path::new(path).is_absolute() => {
match AgentDefinition::from_file(path) {
@@ -1538,17 +1538,17 @@ impl MvpAgent {
path = path, error = % e,
"Failed to load agent definition from file, falling back to default"
);
AgentDefinition::grok_build_plan()
AgentDefinition::kigi_plan()
}
}
}
Some(name) => {
kigi_agent::discovery::by_name_in_cwd(name, cwd)
.unwrap_or_else(AgentDefinition::grok_build_plan)
.unwrap_or_else(AgentDefinition::kigi_plan)
}
None => AgentDefinition::grok_build_plan(),
None => AgentDefinition::kigi_plan(),
};
if !grok_agent_env_set && !config_agent_explicitly_set
if !kigi_agent_env_set && !config_agent_explicitly_set
&& model_requires_strict_harness && let Some(required) = model_agent_type
&& resolved.name != required
{
@@ -1638,7 +1638,7 @@ impl MvpAgent {
.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/fs_notify"))
.and_then(|m| m.get("kigi/fs_notify"))
.and_then(|v| {
use crate::session::{ClientFsConfig, ClientFsMode};
use kigi_fsnotify::FsConfig;
@@ -1712,7 +1712,7 @@ impl MvpAgent {
.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/hunkTracker"))
.and_then(|m| m.get("kigi/hunkTracker"))
.and_then(|v| v.get("mode"))
.and_then(|v| v.as_str()),
);
@@ -1720,14 +1720,14 @@ impl MvpAgent {
.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/incrementalBashOutput"))
.and_then(|m| m.get("kigi/incrementalBashOutput"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let no_color = init
.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/bashOutputNoColor"))
.and_then(|m| m.get("kigi/bashOutputNoColor"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let hunk_tracking_enabled = hunk_plan.enabled();
@@ -2200,7 +2200,7 @@ impl MvpAgent {
.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/gitHeadChanged"))
.and_then(|m| m.get("kigi/gitHeadChanged"))
.and_then(|v| v.as_bool());
let fs_watch_caps = crate::session::fs_watch::FsWatchCapabilities::resolve(crate::session::fs_watch::CapabilityInputs {
client_notify: fs_notify_config.is_some(),
@@ -4,13 +4,13 @@
use super::*;
impl MvpAgent {
/// Parse the `x.ai/codeNavigation.enabled` capability from an initialize
/// Parse the `kigi/codeNavigation.enabled` capability from an initialize
/// request. Returns `false` if the field is absent or not `true`.
pub(crate) fn parse_code_nav_capability(init: &acp::InitializeRequest) -> bool {
init.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/codeNavigation"))
.and_then(|m| m.get("kigi/codeNavigation"))
.and_then(|v| v.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
@@ -56,7 +56,7 @@ impl MvpAgent {
use crate::agent::config::CodebaseIndexingSetting;
// Gate 1: client type
if !matches!(client_type, ClientType::GrokWeb) {
if !matches!(client_type, ClientType::KigiWeb) {
tracing::info!(
client_type = ?client_type,
gate = "client_type",
@@ -71,7 +71,7 @@ impl MvpAgent {
tracing::info!(
gate = "capability",
skip_reason = "capability_not_advertised",
"code-nav eligibility check: skipping (x.ai/codeNavigation.enabled not advertised)"
"code-nav eligibility check: skipping (kigi/codeNavigation.enabled not advertised)"
);
return Err(CodeNavEligibility::CapabilityNotAdvertised);
}
@@ -138,7 +138,7 @@ impl MvpAgent {
Some(sid) => sid,
// No session_id: per-client capability cannot be determined without a
// session. Reject with SessionRequired rather than fall back to shared
// global state. Callers must provide sessionId for x.ai/code/* requests.
// global state. Callers must provide sessionId for kigi/code/* requests.
None => return Err(CodeNavEligibility::SessionRequired),
};
@@ -1,10 +1,10 @@
//! Interactive folder-trust prompt: a dormant agent→GUI-client ACP round-trip
//! (`x.ai/folder_trust/request`) that asks a GUI client (grok-desktop) to decide
//! (`kigi/folder_trust/request`) that asks a GUI client (kigi-desktop) to decide
//! trust for an untrusted-with-configs workspace, then grants + reloads the
//! now-trusted project servers without a restart.
//!
//! DORMANT in production: it only fires when the connected client advertised
//! `x.ai/folderTrust.interactive` AND the folder-trust feature flag is on AND the
//! `kigi/folderTrust.interactive` AND the folder-trust feature flag is on AND the
//! verdict is [`kigi_workspace::folder_trust::TrustOutcome::Prompt`]. No
//! client advertises the capability until the desktop UI ships — so this is
//! inert by default even with the feature flag on. The TUI/headless clients never
@@ -29,7 +29,7 @@ use super::*;
/// the whole connection lifetime.
const TRUST_PROMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60);
/// ACP `x.ai/folder_trust/request` payload (agent → GUI client). Serialized as
/// ACP `kigi/folder_trust/request` payload (agent → GUI client). Serialized as
/// `camelCase` for the ACP JSON-RPC wire format.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -62,21 +62,21 @@ pub(crate) enum FolderTrustOutcome {
Reject,
}
/// ACP `x.ai/folder_trust/request` response (GUI client → agent).
/// ACP `kigi/folder_trust/request` response (GUI client → agent).
#[derive(Debug, Clone, serde::Deserialize)]
pub(crate) struct FolderTrustResponse {
pub outcome: FolderTrustOutcome,
}
impl MvpAgent {
/// Parse the `x.ai/folderTrust.interactive` capability from an initialize
/// Parse the `kigi/folderTrust.interactive` capability from an initialize
/// request. Returns `false` if absent or not `true`. Mirrors
/// [`Self::parse_code_nav_capability`].
pub(crate) fn parse_interactive_trust_capability(init: &acp::InitializeRequest) -> bool {
init.client_capabilities
.meta
.as_ref()
.and_then(|m| m.get("x.ai/folderTrust"))
.and_then(|m| m.get("kigi/folderTrust"))
.and_then(|v| v.get("interactive"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
@@ -84,7 +84,7 @@ impl MvpAgent {
/// Ask a GUI client to decide trust for `session_id`'s workspace, then grant
/// + reload on accept. DORMANT no-op unless the client advertised
/// `x.ai/folderTrust.interactive` AND [`folder_trust::prompt_warranted`]
/// `kigi/folderTrust.interactive` AND [`folder_trust::prompt_warranted`]
/// (feature on + untrusted + repo configs present).
///
/// Non-blocking: the session was already created with project servers GATED
@@ -181,7 +181,7 @@ impl MvpAgent {
return;
}
};
let ext_request = acp::ExtRequest::new("x.ai/folder_trust/request", raw_params.into());
let ext_request = acp::ExtRequest::new("kigi/folder_trust/request", raw_params.into());
use agent_client_protocol::Client as _;
let outcome = match tokio::time::timeout(
@@ -374,7 +374,7 @@ mod tests {
fn parse_interactive_trust_capability_present_and_true() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/folderTrust".to_string(),
"kigi/folderTrust".to_string(),
serde_json::json!({ "interactive": true }),
);
let init = init_with_meta(Some(serde_json::Value::Object(meta)));
@@ -391,7 +391,7 @@ mod tests {
fn parse_interactive_trust_capability_false_returns_false() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/folderTrust".to_string(),
"kigi/folderTrust".to_string(),
serde_json::json!({ "interactive": false }),
);
let init = init_with_meta(Some(serde_json::Value::Object(meta)));
@@ -85,7 +85,7 @@ use tokio_util::sync::CancellationToken;
use kigi_paths::AbsPathBuf;
use kigi_workspace::session::git::GitDiscoveryResult;
use kigi_hunk_tracker::HunkTrackerActor;
/// Hard-error message for legacy Direct hub-bind sessions (`x.ai/cloud_server_id`).
/// Hard-error message for legacy Direct hub-bind sessions (`kigi/cloud_server_id`).
pub(crate) const DIRECT_HUB_CLOUD_REMOVED_MSG: &str = "Direct hub cloud removed; use Gateway (envId or existing-workspace attach)";
/// Reject session `_meta` that still requests Direct hub bind (D8).
///
@@ -93,7 +93,7 @@ pub(crate) const DIRECT_HUB_CLOUD_REMOVED_MSG: &str = "Direct hub cloud removed;
pub(crate) fn reject_direct_hub_cloud_meta(
session_meta: Option<&acp::Meta>,
) -> Result<(), acp::Error> {
if session_meta.and_then(|m| m.get("x.ai/cloud_server_id")).is_some() {
if session_meta.and_then(|m| m.get("kigi/cloud_server_id")).is_some() {
return Err(acp::Error::invalid_params().data(DIRECT_HUB_CLOUD_REMOVED_MSG));
}
Ok(())
@@ -147,13 +147,13 @@ impl BridgeAttach {
!matches!(self, Self::NotAttached)
}
}
/// `_meta["x.ai/session"].kind` → [`SessionKind`]; absent/unknown/malformed → `Build`.
/// `_meta["kigi/session"].kind` → [`SessionKind`]; absent/unknown/malformed → `Build`.
fn parse_session_kind(
meta: Option<&acp::Meta>,
) -> crate::session::unified_list::SessionKind {
use crate::session::unified_list::SessionKind;
use serde::Deserialize;
meta.and_then(|m| m.get("x.ai/session"))
meta.and_then(|m| m.get("kigi/session"))
.and_then(|s| s.get("kind"))
.and_then(|k| SessionKind::deserialize(k).ok())
.unwrap_or(SessionKind::Build)
@@ -191,7 +191,7 @@ fn chat_new_session_model_state(
/// `session/new` / `session/load` `_meta` key carrying per-session plugin roots.
pub(crate) const SESSION_PLUGIN_DIRS_META_KEY: &str = "pluginDirs";
/// `initialize` response `_meta` key advertising [`SESSION_PLUGIN_DIRS_META_KEY`] support.
pub(crate) const SESSION_PLUGIN_DIRS_CAPABILITY_KEY: &str = "x.ai/pluginDirs";
pub(crate) const SESSION_PLUGIN_DIRS_CAPABILITY_KEY: &str = "kigi/pluginDirs";
/// Per-session plugin roots from `session/new` / `session/load` `_meta.pluginDirs`,
/// loaded at CliOverride scope (always trusted) into this session's registry only.
/// Paths must be absolute (the SDKs resolve before sending); anything else is
@@ -271,7 +271,7 @@ fn parse_no_replay(meta: Option<&acp::Meta>) -> bool {
meta.and_then(|m| m.get("noReplay")).and_then(|v| v.as_bool()).unwrap_or(false)
}
/// Insert `key`/`value` into a notification's `_meta`, creating the map if absent.
/// Used to stamp `x.ai/leaderClientId` onto replay notifications so the leader can
/// Used to stamp `kigi/leaderClientId` onto replay notifications so the leader can
/// unicast them to the loading client only (see `forward_raw_replay_line`).
fn stamp_meta_value(meta: &mut Option<acp::Meta>, key: &str, value: &serde_json::Value) {
meta.get_or_insert_with(acp::Meta::new).insert(key.to_string(), value.clone());
@@ -284,7 +284,7 @@ fn mark_as_replay(
let obj = meta.get_or_insert_with(acp::Meta::new);
obj.insert("isReplay".to_string(), is_replay);
if let Some(persist) = persist_data {
obj.insert("x.ai/persist".to_string(), persist.clone());
obj.insert("kigi/persist".to_string(), persist.clone());
}
}
/// Resolve a session's REQUESTED auto flag from `_meta`: an explicit `autoMode`
@@ -393,7 +393,7 @@ pub(crate) fn build_prompt_response_meta(
};
serde_json::to_value(meta).expect("PromptResponseMeta is always serializable")
}
/// Typed payload for the `x.ai/settings/update` notification sent to pager
/// Typed payload for the `kigi/settings/update` notification sent to pager
/// clients after remote settings settings are refreshed on `/new`.
///
/// Keeping this as a `#[derive(Serialize)]` struct gives compile-time
@@ -413,13 +413,13 @@ struct SettingsUpdateNotification {
/// Reason why a client is not eligible to use codebase indexing.
///
/// Returned by [`MvpAgent::code_nav_eligibility`] when one of the policy
/// gates fails. Used in `x.ai/code/status` responses and to generate
/// gates fails. Used in `kigi/code/status` responses and to generate
/// clear error messages on code-nav requests from ineligible clients.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeNavEligibility {
/// Client type is not web (web-only for initial rollout).
ClientNotWeb,
/// Client did not advertise `x.ai/codeNavigation.enabled`.
/// Client did not advertise `kigi/codeNavigation.enabled`.
CapabilityNotAdvertised,
/// `codebase_indexing` feature is disabled in config (or excluded by glob).
DisabledByConfig,
@@ -519,12 +519,12 @@ pub struct MvpAgent {
/// attribution would require threading `clientIdentifier` from `_meta` through
/// every session handler, which is deferred to future work.
client_type: RefCell<ClientType>,
/// Whether the current client advertised `x.ai/codeNavigation.enabled`.
/// Whether the current client advertised `kigi/codeNavigation.enabled`.
/// Updated on every `initialize()` call — same last-client-wins semantics
/// as `client_type`. Using `Cell<bool>` (not `RefCell`) so `.get()` is a
/// plain copy with no borrow that could be held across an await point.
code_nav_enabled: std::cell::Cell<bool>,
/// Whether the current client advertised `x.ai/folderTrust.interactive` (it
/// Whether the current client advertised `kigi/folderTrust.interactive` (it
/// can render the interactive folder-trust prompt). Set on every
/// `initialize()` (last-client-wins, like `code_nav_enabled`); gates the
/// DORMANT agent→client trust round-trip in `new_session`/`load_session`.
@@ -606,14 +606,14 @@ pub struct MvpAgent {
/// Unified sender for all subagent coordinator events.
/// LEADER-SAFE(shared): channel is multi-producer, coordinator drains.
subagent_event_tx: tokio::sync::mpsc::UnboundedSender<
kigi_tools::implementations::grok_build::task::types::SubagentEvent,
kigi_tools::implementations::kigi::task::types::SubagentEvent,
>,
/// Receiver for subagent events. Taken once by `start_subagent_coordinator()`.
/// `None` after the coordinator drain task has been spawned.
subagent_event_rx: RefCell<
Option<
tokio::sync::mpsc::UnboundedReceiver<
kigi_tools::implementations::grok_build::task::types::SubagentEvent,
kigi_tools::implementations::kigi::task::types::SubagentEvent,
>,
>,
>,
@@ -624,7 +624,7 @@ pub struct MvpAgent {
/// Pushed by the `InjectNotification` handler when a turn is active and the
/// notification has `Next` priority. Drained by the session turn loop
/// (`inject_pending_monitor_events`) into a hidden synthetic user message.
monitor_event_buffer: kigi_tools::implementations::grok_build::task::types::MonitorEventBuffer,
monitor_event_buffer: kigi_tools::implementations::kigi::task::types::MonitorEventBuffer,
/// Per-subagent model ID overrides from config.toml `[subagents.models]`.
/// Populated from `SubagentsConfig.models` during `with_models()`.
subagent_model_overrides: std::collections::HashMap<String, String>,
@@ -653,7 +653,7 @@ pub struct MvpAgent {
/// `plugin_registry_handle`.
///
/// Boot-time plugin discovery is deferred past ACP `initialize` (it walks
/// cwd→git root plus user/marketplace dirs and stalled grok-desktop's first
/// cwd→git root plus user/marketplace dirs and stalled kigi-desktop's first
/// `initialize`), so the shared snapshot starts empty. It is built once on
/// the first session-creating call via [`Self::ensure_plugin_registry`];
/// this flag keeps that to a single discovery walk.
@@ -753,8 +753,8 @@ pub(crate) fn inherited_harness_template(
///
/// When a zero-turn switch rebuilds the harness (`did_rebuild`), the handle
/// must adopt the rebuilt harness's agent type. Otherwise the name is left
/// unchanged — compatible stock switches (e.g. `grok-build` →
/// `grok-build-plan`) intentionally preserve the session's original ACP
/// unchanged — compatible stock switches (e.g. `kigi` →
/// `kigi-plan`) intentionally preserve the session's original ACP
/// `agentProfile`.
pub(crate) fn agent_name_after_model_switch(
did_rebuild: bool,
@@ -770,8 +770,8 @@ pub(crate) fn agent_name_after_model_switch(
/// Harness compatibility for zero-turn / mid-turn model switching.
///
/// Two stock (non-strict) agents are interchangeable — they share the
/// default wire format and toolset, so switching e.g. `grok-build` →
/// `grok-build-plan` doesn't require rebuilding the harness and would
/// default wire format and toolset, so switching e.g. `kigi` →
/// `kigi-plan` doesn't require rebuilding the harness and would
/// destroy a client-supplied `_meta.agentProfile` if it did.
///
/// Strict harnesses (`codex`, …) are only compatible with
@@ -927,7 +927,7 @@ fn resolve_inference_idle_timeout_secs(
let remote = remote_settings.and_then(|s| s.inference_idle_timeout_secs);
per_model.or(remote).unwrap_or(600).max(10)
}
/// Parse the client-advertised `x.ai/hunkTracker.mode` string. Case-insensitive
/// Parse the client-advertised `kigi/hunkTracker.mode` string. Case-insensitive
/// and trimmed. Absent/blank/`off`/`disabled` => `None`; unknown => `AllDirty`.
fn resolve_hunk_tracking_mode(
mode_str: Option<&str>,
@@ -1025,7 +1025,7 @@ impl MvpAgent {
/// Dispatches by on-disk method name:
/// - ACP updates (`"session/update"`) → typed `SessionNotification` for correct
/// TUI dispatch (direct dispatch preserves Rust types, not method strings).
/// - xAI updates (`"_x.ai/session/update"`) → `ExtNotification`.
/// - xAI updates (`"_kigi/session/update"`) → `ExtNotification`.
///
/// When `mark_replay` is true, the notification is tagged with
/// `_meta.isReplay: true` so the client knows it's historical data.
@@ -1058,7 +1058,7 @@ impl MvpAgent {
tracing::debug!("replay: skipping JSONL line with no params");
return;
};
let is_xai = method == "_x.ai/session/update";
let is_xai = method == "_kigi/session/update";
if is_xai {
if target_client_id.is_none() && !mark_replay {
if let Ok(owned) = serde_json::value::RawValue::from_string(
@@ -1070,7 +1070,7 @@ impl MvpAgent {
.gateway
.forward_with_completion(
acp::ExtNotification::new(
"x.ai/session/update",
"kigi/session/update",
std::sync::Arc::from(owned),
),
),
@@ -1094,10 +1094,10 @@ impl MvpAgent {
m.insert("isReplay".to_string(), serde_json::json!(true));
}
if let Some(pd) = persist_data {
m.insert("x.ai/persist".to_string(), pd.clone());
m.insert("kigi/persist".to_string(), pd.clone());
}
if let Some(tid) = target_client_id {
m.insert("x.ai/leaderClientId".to_string(), tid.clone());
m.insert("kigi/leaderClientId".to_string(), tid.clone());
}
}
}
@@ -1108,7 +1108,7 @@ impl MvpAgent {
.gateway
.forward_with_completion(
acp::ExtNotification::new(
"x.ai/session/update",
"kigi/session/update",
std::sync::Arc::from(raw_val),
),
),
@@ -1161,7 +1161,7 @@ impl MvpAgent {
mark_as_replay(&mut notification.meta, persist_data);
}
if let Some(tid) = target_client_id {
stamp_meta_value(&mut notification.meta, "x.ai/leaderClientId", tid);
stamp_meta_value(&mut notification.meta, "kigi/leaderClientId", tid);
}
completions.push(self.gateway.forward_with_completion(notification));
}
@@ -1425,7 +1425,7 @@ impl MvpAgent {
.gateway
.forward_with_completion(
acp::ExtNotification::new(
"x.ai/task_completed",
"kigi/task_completed",
params.into(),
),
),
@@ -1507,7 +1507,7 @@ impl MvpAgent {
});
AuthenticateResponse::new().meta(meta)
}
/// Fire-and-forget `x.ai/settings/update` from the current remote snapshot.
/// Fire-and-forget `kigi/settings/update` from the current remote snapshot.
pub(super) fn emit_settings_update_notification(&self) {
let payload = {
let cfg = self.cfg.borrow();
@@ -1528,7 +1528,7 @@ impl MvpAgent {
if let Ok(params) = serde_json::value::to_raw_value(&payload) {
self.gateway
.forward_fire_and_forget(
acp::ExtNotification::new("x.ai/settings/update", params.into()),
acp::ExtNotification::new("kigi/settings/update", params.into()),
);
}
}
@@ -23,12 +23,12 @@ fn args<'a>(
#[test]
fn includes_baseline_keys_without_usage() {
let meta = build_prompt_response_meta(args("sess-1", "prompt-1", 42_000, "grok-4.5"));
let meta = build_prompt_response_meta(args("sess-1", "prompt-1", 42_000, "kigi-4.5"));
assert_eq!(meta["sessionId"], "sess-1");
assert_eq!(meta["requestId"], "prompt-1");
assert_eq!(meta["promptId"], "prompt-1");
assert_eq!(meta["totalTokens"], 42_000);
assert_eq!(meta["modelId"], "grok-4.5");
assert_eq!(meta["modelId"], "kigi-4.5");
// No per-turn keys when usage is absent.
assert!(meta.get("inputTokens").is_none());
assert!(meta.get("outputTokens").is_none());
@@ -46,7 +46,7 @@ fn enriches_meta_with_camelcase_token_keys() {
};
let meta = build_prompt_response_meta(PromptResponseMetaArgs {
last_turn_usage: Some(&usage),
..args("sess-1", "prompt-1", 1_700, "grok-4.5")
..args("sess-1", "prompt-1", 1_700, "kigi-4.5")
});
// Bot's _META_TOKEN_KEY_MAP expects exactly these camelCase keys.
assert_eq!(meta["inputTokens"], 1500);
@@ -12,7 +12,7 @@ impl MvpAgent {
/// Finalize the cloud session replica (fire-and-forget, "Hook 4").
///
/// Marks the session **done** upstream, so this MUST only run on a genuine
/// session end — a terminal/explicit close (`x.ai/session/close`). It must
/// session end — a terminal/explicit close (`kigi/session/close`). It must
/// NOT run on a mere client disconnect or a dead-actor reap: those leave the
/// conversation resumable on disk, and finalizing would wrongly mark a still
/// running/resumable session "done".
@@ -58,7 +58,7 @@ impl MvpAgent {
.clone()
}
/// Close a session in response to an **explicit** terminal close
/// (`x.ai/session/close`). Finalizes the cloud replica (genuine session
/// (`kigi/session/close`). Finalizes the cloud replica (genuine session
/// end), then removes the session terminally as `Completed`.
pub(crate) fn close_session_explicit(&self, id: &acp::SessionId) {
self.finalize_session_replica(id);
@@ -76,7 +76,7 @@ impl MvpAgent {
self.session_live_state.borrow().get(id).copied()
}
/// Roster-delta hook for a terminally removed session. Broadcasts an
/// `x.ai/sessions/changed` notification with the session in `removed` so
/// `kigi/sessions/changed` notification with the session in `removed` so
/// every attached dashboard drops the row promptly. Also
/// records the call site (and the terminal state) for test observability,
/// since the `session_live_state` entry is dropped on removal.
@@ -91,14 +91,14 @@ impl MvpAgent {
self.emit_roster_changed(Vec::new(), vec![id.0.to_string()]);
}
/// Roster-delta hook for a newly-resident / changed session. Broadcasts an
/// `x.ai/sessions/changed` notification with the current entry in
/// `kigi/sessions/changed` notification with the current entry in
/// `upserted` so dashboards add/refresh the row.
pub(crate) fn push_roster_delta_upserted(&self, id: &acp::SessionId) {
if let Some(entry) = self.resident_roster_entry(id) {
self.emit_roster_changed(vec![entry], Vec::new());
}
}
/// Emit an `x.ai/sessions/changed` upsert for a resident session with an
/// Emit an `kigi/sessions/changed` upsert for a resident session with an
/// explicit `activity`, so every attached dashboard reflects a
/// turn-boundary transition (Working / Idle / NeedsInput) *immediately*
/// rather than waiting for the ≤1s roster poll (deltas are emitted
@@ -124,11 +124,11 @@ impl MvpAgent {
self.emit_roster_changed(vec![entry], Vec::new());
}
}
/// Fan an `x.ai/sessions/changed` delta out to every attached client.
/// Fan an `kigi/sessions/changed` delta out to every attached client.
///
/// This is a roster-wide notification (no `sessionId`), so the leader IPC
/// server broadcasts it to all clients rather than routing by session (see
/// the `x.ai/sessions/changed` special-case in `leader/server.rs`).
/// the `kigi/sessions/changed` special-case in `leader/server.rs`).
pub(super) fn emit_roster_changed(
&self,
upserted: Vec<crate::agent::roster::RosterEntry>,
@@ -16,7 +16,7 @@ impl MvpAgent {
};
let agent_ref = LocalRef::new(self);
use crate::agent::subagent::{BlockWaitSlot, is_running, resolve_snapshot};
use kigi_tools::implementations::grok_build::task::types::{
use kigi_tools::implementations::kigi::task::types::{
SubagentCancelOutcome, SubagentCancelTarget, SubagentEvent,
};
tokio::task::spawn_local({
@@ -208,7 +208,7 @@ impl MvpAgent {
SubagentEvent::DescribeType(request) => {
let agent_ref = agent_ref.clone();
tokio::task::spawn_local(async move {
use kigi_tools::implementations::grok_build::task::types::SubagentDescribeOutcome;
use kigi_tools::implementations::kigi::task::types::SubagentDescribeOutcome;
let this = agent_ref.get();
let outcome = match this
.try_build_subagent_spawn_context(&request.parent_session_id)
@@ -224,7 +224,7 @@ fn allocate_turn_number_advances_counter() {
/// With no overrides and model_agent_type = None, the default agent is used.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_defaults_to_grok_build() {
fn resolve_agent_definition_defaults_to_kigi() {
let prev = std::env::var("KIGI_AGENT").ok();
unsafe {
std::env::remove_var("KIGI_AGENT");
@@ -243,7 +243,7 @@ fn resolve_agent_definition_defaults_to_grok_build() {
}
}
/// When model_agent_type = Some("codex"), the codex agent is selected even
/// though the default chain would return grok-build.
/// though the default chain would return kigi.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_model_agent_type_overrides_default() {
@@ -320,13 +320,13 @@ fn resolve_agent_definition_acp_profile_wins_when_model_agent_type_is_default()
}
}
/// Regression: after `DEFAULT_AGENT_TYPE` flipped to
/// `grok-build-plan`, models in the catalog that still declare
/// `agent_type = "grok-build"` explicitly must NOT preempt an ACP
/// profile. Any value in the `grok-build*` family is the stock harness
/// `kigi-plan`, models in the catalog that still declare
/// `agent_type = "kigi"` explicitly must NOT preempt an ACP
/// profile. Any value in the `kigi*` family is the stock harness
/// with no strict requirement.
#[test]
#[serial_test::serial]
fn resolve_agent_definition_acp_profile_wins_for_explicit_grok_build_family() {
fn resolve_agent_definition_acp_profile_wins_for_explicit_kigi_family() {
let prev = std::env::var("KIGI_AGENT").ok();
unsafe {
std::env::remove_var("KIGI_AGENT");
@@ -337,7 +337,7 @@ fn resolve_agent_definition_acp_profile_wins_for_explicit_grok_build_family() {
"Custom devbox profile", }
))
.expect("agent definition must parse");
for family_variant in ["grok-build", "grok-build-plan", "grok-build-concise"] {
for family_variant in ["kigi", "kigi-plan", "kigi-concise"] {
let def = MvpAgent::resolve_agent_definition(
tmp.path(),
None,
@@ -347,7 +347,7 @@ fn resolve_agent_definition_acp_profile_wins_for_explicit_grok_build_family() {
);
assert_eq!(
def.name, "custom-devbox-profile",
"ACP profile must win for grok-build family variant `{family_variant}`"
"ACP profile must win for kigi family variant `{family_variant}`"
);
}
if let Some(v) = prev {
@@ -543,62 +543,62 @@ fn enqueue_replace_system_prompt_override_noop_when_absent_or_empty() {
);
}
/// Regression for the web-client `_meta.agentProfile` -> `set_session_model`
/// flow: a zero-turn switch from `grok-build` (a client profile name) to
/// `grok-build-plan` (the default model agent_type) must be
/// flow: a zero-turn switch from `kigi` (a client profile name) to
/// `kigi-plan` (the default model agent_type) must be
/// treated as compatible so the harness rebuild is skipped and the
/// custom prompt body is preserved.
#[test]
fn harnesses_are_compatible_for_stock_family_pairs() {
assert!(harnesses_are_compatible("grok-build", "grok-build-plan"));
assert!(harnesses_are_compatible("grok-build-plan", "grok-build"));
assert!(harnesses_are_compatible("grok-build", "grok-build"));
assert!(harnesses_are_compatible("kigi", "kigi-plan"));
assert!(harnesses_are_compatible("kigi-plan", "kigi"));
assert!(harnesses_are_compatible("kigi", "kigi"));
assert!(harnesses_are_compatible(
"grok-build-concise",
"grok-build-plan"
"kigi-concise",
"kigi-plan"
));
assert!(harnesses_are_compatible(
"remote-sidebar",
"grok-build-plan"
"kigi-plan"
));
}
#[test]
fn harnesses_are_compatible_rejects_strict_mismatches() {
assert!(harnesses_are_compatible("codex", "codex"));
assert!(!harnesses_are_compatible("grok-build-plan", "codex"));
assert!(!harnesses_are_compatible("kigi-plan", "codex"));
}
#[test]
fn explicit_agent_type_wins_over_session_default() {
assert_eq!(
resolve_required_agent_type(Some("cursor"), "grok-build-plan"),
resolve_required_agent_type(Some("cursor"), "kigi-plan"),
"cursor"
);
}
#[test]
fn null_agent_type_falls_back_to_session_default_grok_build_plan() {
fn null_agent_type_falls_back_to_session_default_kigi_plan() {
assert_eq!(
resolve_required_agent_type(None, "grok-build-plan"),
"grok-build-plan"
resolve_required_agent_type(None, "kigi-plan"),
"kigi-plan"
);
}
#[test]
fn null_agent_type_falls_back_to_session_default_grok_build() {
fn null_agent_type_falls_back_to_session_default_kigi() {
assert_eq!(
resolve_required_agent_type(None, "grok-build"),
"grok-build"
resolve_required_agent_type(None, "kigi"),
"kigi"
);
}
#[test]
fn null_agent_type_returns_to_session_default_after_cursor_switch() {
let session_default = "grok-build-plan";
let session_default = "kigi-plan";
let required_after_null = resolve_required_agent_type(None, session_default);
assert_eq!(required_after_null, "grok-build-plan");
assert_eq!(required_after_null, "kigi-plan");
assert_ne!(required_after_null, "cursor");
}
/// Compatible stock switches (no rebuild) must NOT mutate `agent_name`,
/// preserving the session's original ACP `agentProfile`.
#[test]
fn agent_name_unchanged_without_harness_rebuild() {
let unchanged = agent_name_after_model_switch(false, "grok-build-plan", "remote-sidebar");
let unchanged = agent_name_after_model_switch(false, "kigi-plan", "remote-sidebar");
assert_eq!(
unchanged, "remote-sidebar",
"a compatible stock switch must preserve the original agent profile name"
@@ -648,7 +648,7 @@ async fn file_toolset_override_e2e_to_finalized_toolset() {
web_search_config: kigi_tools::implementations::web_search::WebSearchConfig::default(),
web_fetch_config: Default::default(),
lsp: None,
app_builder_deployer_config: kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig::default(),
app_builder_deployer_config: kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig::default(),
api_key_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
@@ -741,7 +741,7 @@ fn make_test_handle(
force_compact: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
permission_handle: kigi_workspace::permission::PermissionHandle::allow_all(),
attribution_callback: None,
agent_name: "grok-build".to_string(),
agent_name: "kigi".to_string(),
session_default_agent_profile: None,
allowed_subagent_types: None,
hook_registry: None,
@@ -758,7 +758,7 @@ async fn lookup_session_model_returns_per_session_model() {
let sid_b = acp::SessionId::new("sess-b");
let default_model = acp::ModelId::new("default-model");
let sessions: HashMap<acp::SessionId, crate::session::SessionHandle> = [
(sid_a.clone(), make_test_handle("grok-3-fast", false, None)),
(sid_a.clone(), make_test_handle("kigi-3-fast", false, None)),
(sid_b.clone(), make_test_handle("codex-mini", false, None)),
]
.into();
@@ -766,7 +766,7 @@ async fn lookup_session_model_returns_per_session_model() {
lookup_session_model(&sessions, Some(&sid_a), &default_model)
.0
.as_ref(),
"grok-3-fast"
"kigi-3-fast"
);
assert_eq!(
lookup_session_model(&sessions, Some(&sid_b), &default_model)
@@ -778,13 +778,13 @@ async fn lookup_session_model_returns_per_session_model() {
/// lookup_session_model falls back to the default when session_id is None.
#[tokio::test]
async fn lookup_session_model_fallback_no_session() {
let default_model = acp::ModelId::new("grok-3");
let default_model = acp::ModelId::new("kigi-3");
let sessions: HashMap<acp::SessionId, crate::session::SessionHandle> = HashMap::new();
assert_eq!(
lookup_session_model(&sessions, None, &default_model)
.0
.as_ref(),
"grok-3"
"kigi-3"
);
}
/// Mutating session A's model_id via the handle does not affect session B.
@@ -794,8 +794,8 @@ async fn set_session_model_does_not_cross_contaminate() {
let sid_b = acp::SessionId::new("sess-b");
let default_model = acp::ModelId::new("default");
let mut sessions: HashMap<acp::SessionId, crate::session::SessionHandle> = [
(sid_a.clone(), make_test_handle("grok-3", false, None)),
(sid_b.clone(), make_test_handle("grok-3", false, None)),
(sid_a.clone(), make_test_handle("kigi-3", false, None)),
(sid_b.clone(), make_test_handle("kigi-3", false, None)),
]
.into();
sessions.get_mut(&sid_a).unwrap().model_id = acp::ModelId::new("codex-mini");
@@ -809,7 +809,7 @@ async fn set_session_model_does_not_cross_contaminate() {
lookup_session_model(&sessions, Some(&sid_b), &default_model)
.0
.as_ref(),
"grok-3",
"kigi-3",
"Session B's model must not be affected by session A's model change"
);
}
@@ -903,15 +903,15 @@ async fn yolo_toggle_scoped_by_client_identifier() {
let mut sessions: HashMap<acp::SessionId, crate::session::SessionHandle> = [
(
sid_tui.clone(),
make_test_handle("grok-3", false, Some("grok-tui")),
make_test_handle("kigi-3", false, Some("kigi-tui")),
),
(
sid_vscode.clone(),
make_test_handle("grok-3", false, Some("grok-code-extension")),
make_test_handle("kigi-3", false, Some("kigi-code-extension")),
),
]
.into();
let updated = apply_yolo_mode_to_matching_sessions(&mut sessions, Some("grok-tui"), true);
let updated = apply_yolo_mode_to_matching_sessions(&mut sessions, Some("kigi-tui"), true);
assert_eq!(updated, 1, "exactly one matching session should be updated");
assert!(
sessions[&sid_tui].yolo_mode,
@@ -931,15 +931,15 @@ async fn yolo_toggle_can_disable_session_started_with_yolo_enabled() {
let mut sessions: HashMap<acp::SessionId, crate::session::SessionHandle> = [
(
sid_tui.clone(),
make_test_handle("grok-3", true, Some("grok-tui")),
make_test_handle("kigi-3", true, Some("kigi-tui")),
),
(
sid_other.clone(),
make_test_handle("grok-3", true, Some("grok-code-extension")),
make_test_handle("kigi-3", true, Some("kigi-code-extension")),
),
]
.into();
let updated = apply_yolo_mode_to_matching_sessions(&mut sessions, Some("grok-tui"), false);
let updated = apply_yolo_mode_to_matching_sessions(&mut sessions, Some("kigi-tui"), false);
assert_eq!(updated, 1, "only the sender's session should be updated");
assert!(
!sessions[&sid_tui].yolo_mode,
@@ -1030,7 +1030,7 @@ async fn drain_respects_deadline() {
fn parse_code_nav_capability_present_and_true() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/codeNavigation".to_string(),
"kigi/codeNavigation".to_string(),
serde_json::json!({ "enabled" : true }),
);
let init = acp::InitializeRequest::new(acp::ProtocolVersion::V1).client_capabilities(
@@ -1054,7 +1054,7 @@ fn parse_code_nav_capability_absent_returns_false() {
fn parse_code_nav_capability_false_returns_false() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/codeNavigation".to_string(),
"kigi/codeNavigation".to_string(),
serde_json::json!({ "enabled" : false }),
);
let init = acp::InitializeRequest::new(acp::ProtocolVersion::V1).client_capabilities(
@@ -1074,18 +1074,18 @@ fn parse_code_nav_capability_false_returns_false() {
#[tokio::test]
async fn test_per_session_code_nav_isolation() {
let web_handle = {
let mut h = make_test_handle("model", false, Some("grok-web"));
let mut h = make_test_handle("model", false, Some("kigi-web"));
h.code_nav_enabled = true;
h
};
let tui_handle = {
let mut h = make_test_handle("model", false, Some("grok-tui"));
let mut h = make_test_handle("model", false, Some("kigi-tui"));
h.code_nav_enabled = false;
h
};
let check = |handle: &crate::session::SessionHandle| {
let ct = crate::http::client_type_from_origin(handle.origin_client.as_ref());
if !matches!(ct, ClientType::GrokWeb) {
if !matches!(ct, ClientType::KigiWeb) {
return Err(CodeNavEligibility::ClientNotWeb);
}
if !handle.code_nav_enabled {
@@ -1322,7 +1322,7 @@ async fn resident_activity_reports_needs_input_when_pending() {
use crate::agent::roster::RosterActivity;
let agent = build_minimal_agent_for_tests();
let sid = acp::SessionId::new("sess-pending");
let handle = make_test_handle("grok-3", false, None);
let handle = make_test_handle("kigi-3", false, None);
let pending = handle.pending_interactions.clone();
let prompt_id = handle.current_prompt_id.clone();
agent.sessions.borrow_mut().insert(sid.clone(), handle);
@@ -1339,7 +1339,7 @@ async fn resident_activity_reports_needs_input_when_pending() {
pending.lock().unwrap().clear();
assert_eq!(agent.resident_activity(&sid), RosterActivity::Working);
}
/// Drain the agent gateway, returning the first `x.ai/sessions/changed`
/// Drain the agent gateway, returning the first `kigi/sessions/changed`
/// payload that carries an upserted entry (ignoring any unrelated
/// notifications, which parse into an empty `RosterChanged`).
fn drain_roster_changed(
@@ -1362,7 +1362,7 @@ fn drain_roster_changed(
found
}
/// A turn-boundary activity delta (`push_roster_activity_delta`) broadcasts
/// an `x.ai/sessions/changed` upsert carrying the *overridden* activity, so
/// an `kigi/sessions/changed` upsert carrying the *overridden* activity, so
/// every attached dashboard reflects Working/Idle immediately instead of
/// waiting for the ≤1s roster poll (turn-start/turn-end). The
/// override matters because at turn-start the actor has not yet published
@@ -1384,7 +1384,7 @@ async fn push_roster_activity_delta_broadcasts_overridden_activity() {
agent
.sessions
.borrow_mut()
.insert(sid.clone(), make_test_handle("grok-3", false, None));
.insert(sid.clone(), make_test_handle("kigi-3", false, None));
agent.push_roster_activity_delta(&sid, RosterActivity::Working);
let changed = drain_roster_changed(&mut rx).expect("turn-start delta emitted");
assert_eq!(changed.upserted.len(), 1);
@@ -1427,7 +1427,7 @@ fn check_nav_eligibility_from_sessions(
return Err(CodeNavEligibility::SessionRequired);
};
let ct = crate::http::client_type_from_origin(handle.origin_client.as_ref());
if !matches!(ct, ClientType::GrokWeb) {
if !matches!(ct, ClientType::KigiWeb) {
return Err(CodeNavEligibility::ClientNotWeb);
}
if !handle.code_nav_enabled {
@@ -1442,7 +1442,7 @@ fn check_nav_eligibility_from_sessions(
#[tokio::test]
async fn test_web_session_with_capability_is_eligible() {
let sid = acp::SessionId::new("sess-web");
let mut handle = make_test_handle("model", false, Some("grok-web"));
let mut handle = make_test_handle("model", false, Some("kigi-web"));
handle.code_nav_enabled = true;
let sessions = [(sid.clone(), handle)].into();
assert!(
@@ -1454,7 +1454,7 @@ async fn test_web_session_with_capability_is_eligible() {
#[tokio::test]
async fn test_tui_session_is_rejected() {
let sid = acp::SessionId::new("sess-tui");
let mut handle = make_test_handle("model", false, Some("grok-tui"));
let mut handle = make_test_handle("model", false, Some("kigi-tui"));
handle.code_nav_enabled = true;
let sessions = [(sid.clone(), handle)].into();
assert_eq!(
@@ -1467,7 +1467,7 @@ async fn test_tui_session_is_rejected() {
#[tokio::test]
async fn test_web_session_without_capability_is_rejected() {
let sid = acp::SessionId::new("sess-web-no-cap");
let mut handle = make_test_handle("model", false, Some("grok-web"));
let mut handle = make_test_handle("model", false, Some("kigi-web"));
handle.code_nav_enabled = false;
let sessions = [(sid.clone(), handle)].into();
assert_eq!(
@@ -1482,9 +1482,9 @@ async fn test_web_session_without_capability_is_rejected() {
async fn test_leader_mode_two_sessions_stay_isolated() {
let web_sid = acp::SessionId::new("web");
let tui_sid = acp::SessionId::new("tui");
let mut web_handle = make_test_handle("model", false, Some("grok-web"));
let mut web_handle = make_test_handle("model", false, Some("kigi-web"));
web_handle.code_nav_enabled = true;
let mut tui_handle = make_test_handle("model", false, Some("grok-tui"));
let mut tui_handle = make_test_handle("model", false, Some("kigi-tui"));
tui_handle.code_nav_enabled = false;
let sessions = [(web_sid.clone(), web_handle), (tui_sid.clone(), tui_handle)].into();
assert!(
@@ -1505,7 +1505,7 @@ async fn test_leader_mode_two_sessions_stay_isolated() {
#[tokio::test]
async fn test_unknown_session_id_returns_session_required() {
let known_sid = acp::SessionId::new("known");
let mut known_handle = make_test_handle("model", false, Some("grok-web"));
let mut known_handle = make_test_handle("model", false, Some("kigi-web"));
known_handle.code_nav_enabled = true;
let sessions = [(known_sid.clone(), known_handle)].into();
let stale_sid = acp::SessionId::new("stale-or-evicted");
@@ -1567,7 +1567,7 @@ mod eligibility_gates {
code_nav_enabled: bool,
indexing_enabled: bool,
) -> Result<(), CodeNavEligibility> {
if !matches!(client_type, ClientType::GrokWeb) {
if !matches!(client_type, ClientType::KigiWeb) {
return Err(CodeNavEligibility::ClientNotWeb);
}
if !code_nav_enabled {
@@ -1588,27 +1588,27 @@ mod eligibility_gates {
#[test]
fn tui_client_rejected() {
assert_eq!(
check_gates(ClientType::GrokTUI, true, true),
check_gates(ClientType::KigiTUI, true, true),
Err(CodeNavEligibility::ClientNotWeb)
);
}
#[test]
fn web_client_no_capability_rejected() {
assert_eq!(
check_gates(ClientType::GrokWeb, false, true),
check_gates(ClientType::KigiWeb, false, true),
Err(CodeNavEligibility::CapabilityNotAdvertised)
);
}
#[test]
fn web_client_with_capability_config_disabled_rejected() {
assert_eq!(
check_gates(ClientType::GrokWeb, true, false),
check_gates(ClientType::KigiWeb, true, false),
Err(CodeNavEligibility::DisabledByConfig)
);
}
#[test]
fn web_client_with_capability_and_config_passes_first_three_gates() {
assert!(check_gates(ClientType::GrokWeb, true, true).is_ok());
assert!(check_gates(ClientType::KigiWeb, true, true).is_ok());
}
}
#[test]
@@ -1672,12 +1672,12 @@ fn write_updates(dir: &std::path::Path, lines: &[&str]) -> PathBuf {
}
fn bg_line(task_id: &str) -> String {
format!(
r#"{{"timestamp":1,"method":"_x.ai/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"task_backgrounded","task_id":"{task_id}","command":"sleep 99","cwd":"/tmp"}}}}}}"#
r#"{{"timestamp":1,"method":"_kigi/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"task_backgrounded","task_id":"{task_id}","command":"sleep 99","cwd":"/tmp"}}}}}}"#
)
}
fn completed_line(task_id: &str) -> String {
format!(
r#"{{"timestamp":2,"method":"_x.ai/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"task_completed","task_snapshot":{{"task_id":"{task_id}","completed":true}}}}}}}}"#
r#"{{"timestamp":2,"method":"_kigi/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"task_completed","task_snapshot":{{"task_id":"{task_id}","completed":true}}}}}}}}"#
)
}
fn orphaned_ids(tasks: &[OrphanedTask]) -> std::collections::HashSet<&str> {
@@ -1751,7 +1751,7 @@ fn orphaned_tasks_skips_malformed_lines() {
fn orphaned_tasks_ignores_unrelated_updates() {
let tmp = tempfile::tempdir().unwrap();
let bg = bg_line("t1");
let unrelated = r#"{"timestamp":1,"method":"_x.ai/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"auto_compact_started","percentage":80}}}"#;
let unrelated = r#"{"timestamp":1,"method":"_kigi/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"auto_compact_started","percentage":80}}}"#;
let path = write_updates(tmp.path(), &[&bg, unrelated]);
let result = MvpAgent::find_orphaned_background_tasks(&Some(path));
assert_eq!(result.len(), 1);
@@ -1761,7 +1761,7 @@ fn orphaned_tasks_filters_rewind_dead_branches() {
let tmp = tempfile::tempdir().unwrap();
let user_msg = r#"{"timestamp":0,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"user_message_chunk","content":{"type":"text","text":"hello"}}}}"#;
let bg_before_rewind = bg_line("t-dead");
let rewind = r#"{"timestamp":3,"method":"_x.ai/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"rewind_marker","target_prompt_index":0,"created_at":"2025-01-01T00:00:00Z"}}}"#;
let rewind = r#"{"timestamp":3,"method":"_kigi/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"rewind_marker","target_prompt_index":0,"created_at":"2025-01-01T00:00:00Z"}}}"#;
let user_msg2 = r#"{"timestamp":4,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"user_message_chunk","content":{"type":"text","text":"retry"}}}}"#;
let bg_after_rewind = bg_line("t-alive");
let path = write_updates(
@@ -1804,7 +1804,7 @@ fn on_demand_enabled_from_remote_settings() {
async fn auth_type_session_based_no_current_returns_session_token() {
for method_id in [
crate::agent::auth_method::CACHED_TOKEN_AUTH_METHOD_ID,
crate::agent::auth_method::KIGI_COM_METHOD_ID,
crate::agent::auth_method::KIMI_CODE_METHOD_ID,
] {
let agent = build_minimal_agent_for_tests();
agent.set_auth_method(acp::AuthMethodId::new(method_id));
@@ -1848,7 +1848,7 @@ async fn auth_type_session_based_with_current_returns_session_token() {
use crate::auth::KimiAuth;
let agent = build_minimal_agent_for_tests();
agent.set_auth_method(acp::AuthMethodId::new(
crate::agent::auth_method::KIGI_COM_METHOD_ID,
crate::agent::auth_method::KIMI_CODE_METHOD_ID,
));
agent.auth_manager.hot_swap(KimiAuth::test_default());
assert!(agent.auth_manager.current().is_some());
@@ -1899,12 +1899,12 @@ async fn cached_token_fallthrough_prefers_api_key_for_deployment_key() {
);
}
/// No advertiseable credentials at all (no env key, no kill switch): the user
/// genuinely needs to log in, so the fallthrough is interactive `grok.com`.
/// genuinely needs to log in, so the fallthrough is interactive `kimi-code`.
#[tokio::test(flavor = "current_thread")]
#[serial_test::serial]
async fn cached_token_fallthrough_falls_to_grok_com_without_credentials() {
async fn cached_token_fallthrough_falls_to_kigi_com_without_credentials() {
use crate::agent::auth_method::{
KIGI_COM_METHOD_ID, LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR,
KIMI_CODE_METHOD_ID, LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR,
};
use kigi_test_support::EnvGuard;
let _lockdown = EnvGuard::unset("KIGI_DISABLE_API_KEY_AUTH");
@@ -1913,8 +1913,8 @@ async fn cached_token_fallthrough_falls_to_grok_com_without_credentials() {
let agent = build_minimal_agent_for_tests();
assert_eq!(
agent.cached_token_fallthrough_method_id().0.as_ref(),
KIGI_COM_METHOD_ID,
"no API-key creds and no kill switch -> interactive grok.com login",
KIMI_CODE_METHOD_ID,
"no API-key creds and no kill switch -> interactive kimi.com login",
);
}
/// `parse_session_kind` routes `session/load` to the gateway Chat path vs. the
@@ -1926,22 +1926,22 @@ fn parse_session_kind_matrix() {
let cases: &[(&str, serde_json::Value, SessionKind)] = &[
(
"chat",
json!({ "x.ai/session" : { "kind" : "chat" } }),
json!({ "kigi/session" : { "kind" : "chat" } }),
SessionKind::Chat,
),
(
"build",
json!({ "x.ai/session" : { "kind" : "build" } }),
json!({ "kigi/session" : { "kind" : "build" } }),
SessionKind::Build,
),
(
"chat_malformed_sibling",
json!({ "x.ai/session" : { "kind" : "chat", "facets" : "not-a-map" } }),
json!({ "kigi/session" : { "kind" : "chat", "facets" : "not-a-map" } }),
SessionKind::Chat,
),
(
"unknown_kind",
json!({ "x.ai/session" : { "kind" : "frob" } }),
json!({ "kigi/session" : { "kind" : "frob" } }),
SessionKind::Build,
),
("absent", json!({}), SessionKind::Build),
@@ -1954,9 +1954,9 @@ fn parse_session_kind_matrix() {
#[test]
fn chat_initial_model_matrix() {
let cases: &[(&str, bool, Option<&str>, Option<&str>)] = &[
("chat_with_model", true, Some("grok-4.5"), Some("grok-4.5")),
("chat_with_model", true, Some("kigi-4.5"), Some("kigi-4.5")),
("chat_without_model", true, None, None),
("build_with_model", false, Some("grok-4.5"), None),
("build_with_model", false, Some("kigi-4.5"), None),
("build_without_model", false, None, None),
];
for (label, is_chat_kind, custom_model_id, expected) in cases {
@@ -1983,27 +1983,27 @@ fn chat_new_session_model_state_matrix() {
let cases: &[(&str, acp::SessionModelState, Option<&str>, &str)] = &[
(
"requested_in_catalog",
state_with("auto", &["auto", "grok-4"]),
Some("grok-4"),
"grok-4",
state_with("auto", &["auto", "kigi-4"]),
Some("kigi-4"),
"kigi-4",
),
(
"no_request_keeps_catalog_default",
state_with("auto", &["auto", "grok-4"]),
state_with("auto", &["auto", "kigi-4"]),
None,
"auto",
),
(
"requested_not_in_catalog",
state_with("auto", &["auto"]),
Some("grok-4.5"),
"grok-4.5",
Some("kigi-4.5"),
"kigi-4.5",
),
(
"requested_with_empty_catalog",
state_with("", &[]),
Some("grok-4"),
"grok-4",
Some("kigi-4"),
"kigi-4",
),
];
for (label, state, requested, expected) in cases {
@@ -2069,7 +2069,7 @@ fn ext_method_rewind_uses_local_dispatch_without_bridge() {
let params = serde_json::json!({ "sessionId" : "sess-local" });
let err = agent
.ext_method(acp::ExtRequest::new(
"x.ai/rewind/points",
"kigi/rewind/points",
std::sync::Arc::from(serde_json::value::to_raw_value(&params).unwrap()),
))
.await
@@ -2116,7 +2116,7 @@ fn make_live_session_handle(
tokio::sync::mpsc::UnboundedReceiver<TestSessionCommand>,
) {
let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
let mut handle = make_test_handle("test-model", false, Some("grok-tui"));
let mut handle = make_test_handle("test-model", false, Some("kigi-tui"));
handle.cmd_tx = cmd_tx.clone();
handle.info = crate::session::info::Info {
id: sid.clone(),
@@ -2149,14 +2149,14 @@ fn spawn_fake_actor(
});
observed_rx
}
/// Drive `x.ai/internal/evict_sessions` through the real `ext_notification`
/// Drive `kigi/internal/evict_sessions` through the real `ext_notification`
/// handler path (not the internal helper) — matches how the leader server
/// signals a client disconnect.
async fn drive_disconnect(agent: &MvpAgent, sid: &acp::SessionId) {
drive_disconnect_many(agent, &[sid]).await;
}
/// Like `drive_disconnect`, but evicts several sessions in a single
/// `x.ai/internal/evict_sessions` notification — the realistic shape of a
/// `kigi/internal/evict_sessions` notification — the realistic shape of a
/// real client disconnect, and the path that exercises `handle_evict_sessions`'
/// concurrent `join_all` check pass followed by the sequential act pass.
async fn drive_disconnect_many(agent: &MvpAgent, sids: &[&acp::SessionId]) {
@@ -2166,13 +2166,13 @@ async fn drive_disconnect_many(agent: &MvpAgent, sids: &[&acp::SessionId]) {
let raw = serde_json::value::to_raw_value(&params).unwrap();
agent
.ext_notification(acp::ExtNotification::new(
"x.ai/internal/evict_sessions",
"kigi/internal/evict_sessions",
raw.into(),
))
.await
.expect("evict_sessions notification must be handled");
}
/// Drive `x.ai/session/close` through the real `ext_method` dispatch
/// Drive `kigi/session/close` through the real `ext_method` dispatch
/// (`ext_method` → `handlers::session::handle` → `handle_session_close`),
/// exercising the exact production path that finalizes the replica.
async fn drive_close(agent: &MvpAgent, session_id: &str) -> Result<acp::ExtResponse, acp::Error> {
@@ -2181,7 +2181,7 @@ async fn drive_close(agent: &MvpAgent, session_id: &str) -> Result<acp::ExtRespo
let raw = serde_json::value::to_raw_value(&params).unwrap();
agent
.ext_method(acp::ExtRequest::new(
"x.ai/session/close",
"kigi/session/close",
std::sync::Arc::from(raw),
))
.await
@@ -2423,7 +2423,7 @@ fn disconnect_keeps_resident_when_plan_approval_parked() {
);
});
}
/// Mixed batch in a *single* `x.ai/internal/evict_sessions` notification —
/// Mixed batch in a *single* `kigi/internal/evict_sessions` notification —
/// the realistic disconnect shape and the path that exercises
/// `handle_evict_sessions`' `join_all` two-pass (concurrent `IsBusy` checks,
/// then sequential act). One session's actor reports busy (→ kept resident,
@@ -2512,7 +2512,7 @@ fn session_live_state_map_is_bounded_across_cycles() {
});
}
/// Finalize fires on a genuine terminal close — driven through the **real**
/// `x.ai/session/close` dispatch (`ext_method` → `handle_session_close`),
/// `kigi/session/close` dispatch (`ext_method` → `handle_session_close`),
/// not the internal helper. Proves finalize was *moved* (not removed) and
/// guards the handler's `existed` gate. (Finalize assertion is
/// invocation-level; see note in `finalize_session_replica`.)
@@ -2663,7 +2663,7 @@ fn reload_after_terminal_removal_starts_clean() {
}
/// Build an agent whose gateway is wired to a live receiver, so a test can
/// observe (and answer) agent→client reverse-requests like the dormant
/// `x.ai/folder_trust/request` round-trip.
/// `kigi/folder_trust/request` round-trip.
fn build_agent_with_gateway_rx() -> (
MvpAgent,
tokio::sync::mpsc::UnboundedReceiver<kigi_acp_lib::AcpClientMessage>,
@@ -2698,7 +2698,7 @@ fn folder_trust_on() -> crate::util::config::RemoteSettings {
..Default::default()
}
}
/// Pull the next `x.ai/folder_trust/request` reverse-request off the gateway and
/// Pull the next `kigi/folder_trust/request` reverse-request off the gateway and
/// answer it with `outcome`. Returns the request's decoded params.
async fn answer_folder_trust_request(
gw_rx: &mut tokio::sync::mpsc::UnboundedReceiver<kigi_acp_lib::AcpClientMessage>,
@@ -2711,7 +2711,7 @@ async fn answer_folder_trust_request(
let kigi_acp_lib::AcpClientMessage::ExtMethod(args) = msg else {
panic!("expected an ext_method reverse-request, got a different message");
};
assert_eq!(args.request.method.as_ref(), "x.ai/folder_trust/request");
assert_eq!(args.request.method.as_ref(), "kigi/folder_trust/request");
let params: serde_json::Value = serde_json::from_str(args.request.params.get()).unwrap();
let resp: acp::ExtResponse = acp::ExtResponse::new(std::sync::Arc::from(
serde_json::value::to_raw_value(&serde_json::json!({ "outcome" : outcome })).unwrap(),
@@ -3142,21 +3142,21 @@ mod direct_hub_cloud_removed {
}
#[test]
fn cloud_server_id_meta_is_hard_error() {
let meta = serde_json::json!({ "x.ai/cloud_server_id" : "srv-123" });
let meta = serde_json::json!({ "kigi/cloud_server_id" : "srv-123" });
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("must reject");
assert_direct_hub_error(err);
}
#[test]
fn cloud_server_id_null_still_present_is_hard_error() {
let meta = serde_json::json!({ "x.ai/cloud_server_id" : null });
let meta = serde_json::json!({ "kigi/cloud_server_id" : null });
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("must reject");
assert_direct_hub_error(err);
}
#[test]
fn cloud_server_id_with_gateway_meta_still_hard_error() {
let meta = serde_json::json!(
{ "x.ai/cloud_server_id" : "srv-legacy", "envId" : "env-1",
"x.ai/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
{ "kigi/cloud_server_id" : "srv-legacy", "envId" : "env-1",
"kigi/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
"/workspace" } }
);
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("Direct stamp wins");
@@ -3173,7 +3173,7 @@ mod direct_hub_cloud_removed {
assert!(
reject_direct_hub_cloud_meta(
serde_json::json!({
"x.ai/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
"kigi/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
"/workspace" } })
.as_object()
)
@@ -3212,7 +3212,7 @@ mod soft_default_settings_emit {
let kigi_acp_lib::AcpClientMessage::ExtNotification(args) = msg else {
panic!("expected ExtNotification, got {msg:?}");
};
assert_eq!(args.request.method.as_ref(), "x.ai/settings/update");
assert_eq!(args.request.method.as_ref(), "kigi/settings/update");
let params: serde_json::Value =
serde_json::from_str(args.request.params.get()).expect("parse params");
assert_eq!(
@@ -5,8 +5,8 @@
//! actors) plus recently-touched on-disk (`Dormant`) sessions. Clients read it
//! two ways:
//!
//! - request/response `x.ai/sessions/list` → `{ "sessions": [RosterEntry, …] }`
//! - broadcast notification `x.ai/sessions/changed` →
//! - request/response `kigi/sessions/list` → `{ "sessions": [RosterEntry, …] }`
//! - broadcast notification `kigi/sessions/changed` →
//! `{ "upserted": [RosterEntry, …], "removed": ["sess-abc", …] }`
//!
//! The wire shape is intentionally small and current-state only — no event
@@ -76,13 +76,13 @@ pub struct RosterEntry {
pub origin: RosterOrigin,
}
/// Response payload for `x.ai/sessions/list`.
/// Response payload for `kigi/sessions/list`.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct RosterListResponse {
pub sessions: Vec<RosterEntry>,
}
/// Params payload for the `x.ai/sessions/changed` broadcast notification.
/// Params payload for the `kigi/sessions/changed` broadcast notification.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct RosterChanged {
#[serde(default)]
@@ -92,8 +92,8 @@ pub struct RosterChanged {
}
/// JSON-RPC method names for the roster API.
pub const SESSIONS_LIST_METHOD: &str = "x.ai/sessions/list";
pub const SESSIONS_CHANGED_METHOD: &str = "x.ai/sessions/changed";
pub const SESSIONS_LIST_METHOD: &str = "kigi/sessions/list";
pub const SESSIONS_CHANGED_METHOD: &str = "kigi/sessions/changed";
/// Merge live `resident` rows with on-disk `summaries` into the sorted roster.
/// Pure, so it is unit-testable without disk or a live actor.
@@ -173,7 +173,7 @@ mod merge_roster_tests {
title: None,
cwd: format!("/live/{id}"),
is_worktree: false,
model_id: Some("grok-4".into()),
model_id: Some("kigi-4".into()),
reasoning_effort: None,
yolo: false,
activity,
@@ -1,7 +1,7 @@
//! WebSocket server for remote agent connections.
//!
//! This module provides a WebSocket server that allows remote TUI clients to
//! connect to a grok agent running on a different machine.
//! connect to a kigi agent running on a different machine.
//!
//! The agent persists across WebSocket reconnections: a single MvpAgent instance
//! is created on first connection and reused for all subsequent connections. This
@@ -25,7 +25,7 @@ pub struct SessionConfigOption {
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GrokSessionDetail {
pub struct KigiSessionDetail {
pub session_id: String,
pub kind: String,
pub cwd: String,
@@ -34,7 +34,7 @@ pub struct GrokSessionDetail {
pub title: Option<String>,
}
impl GrokSessionDetail {
impl KigiSessionDetail {
pub fn build(
session_id: String,
cwd: String,
@@ -124,11 +124,8 @@ mod tests {
#[test]
fn options_have_one_selected_model_and_a_mode_per_effort() {
let models = [
model("grok-build", "Grok Build"),
model("grok-4.5", "Grok 4.5"),
];
let current = acp::ModelId::from("grok-build");
let models = [model("kigi", "Kigi"), model("kigi-4.5", "Kigi 4.5")];
let current = acp::ModelId::from("kigi");
let opts = build_session_config_options(
&models,
&current,
@@ -140,7 +137,7 @@ mod tests {
assert_eq!(model_opts.len(), 2);
let selected_models: Vec<_> = model_opts.iter().filter(|o| o.selected).collect();
assert_eq!(selected_models.len(), 1);
assert_eq!(selected_models[0].id, "grok-build");
assert_eq!(selected_models[0].id, "kigi");
let mode_opts: Vec<_> = opts.iter().filter(|o| o.category == "mode").collect();
assert_eq!(mode_opts.len(), SELECTABLE_REASONING_EFFORTS.len());
@@ -153,8 +150,8 @@ mod tests {
#[test]
fn none_effort_is_not_a_user_selectable_mode() {
assert!(!SELECTABLE_REASONING_EFFORTS.contains(&ReasoningEffort::None));
let models = [model("grok-build", "Grok Build")];
let current = acp::ModelId::from("grok-build");
let models = [model("kigi", "Kigi")];
let current = acp::ModelId::from("kigi");
let opts = build_session_config_options(
&models,
&current,
@@ -168,8 +165,8 @@ mod tests {
#[test]
fn no_mode_options_when_model_lacks_effort_support() {
let models = [model("grok-build", "Grok Build")];
let current = acp::ModelId::from("grok-build");
let models = [model("kigi", "Kigi")];
let current = acp::ModelId::from("kigi");
let opts = build_session_config_options(&models, &current, &[], None);
assert_eq!(opts.len(), 1);
assert!(opts.iter().all(|o| o.category == "model"));
@@ -177,42 +174,42 @@ mod tests {
#[test]
fn model_label_falls_back_to_id_when_name_empty() {
let models = [model("grok-build", "")];
let current = acp::ModelId::from("grok-build");
let models = [model("kigi", "")];
let current = acp::ModelId::from("kigi");
let opts = build_session_config_options(&models, &current, &[], None);
assert_eq!(opts[0].label, "grok-build");
assert_eq!(opts[0].label, "kigi");
}
#[test]
fn session_config_option_serializes_camel_case() {
let opt = SessionConfigOption {
id: "grok-build".to_string(),
id: "kigi".to_string(),
category: "model".to_string(),
label: "Grok Build".to_string(),
label: "Kigi".to_string(),
description: None,
selected: true,
};
let v = serde_json::to_value(&opt).expect("serialize");
assert_eq!(v["id"], "grok-build");
assert_eq!(v["id"], "kigi");
assert_eq!(v["category"], "model");
assert_eq!(v["label"], "Grok Build");
assert_eq!(v["label"], "Kigi");
assert_eq!(v["selected"], true);
assert!(v.get("description").is_none());
}
#[test]
fn grok_session_detail_serializes_camel_case() {
let detail = GrokSessionDetail::build(
fn kigi_session_detail_serializes_camel_case() {
let detail = KigiSessionDetail::build(
"sess-1".to_string(),
"/Users/me/xai".to_string(),
"grok-build".to_string(),
"kigi".to_string(),
None,
);
let v = serde_json::to_value(&detail).expect("serialize");
assert_eq!(v["sessionId"], "sess-1");
assert_eq!(v["kind"], "build");
assert_eq!(v["cwd"], "/Users/me/xai");
assert_eq!(v["currentModelId"], "grok-build");
assert_eq!(v["currentModelId"], "kigi");
assert!(v.get("title").is_none());
}
}
@@ -15,7 +15,7 @@ use crate::session::{
use crate::terminal::AsyncTerminalRunner;
use crate::tools::ToolContext;
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
use kigi_tools::implementations::grok_build::task::types::*;
use kigi_tools::implementations::kigi::task::types::*;
use kigi_workspace::file_system::AsyncFileSystem;
use kigi_hunk_tracker::HunkTrackerHandle;
use super::*;
@@ -156,8 +156,8 @@ impl SubagentCoordinator {
pub fn outstanding_reply_for_prompt(
&self,
prompt_id: &str,
) -> kigi_tools::implementations::grok_build::task::types::SubagentOutstandingReply {
kigi_tools::implementations::grok_build::task::types::SubagentOutstandingReply {
) -> kigi_tools::implementations::kigi::task::types::SubagentOutstandingReply {
kigi_tools::implementations::kigi::task::types::SubagentOutstandingReply {
live_ids: self.outstanding_for_prompt(prompt_id),
background_live: self.background_live_for_prompt(prompt_id),
subagent_usage_not_applied: self.subagent_usage_not_applied(prompt_id),
@@ -15,7 +15,7 @@ use crate::session::{
use crate::terminal::AsyncTerminalRunner;
use crate::tools::ToolContext;
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
use kigi_tools::implementations::grok_build::task::types::*;
use kigi_tools::implementations::kigi::task::types::*;
use kigi_workspace::file_system::AsyncFileSystem;
use kigi_hunk_tracker::HunkTrackerHandle;
use super::*;
@@ -323,7 +323,7 @@ impl SubagentCoordinator {
/// asynchronously after dropping the coordinator borrow.
///
/// Returns an empty `Vec` if no active subagents match the given
/// parent session ID. Callers (e.g. the `x.ai/subagent/list_running`
/// parent session ID. Callers (e.g. the `kigi/subagent/list_running`
/// ACP handler) should treat an empty result as a normal "no running
/// subagents" response, not an error.
pub(crate) fn list_running_for_parent(
@@ -15,7 +15,7 @@ use crate::session::{
use crate::terminal::AsyncTerminalRunner;
use crate::tools::ToolContext;
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
use kigi_tools::implementations::grok_build::task::types::*;
use kigi_tools::implementations::kigi::task::types::*;
use kigi_workspace::file_system::AsyncFileSystem;
use kigi_hunk_tracker::HunkTrackerHandle;
use super::*;
@@ -306,7 +306,7 @@ pub(crate) async fn handle_subagent_request(
subagent_id = % request.id, error = % e,
"Could not resolve worktree base dir, using temp dir for subagent worktree"
);
std::env::temp_dir().join("grok-subagent-worktrees").join(&request.id)
std::env::temp_dir().join("kigi-subagent-worktrees").join(&request.id)
}
};
let source_clone = source_cwd;
@@ -396,7 +396,7 @@ pub(crate) async fn handle_subagent_request(
);
}
{
use kigi_tools::implementations::grok_build::task::MAX_SUBAGENT_DEPTH;
use kigi_tools::implementations::kigi::task::MAX_SUBAGENT_DEPTH;
use kigi_tools::types::tool::ToolKind;
let child_depth = ctx.parent_depth + 1;
if child_depth >= MAX_SUBAGENT_DEPTH {
@@ -754,10 +754,10 @@ pub(crate) async fn handle_subagent_request(
}
}
if let Some(scope) = agent_memory_scope {
use kigi_tools::implementations::grok_build;
use kigi_tools::implementations::kigi;
use kigi_tools::implementations::opencode;
let memory_tools: Vec<kigi_tools::registry::types::ToolConfig> = vec![
(& grok_build::ReadFileTool).into(), (& grok_build::SearchReplaceTool)
(& kigi::ReadFileTool).into(), (& kigi::SearchReplaceTool)
.into(), (& opencode::OpenCodeWriteTool).into(),
];
for tc in memory_tools {
@@ -1570,7 +1570,7 @@ pub(crate) async fn handle_subagent_request(
let mut worktree_removed = false;
if let Some(ref wt_path) = worktree_path {
if snapshot_dispose_enabled {
let ref_name = format!("refs/grok/subagents/{}", request.id);
let ref_name = format!("refs/kigi/subagents/{}", request.id);
let source_repo = resolve_subagent_source_repo(&ctx);
match crate::session::worktree::snapshot_subagent_worktree(
wt_path,
@@ -23,7 +23,7 @@ use crate::tools::ToolContext;
use agent_client_protocol as acp;
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
use kigi_hunk_tracker::HunkTrackerHandle;
use kigi_tools::implementations::grok_build::task::types::*;
use kigi_tools::implementations::kigi::task::types::*;
use kigi_workspace::file_system::AsyncFileSystem;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -197,7 +197,7 @@ pub(crate) struct SubagentSpawnContext {
/// Parent's scheduler handle. When `Some`, the subagent reuses the
/// parent's scheduler actor so scheduled tasks survive subagent exit.
pub parent_scheduler_handle:
Option<kigi_tools::implementations::grok_build::scheduler::types::SchedulerHandle>,
Option<kigi_tools::implementations::kigi::scheduler::types::SchedulerHandle>,
/// Parent's session environment variables (.envrc + color settings).
/// Shared so the child inherits the same env without re-loading.
pub session_env: Arc<HashMap<String, String>>,
@@ -207,10 +207,10 @@ pub(crate) struct SubagentSpawnContext {
/// Resolved sampling config for web_search.
pub web_search_config: kigi_tools::implementations::WebSearchConfig,
/// Resolved config for web fetch.
pub web_fetch_config: kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig,
pub web_fetch_config: kigi_tools::implementations::kigi::web_fetch::WebFetchConfig,
/// Resolved config for the deploy service.
pub app_builder_deployer_config:
kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig,
kigi_tools::implementations::kigi::deploy_app::AppBuilderDeployerConfig,
/// Whether the write_file tool is enabled.
pub write_file_enabled: bool,
/// Whether goal mode (`/goal`) is enabled.
@@ -301,7 +301,7 @@ pub(crate) struct SubagentSpawnContext {
/// goes through `agent/config.rs::sampling_config_for_model`
/// which always sets that field to `None`.
pub attribution_callback: Option<kigi_sampler::SharedAttributionCallback>,
/// Parent session's agent name (e.g. "grok-build").
/// Parent session's agent name (e.g. "kigi").
pub parent_agent_name: Option<String>,
/// `agent_type` of the parent's current model — the harness-flavor fallback
/// when `parent_agent_name` is not a recognized harness, e.g. a custom
@@ -1639,12 +1639,12 @@ pub(crate) fn subagent_harness_flavor_is_representable(_agent_type: &str) -> boo
/// Apply the harness-dependent toolset/prompt re-selection to a resolved
/// agent definition.
///
/// The harness flavor (alternate vs grok-build) normally follows the PARENT
/// agent: `GrokBuildOrchestrator` parents give children
/// The harness flavor (alternate vs kigi) normally follows the PARENT
/// agent: `KigiOrchestrator` parents give children
/// the alternate harness; the orchestrator keeps children lean, and other parents
/// inherit the file-tool override (hashline vs standard). A `/goal` role may
/// pass `harness_agent_type` to OVERRIDE that flavor regardless of the parent
/// (so a grok-build session can run an alternate-harness verifier and vice-versa);
/// (so a kigi session can run an alternate-harness verifier and vice-versa);
/// `None` for every non-goal spawn ⇒ the parent decides (unchanged). The base
/// toolset stays role-dependent on `subagent_type` (general-purpose →
/// implementer, else explorer), so the role keeps a capable toolset on the
@@ -2170,7 +2170,7 @@ fn emit_subagent_notification(
.ok();
if let Some(params) = params {
let ext_notification =
acp::ExtNotification::new("x.ai/session_notification", params.into());
acp::ExtNotification::new("kigi/session_notification", params.into());
gateway.forward_fire_and_forget(ext_notification);
}
}
@@ -2224,7 +2224,7 @@ fn goal_tick_cmd_tx(
///
/// Notifications are **not** persisted to JSONL — they are transient UI
/// hints, not authoritative lifecycle events. The TUI can resync via
/// `x.ai/subagent/list_running` on reconnect.
/// `kigi/subagent/list_running` on reconnect.
fn spawn_progress_publisher(
signals_handle: crate::session::signals::SessionSignalsHandle,
gateway: GatewaySender,
@@ -2289,7 +2289,7 @@ fn spawn_progress_publisher(
}
if let Some(params) = params {
let ext_notification =
acp::ExtNotification::new("x.ai/session_notification", params.into());
acp::ExtNotification::new("kigi/session_notification", params.into());
gateway.forward_fire_and_forget(ext_notification);
}
}
@@ -119,11 +119,11 @@ fn subagent_max_turns_definition_wins_else_inherits_parent() {
fn resume_worktree_action_covers_three_outcomes() {
use super::{ResumeWorktreeAction, resume_worktree_action};
assert_eq!(
resume_worktree_action(true, Some("refs/grok/subagents/x")),
resume_worktree_action(true, Some("refs/kigi/subagents/x")),
ResumeWorktreeAction::Rehydrate
);
assert_eq!(
resume_worktree_action(false, Some("refs/grok/subagents/x")),
resume_worktree_action(false, Some("refs/kigi/subagents/x")),
ResumeWorktreeAction::Rehydrate
);
assert_eq!(resume_worktree_action(true, None), ResumeWorktreeAction::Reuse);
@@ -1099,7 +1099,7 @@ fn dummy_tracker(
force_compact: Arc::new(AtomicBool::new(false)),
permission_handle: kigi_workspace::permission::PermissionHandle::allow_all(),
attribution_callback: None,
agent_name: "grok-build".to_string(),
agent_name: "kigi".to_string(),
session_default_agent_profile: None,
allowed_subagent_types: None,
hook_registry: None,
@@ -1938,7 +1938,7 @@ async fn bootstrap_fork_live_parent_chat_state_is_forked_with_marker() {
const MARKER: &str = "UNIQUE_LIVE_FORK_MARKER_xyz789";
let req = bootstrap_test_request(true);
let mut ctx = ctx_with_toggle(HashMap::new());
let chat = spawn_test_parent_chat_state("grok-4.5");
let chat = spawn_test_parent_chat_state("kigi-4.5");
chat.replace_conversation(
vec![
ConversationItem::system("parent system"),
@@ -2127,7 +2127,7 @@ async fn handle_subagent_request_rejects_nonexistent_cwd() {
#[tokio::test]
async fn handle_subagent_request_rejects_file_as_cwd() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp_file = tmp_dir.path().join("grok-test-cwd-file");
let tmp_file = tmp_dir.path().join("kigi-test-cwd-file");
std::fs::write(&tmp_file, b"not a directory").unwrap();
let ctx = ctx_with_toggle(HashMap::new());
let coordinator = std::cell::RefCell::new(SubagentCoordinator::new());
@@ -2359,7 +2359,7 @@ fn subagent_await_budget_default_and_override() {
fn summarize_tool_config_uses_name_override_and_strips_namespace() {
use kigi_tools::registry::types::{ToolConfig, ToolServerConfig};
use kigi_tools::types::tool::ToolKind;
let mut read = ToolConfig::from_id("GrokBuild:read_file");
let mut read = ToolConfig::from_id("Kigi:read_file");
read.kind = Some(ToolKind::Read);
let mut read_dup = ToolConfig::from_id("Codex:read_file");
read_dup.kind = Some(ToolKind::Read);
@@ -2410,7 +2410,7 @@ fn describe_subagent_type_not_allowed_outside_allow_list() {
other => panic!("expected NotAllowed, got {other:?}"),
}
}
/// Regression: on the DEFAULT grok-build host —
/// Regression: on the DEFAULT kigi host —
/// the primary `/goal` host — the `general-purpose` toolset's only
/// file-mutator is `search_replace` (`ToolKind::Edit`); the `write`
/// tool (`ToolKind::Write`) is injection-only and absent from the
@@ -2465,7 +2465,7 @@ fn subagent_keeps_default_flavor_when_parent_model_is_non_strict() {
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.parent_agent_name = Some("ai-oncall-bot".to_string());
ctx.parent_model_agent_type = Some(
BuiltinAgentName::GrokBuildPlan.as_ref().to_string(),
BuiltinAgentName::KigiPlan.as_ref().to_string(),
);
let mut def = resolve_agent_definition("general-purpose", &ctx).expect("resolves");
resolve_subagent_toolset("general-purpose", None, &ctx, &mut def);
@@ -2696,7 +2696,7 @@ async fn background_unknown_type_emits_subagent_finished_notification() {
while let Ok(msg) = gateway_rx.try_recv() {
if let kigi_acp_lib::AcpClientMessage::ExtNotification(args) = msg {
let req: &acp::ExtNotification = &args.request;
assert_eq!(req.method.as_ref(), "x.ai/session_notification");
assert_eq!(req.method.as_ref(), "kigi/session_notification");
let body = req.params.get();
assert!(body.contains("subagent_finished"));
assert!(body.contains(& subagent_id));
@@ -3090,8 +3090,8 @@ fn subagent_auth_type_rule() {
use kigi_chat_state::AuthType;
let session = acp::AuthMethodId::new(CACHED_TOKEN_AUTH_METHOD_ID);
let api_key = acp::AuthMethodId::new(XAI_API_KEY_METHOD_ID);
let byok = byok_model_entry("grok-byok");
let plain = test_model_entry("grok-plain");
let byok = byok_model_entry("kigi-byok");
let plain = test_model_entry("kigi-plain");
assert_eq!(super::subagent_auth_type(Some(& byok), & session), AuthType::ApiKey);
assert_eq!(super::subagent_auth_type(Some(& byok), & api_key), AuthType::ApiKey);
assert_eq!(
@@ -3104,14 +3104,14 @@ fn subagent_auth_type_rule() {
#[test]
fn fresh_tool_model_accepts_visible_key_and_internal_id() {
let mut models = indexmap::IndexMap::new();
models.insert("grok-3".to_string(), test_model_entry("grok-3-2025-02-15"));
models.insert("kigi-3".to_string(), test_model_entry("kigi-3-2025-02-15"));
assert!(
super::handle_request::task_model_override_error(Some("grok-3"),
super::handle_request::task_model_override_error(Some("kigi-3"),
ModelOverrideProvenance::Tool, false, & models, false,).is_none(),
"key lookup should succeed"
);
assert!(
super::handle_request::task_model_override_error(Some("grok-3-2025-02-15"),
super::handle_request::task_model_override_error(Some("kigi-3-2025-02-15"),
ModelOverrideProvenance::Tool, false, & models, false,).is_none(),
"info().model lookup should succeed"
);
@@ -3182,7 +3182,7 @@ fn fresh_tool_model_rejects_unknown_and_nonavailable_entries() {
format!("Unknown Task.model slug '{requested}'. Valid model slugs: alpha, zeta. \
Omit `model` to inherit the parent model.")
);
assert!(! error.contains("grok models"));
assert!(! error.contains("kigi models"));
}
assert!(
super::handle_request::task_model_override_error(Some("oauth-only"),
@@ -343,7 +343,7 @@ fn resumable_source_returns_info_for_completed_subagent() {
child_cwd: "/workspace".into(),
worktree_path: Some(PathBuf::from("/tmp/worktree-1")),
snapshot_ref: None,
effective_model_id: "grok-3".into(),
effective_model_id: "kigi-3".into(),
block_waited: false,
explicitly_killed: false,
},
@@ -479,16 +479,16 @@ fn snapshot_ref_field_in_meta_roundtrips() {
persona: None,
resumed_from: None,
child_cwd: None,
worktree_path: Some("/tmp/grok-wt/sa-snap".into()),
snapshot_ref: Some("refs/grok/subagent-snapshots/sa-snap".into()),
worktree_path: Some("/tmp/kigi-wt/sa-snap".into()),
snapshot_ref: Some("refs/kigi/subagent-snapshots/sa-snap".into()),
effective_model_id: None,
};
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("snapshot_ref"));
assert!(json.contains("refs/grok/subagent-snapshots/sa-snap"));
assert!(json.contains("refs/kigi/subagent-snapshots/sa-snap"));
let parsed: SubagentMeta = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.snapshot_ref.as_deref(), Some("refs/grok/subagent-snapshots/sa-snap")
parsed.snapshot_ref.as_deref(), Some("refs/kigi/subagent-snapshots/sa-snap")
);
}
#[test]
@@ -528,7 +528,7 @@ fn snapshot_test_meta(id: &str) -> SubagentMeta {
persona: None,
resumed_from: None,
child_cwd: None,
worktree_path: Some("/tmp/grok-wt/subagent-x".into()),
worktree_path: Some("/tmp/kigi-wt/subagent-x".into()),
snapshot_ref: None,
effective_model_id: None,
}
@@ -540,14 +540,14 @@ fn update_subagent_meta_snapshot_ref_persists_to_disk() {
let dir = tempfile::TempDir::new().unwrap();
assert!(write_subagent_meta(dir.path(), & snapshot_test_meta("sa-write")));
assert!(
update_subagent_meta_snapshot_ref(dir.path(), "refs/grok/subagents/sa-write",
update_subagent_meta_snapshot_ref(dir.path(), "refs/kigi/subagents/sa-write",
"completed"), "persisting the ref into an existing meta.json must report success"
);
let data = std::fs::read_to_string(dir.path().join("meta.json")).unwrap();
let reread: SubagentMeta = serde_json::from_str(&data).unwrap();
assert_eq!(reread.snapshot_ref.as_deref(), Some("refs/grok/subagents/sa-write"));
assert_eq!(reread.snapshot_ref.as_deref(), Some("refs/kigi/subagents/sa-write"));
assert_eq!(reread.status, "completed");
assert_eq!(reread.worktree_path.as_deref(), Some("/tmp/grok-wt/subagent-x"));
assert_eq!(reread.worktree_path.as_deref(), Some("/tmp/kigi-wt/subagent-x"));
}
/// Missing meta.json → the writer reports failure (it `warn!`s), so the
/// completion path keeps the worktree instead of removing it ref-less.
@@ -555,7 +555,7 @@ fn update_subagent_meta_snapshot_ref_persists_to_disk() {
fn update_subagent_meta_snapshot_ref_reports_failure_when_meta_missing() {
let dir = tempfile::TempDir::new().unwrap();
assert!(
! update_subagent_meta_snapshot_ref(dir.path(), "refs/grok/subagents/sa-missing",
! update_subagent_meta_snapshot_ref(dir.path(), "refs/kigi/subagents/sa-missing",
"completed")
);
}
@@ -569,12 +569,12 @@ fn snapshot_ref_write_promotes_nonterminal_status_to_terminal() {
meta.status = "running".into();
assert!(write_subagent_meta(dir.path(), & meta));
assert!(
update_subagent_meta_snapshot_ref(dir.path(), "refs/grok/subagents/x",
update_subagent_meta_snapshot_ref(dir.path(), "refs/kigi/subagents/x",
"completed")
);
let data = std::fs::read_to_string(dir.path().join("meta.json")).unwrap();
let reread: SubagentMeta = serde_json::from_str(&data).unwrap();
assert_eq!(Some("refs/grok/subagents/x"), reread.snapshot_ref.as_deref());
assert_eq!(Some("refs/kigi/subagents/x"), reread.snapshot_ref.as_deref());
assert_eq!("completed", reread.status);
}
/// The coordinator setter stamps the snapshot ref onto the in-memory
@@ -600,17 +600,17 @@ async fn set_completed_snapshot_ref_updates_in_memory_entry() {
.unwrap();
assert!(before.snapshot_ref.is_none());
coordinator
.set_completed_snapshot_ref("sa-mem", "refs/grok/subagents/sa-mem".into());
.set_completed_snapshot_ref("sa-mem", "refs/kigi/subagents/sa-mem".into());
let after = coordinator
.resumable_source_for("sa-mem", "session-A", Path::new("/tmp"))
.unwrap();
assert_eq!(after.snapshot_ref.as_deref(), Some("refs/grok/subagents/sa-mem"));
assert_eq!(after.snapshot_ref.as_deref(), Some("refs/kigi/subagents/sa-mem"));
}
/// Unknown id is a no-op (entry already TTL-evicted; meta.json still holds it).
#[test]
fn set_completed_snapshot_ref_unknown_id_is_noop() {
let mut coordinator = SubagentCoordinator::new();
coordinator.set_completed_snapshot_ref("ghost", "refs/grok/subagents/ghost".into());
coordinator.set_completed_snapshot_ref("ghost", "refs/kigi/subagents/ghost".into());
assert!(
coordinator.resumable_source_for("ghost", "session-A", Path::new("/tmp"))
.is_none()
@@ -719,7 +719,7 @@ async fn completion_snapshot_sequence_persists_ref_then_removes_worktree() {
let meta_dir = temp.path().join("meta");
write_subagent_meta(&meta_dir, &snapshot_test_meta("glue-1"));
let mut coordinator = coordinator_with_completed("glue-1");
let ref_name = "refs/grok/subagents/glue-1";
let ref_name = "refs/kigi/subagents/glue-1";
let snapshot_ref = crate::session::worktree::snapshot_subagent_worktree(
&wt,
&repo,
@@ -744,7 +744,7 @@ async fn completion_snapshot_sequence_persists_ref_then_removes_worktree() {
/// the tracker-retained direct `worktree_path` plus the snapshot_ref.
#[tokio::test]
async fn gate_on_completion_clears_model_facing_worktree_path_but_resume_retains_it() {
let wt = PathBuf::from("/tmp/grok-wt/subagent-disp-1");
let wt = PathBuf::from("/tmp/kigi-wt/subagent-disp-1");
let mut coordinator = SubagentCoordinator::new();
let mut tracker = dummy_tracker("disp-1", "session-A", "explore", "task");
tracker.worktree_path = Some(wt.clone());
@@ -762,21 +762,21 @@ async fn gate_on_completion_clears_model_facing_worktree_path_but_resume_retains
}
coordinator.move_to_completed("disp-1", "task".into(), "explore".into(), result);
coordinator
.set_completed_snapshot_ref("disp-1", "refs/grok/subagents/disp-1".into());
.set_completed_snapshot_ref("disp-1", "refs/kigi/subagents/disp-1".into());
let listed = coordinator.completed.get("disp-1").expect("completed entry");
assert_eq!(None, listed.result.worktree_path);
let src = coordinator
.resumable_source_for("disp-1", "session-A", Path::new("/tmp"))
.unwrap();
assert_eq!(Some(wt), src.worktree_path);
assert_eq!(Some("refs/grok/subagents/disp-1"), src.snapshot_ref.as_deref());
assert_eq!(Some("refs/kigi/subagents/disp-1"), src.snapshot_ref.as_deref());
}
/// Gate on but the worktree was NOT removed (snapshot/persist/remove failed):
/// the model-facing `result.worktree_path` is RETAINED so the parent can still
/// locate the preserved dir.
#[tokio::test]
async fn gate_on_completion_retains_worktree_path_when_not_removed() {
let wt = PathBuf::from("/tmp/grok-wt/subagent-keep-1");
let wt = PathBuf::from("/tmp/kigi-wt/subagent-keep-1");
let mut coordinator = SubagentCoordinator::new();
coordinator.insert(dummy_tracker("keep-1", "session-A", "explore", "task"));
let mut result = SubagentResult {
@@ -817,7 +817,7 @@ async fn disposal_completes_before_subagent_is_observable() {
write_subagent_meta(&meta_dir, &snapshot_test_meta("order-1"));
let mut coordinator = SubagentCoordinator::new();
coordinator.insert(dummy_tracker("order-1", "session-A", "explore", "task"));
let ref_name = "refs/grok/subagents/order-1";
let ref_name = "refs/kigi/subagents/order-1";
let snapshot_ref = crate::session::worktree::snapshot_subagent_worktree(
&wt,
&repo,
@@ -989,7 +989,7 @@ fn resume_inherited_cwd_requires_existing_non_worktree_dir() {
};
assert_eq!(resume_inherited_cwd(Some(& present)), Some(existing.as_str()));
let missing = ResumeSourceData {
child_cwd: "/no/such/dir/grok-missing".into(),
child_cwd: "/no/such/dir/kigi-missing".into(),
..present.clone()
};
assert_eq!(resume_inherited_cwd(Some(& missing)), None);
@@ -1120,7 +1120,7 @@ fn token_estimation_accounts_for_images() {
#[test]
fn durable_fallback_roundtrips_child_cwd_and_worktree() {
let dir = std::env::temp_dir()
.join("grok-test-durable-resume")
.join("kigi-test-durable-resume")
.join(uuid::Uuid::now_v7().to_string());
let _ = std::fs::create_dir_all(&dir);
let meta = SubagentMeta {
@@ -1143,22 +1143,22 @@ fn durable_fallback_roundtrips_child_cwd_and_worktree() {
persona: Some("implementer".into()),
resumed_from: None,
child_cwd: Some("/workspace/project".into()),
worktree_path: Some("/tmp/grok-wt/sa-dur".into()),
worktree_path: Some("/tmp/kigi-wt/sa-dur".into()),
snapshot_ref: None,
effective_model_id: Some("grok-3".into()),
effective_model_id: Some("kigi-3".into()),
};
write_subagent_meta(&dir, &meta);
let data = std::fs::read_to_string(dir.join("meta.json")).unwrap();
let loaded: SubagentMeta = serde_json::from_str(&data).unwrap();
assert_eq!(loaded.child_cwd.as_deref(), Some("/workspace/project"));
assert_eq!(loaded.worktree_path.as_deref(), Some("/tmp/grok-wt/sa-dur"));
assert_eq!(loaded.worktree_path.as_deref(), Some("/tmp/kigi-wt/sa-dur"));
assert_eq!(loaded.status, "completed");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn durable_fallback_rejects_running_status() {
let dir = std::env::temp_dir()
.join("grok-test-durable-status")
.join("kigi-test-durable-status")
.join(uuid::Uuid::now_v7().to_string());
let parent_dir = dir.join("subagents").join("sa-running");
let _ = std::fs::create_dir_all(&parent_dir);
@@ -1227,7 +1227,7 @@ fn drain_cancelled_finish_broadcasts(
let kigi_acp_lib::AcpClientMessage::ExtNotification(args) = msg else {
continue;
};
assert_eq!(args.request.method.as_ref(), "x.ai/session_notification");
assert_eq!(args.request.method.as_ref(), "kigi/session_notification");
let notification: SessionNotification = serde_json::from_str(
args.request.params.get(),
)
@@ -1666,11 +1666,11 @@ fn resume_allows_matching_identity() {
snapshot_ref: None,
subagent_type: "general-purpose".into(),
persona: Some("implementer".into()),
model_id: Some("grok-3".into()),
model_id: Some("kigi-3".into()),
};
assert_eq!("general-purpose", source.subagent_type);
assert_eq!(Some("implementer"), source.persona.as_deref());
assert_eq!(Some("grok-3"), source.model_id.as_deref());
assert_eq!(Some("kigi-3"), source.model_id.as_deref());
}
#[test]
fn resume_identity_does_not_gate_on_model() {
@@ -1682,21 +1682,21 @@ fn resume_identity_does_not_gate_on_model() {
snapshot_ref: None,
subagent_type: "general-purpose".into(),
persona: None,
model_id: Some("grok-3".into()),
model_id: Some("kigi-3".into()),
};
assert!(
kigi_subagent_resolution::validate_resume_identity("general-purpose", None, &
source,).is_ok()
);
assert_eq!(
source.model_id.as_deref(), Some("grok-3"),
source.model_id.as_deref(), Some("kigi-3"),
"source model remains available for pinning"
);
}
#[test]
fn durable_meta_roundtrips_effective_model_id() {
let dir = std::env::temp_dir()
.join("grok-test-model-roundtrip")
.join("kigi-test-model-roundtrip")
.join(uuid::Uuid::now_v7().to_string());
let _ = std::fs::create_dir_all(&dir);
let meta = SubagentMeta {
@@ -1721,24 +1721,24 @@ fn durable_meta_roundtrips_effective_model_id() {
child_cwd: Some("/workspace".into()),
worktree_path: None,
snapshot_ref: None,
effective_model_id: Some("grok-3".into()),
effective_model_id: Some("kigi-3".into()),
};
write_subagent_meta(&dir, &meta);
let data = std::fs::read_to_string(dir.join("meta.json")).unwrap();
let loaded: SubagentMeta = serde_json::from_str(&data).unwrap();
assert_eq!(
loaded.effective_model_id.as_deref(), Some("grok-3"),
loaded.effective_model_id.as_deref(), Some("kigi-3"),
"model ID should round-trip through meta.json"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn resume_model_pinning_overrides_default_resolution() {
let source_model = Some("grok-3".to_string());
let resolved_model = "grok-light";
let source_model = Some("kigi-3".to_string());
let resolved_model = "kigi-light";
let needs_pin = source_model.as_deref() != Some(resolved_model);
assert!(needs_pin, "resolved model differs from source — pinning should trigger");
let resolved_same = "grok-3";
let resolved_same = "kigi-3";
let no_pin = source_model.as_deref() == Some(resolved_same);
assert!(no_pin, "same model — no pinning needed");
}
@@ -2108,19 +2108,19 @@ fn ctx_with_parent_chat_state(
#[tokio::test]
async fn read_parent_sampling_config_keeps_auto_catalog_id_with_routing_slug() {
let mut models = indexmap::IndexMap::new();
models.insert("auto".to_string(), test_model_entry("grok-4.5"));
let ctx = ctx_with_parent_chat_state("auto", "grok-4.5", "composer-2-fast", models);
models.insert("auto".to_string(), test_model_entry("kigi-4.5"));
let ctx = ctx_with_parent_chat_state("auto", "kigi-4.5", "composer-2-fast", models);
let (config, model_id) = read_parent_sampling_config(&ctx).await;
assert_eq!(config.model, "grok-4.5");
assert_eq!(config.model, "kigi-4.5");
assert_eq!(model_id.0.as_ref(), "auto");
}
#[tokio::test]
async fn read_parent_sampling_config_keeps_auto_when_catalog_has_slug_key_only() {
let mut models = indexmap::IndexMap::new();
models.insert("grok-4.5".to_string(), test_model_entry("grok-4.5"));
let ctx = ctx_with_parent_chat_state("auto", "grok-4.5", "auto", models);
models.insert("kigi-4.5".to_string(), test_model_entry("kigi-4.5"));
let ctx = ctx_with_parent_chat_state("auto", "kigi-4.5", "auto", models);
let (config, model_id) = read_parent_sampling_config(&ctx).await;
assert_eq!(config.model, "grok-4.5");
assert_eq!(config.model, "kigi-4.5");
assert_eq!(model_id.0.as_ref(), "auto");
}
#[tokio::test]
@@ -2161,11 +2161,11 @@ async fn read_parent_sampling_config_ignores_global_default() {
}
#[tokio::test]
async fn read_parent_sampling_config_resolves_backend_search_from_catalog() {
let mut entry = test_model_entry("grok-4.5");
let mut entry = test_model_entry("kigi-4.5");
entry.info.supports_backend_search = true;
let mut models = indexmap::IndexMap::new();
models.insert("auto".to_string(), entry);
let mut ctx = ctx_with_parent_chat_state("auto", "grok-4.5", "auto", models);
let mut ctx = ctx_with_parent_chat_state("auto", "kigi-4.5", "auto", models);
ctx.sampling_config.supports_backend_search = false;
let (config, _model_id) = read_parent_sampling_config(&ctx).await;
assert!(
@@ -2201,11 +2201,11 @@ async fn read_parent_sampling_config_fallback_resolves_backend_search_from_catal
#[tokio::test]
async fn read_parent_sampling_config_resolves_compactions_remaining_from_catalog() {
use kigi_sampling_types::CompactionsRemaining;
let mut entry = test_model_entry("grok-4.5");
let mut entry = test_model_entry("kigi-4.5");
entry.info.compactions_remaining = Some(CompactionsRemaining::Dynamic(true));
let mut models = indexmap::IndexMap::new();
models.insert("auto".to_string(), entry);
let mut ctx = ctx_with_parent_chat_state("auto", "grok-4.5", "auto", models);
let mut ctx = ctx_with_parent_chat_state("auto", "kigi-4.5", "auto", models);
ctx.sampling_config.compactions_remaining = None;
let (config, _model_id) = read_parent_sampling_config(&ctx).await;
assert_eq!(
@@ -2360,7 +2360,7 @@ async fn fork_context_pins_parent_model_over_overrides() {
#[tokio::test]
async fn resolve_subagent_inherits_parent_model_without_pins() {
use kigi_agent::config::ModelOverride;
for parent_model in ["grok-4.5", "composer-2-fast", "my-custom-byok-model"] {
for parent_model in ["kigi-4.5", "composer-2-fast", "my-custom-byok-model"] {
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = parent_model.to_string();
ctx.model_id = acp::ModelId::new(parent_model);
@@ -2379,12 +2379,12 @@ async fn resolve_subagent_inherits_parent_model_without_pins() {
}
/// An explicit `[subagents.models]` pin routes the subagent to that
/// model regardless of the parent model — both a light parent
/// (`grok-4.5`) and a custom parent (`composer-2-fast`)
/// (`kigi-4.5`) and a custom parent (`composer-2-fast`)
/// honor the pin identically now that the heavy-model gate is gone.
#[tokio::test]
async fn resolve_subagent_config_override_pin_applies_for_any_parent() {
use kigi_agent::config::ModelOverride;
for parent_model in ["grok-4.5", "composer-2-fast"] {
for parent_model in ["kigi-4.5", "composer-2-fast"] {
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = parent_model.to_string();
ctx.model_id = acp::ModelId::new(parent_model);
@@ -2411,8 +2411,8 @@ async fn resolve_subagent_config_override_pin_applies_for_any_parent() {
async fn resolve_subagent_agent_definition_pin_applies_for_light_parent() {
use kigi_agent::config::ModelOverride;
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = "grok-4.5".to_string();
ctx.model_id = acp::ModelId::new("grok-4.5");
ctx.sampling_config.model = "kigi-4.5".to_string();
ctx.model_id = acp::ModelId::new("kigi-4.5");
ctx.available_models
.insert("pinned-model".to_string(), test_model_entry("pinned-model"));
let agent_model = ModelOverride::Override("pinned-model".to_string());
@@ -2431,8 +2431,8 @@ async fn resolve_subagent_agent_definition_pin_applies_for_light_parent() {
async fn resolve_subagent_config_override_wins_over_agent_definition() {
use kigi_agent::config::ModelOverride;
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = "grok-4.5".to_string();
ctx.model_id = acp::ModelId::new("grok-4.5");
ctx.sampling_config.model = "kigi-4.5".to_string();
ctx.model_id = acp::ModelId::new("kigi-4.5");
ctx.available_models
.insert("config-pin".to_string(), test_model_entry("config-pin"));
ctx.available_models
@@ -2454,8 +2454,8 @@ async fn resolve_subagent_config_override_wins_over_agent_definition() {
async fn resolve_subagent_config_override_unknown_model_falls_through_to_inherit() {
use kigi_agent::config::ModelOverride;
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = "grok-4.5".to_string();
ctx.model_id = acp::ModelId::new("grok-4.5");
ctx.sampling_config.model = "kigi-4.5".to_string();
ctx.model_id = acp::ModelId::new("kigi-4.5");
ctx.subagent_model_overrides
.insert("explore".to_string(), "does-not-exist".to_string());
let (config, model_id) = resolve_subagent_sampling_config(
@@ -2464,8 +2464,8 @@ async fn resolve_subagent_config_override_unknown_model_falls_through_to_inherit
&ctx,
)
.await;
assert_eq!(config.model, "grok-4.5");
assert_eq!(model_id.0.as_ref(), "grok-4.5");
assert_eq!(config.model, "kigi-4.5");
assert_eq!(model_id.0.as_ref(), "kigi-4.5");
}
/// An unresolvable `AgentDefinition.model` pin (model absent from
/// `available_models`) falls through to inherit the parent model.
@@ -2473,8 +2473,8 @@ async fn resolve_subagent_config_override_unknown_model_falls_through_to_inherit
async fn resolve_subagent_agent_definition_unknown_model_falls_through_to_inherit() {
use kigi_agent::config::ModelOverride;
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.sampling_config.model = "grok-4.5".to_string();
ctx.model_id = acp::ModelId::new("grok-4.5");
ctx.sampling_config.model = "kigi-4.5".to_string();
ctx.model_id = acp::ModelId::new("kigi-4.5");
let agent_model = ModelOverride::Override("does-not-exist".to_string());
let (config, model_id) = resolve_subagent_sampling_config(
"explore",
@@ -2482,8 +2482,8 @@ async fn resolve_subagent_agent_definition_unknown_model_falls_through_to_inheri
&ctx,
)
.await;
assert_eq!(config.model, "grok-4.5");
assert_eq!(model_id.0.as_ref(), "grok-4.5");
assert_eq!(config.model, "kigi-4.5");
assert_eq!(model_id.0.as_ref(), "kigi-4.5");
}
#[test]
fn key_prefix_truncates_to_8_chars() {
@@ -829,7 +829,7 @@ mod tests {
/// Line printed to stdout once the subprocess holds the flock.
#[cfg(unix)]
const LOCK_HOLDER_READY: &str = "__GROK_LOCK_HOLDER_READY__";
const LOCK_HOLDER_READY: &str = "__KIGI_LOCK_HOLDER_READY__";
/// Subprocess entry point for the cross-process lock tests. Only does
/// anything when re-executed with `KIGI_TEST_LOCK_HOLDER` set; a normal
@@ -250,13 +250,13 @@ mod tests {
/// it from the raw JSON to preserve downstream data fidelity.
#[test]
fn test_assistant_with_reasoning() {
let v1 = r#"{"type":"assistant","content":"The answer is 42.","reasoning":{"text":"Let me think..."},"tool_calls":[],"model_id":"grok-3"}"#;
let v1 = r#"{"type":"assistant","content":"The answer is 42.","reasoning":{"text":"Let me think..."},"tool_calls":[],"model_id":"kigi-3"}"#;
let out = convert_line_for_test(v1);
let v = v0_value(&out);
assert_eq!(v["role"], "assistant");
assert_eq!(v["content"], "The answer is 42.");
assert_eq!(v["reasoning_content"], "Let me think...");
assert_eq!(v["model_id"], "grok-3");
assert_eq!(v["model_id"], "kigi-3");
}
/// Current shape: reasoning is a sibling line
@@ -274,7 +274,7 @@ mod tests {
);
assert_eq!(pending.len(), 1);
let a_line = r#"{"type":"assistant","content":"The answer is 42.","tool_calls":[],"model_id":"grok-3"}"#;
let a_line = r#"{"type":"assistant","content":"The answer is 42.","tool_calls":[],"model_id":"kigi-3"}"#;
let a = convert_line(a_line, &mut pending)
.unwrap()
.expect("assistant line produces a v0 message");
@@ -5,9 +5,9 @@
//! Usage:
//! cargo run --bin trace_classify -- \
//! --trace /path/to/trace-<id>-all-turns.json \
//! --api-base-url <url> \
//! [--output out.jsonl] \
//! [--model kimi-for-coding] \
//! [--api-base-url https://api.x.ai/v1] \
//! [--api-key <key> | $XAI_API_KEY | <kigi-home>/auth.json] \
//! [--min-confidence 0.7] \
//! [--include-reasoning true] \
@@ -47,8 +47,9 @@ struct Cli {
#[arg(long, default_value = "kimi-for-coding")]
model: String,
/// Sampler base URL.
#[arg(long, default_value = "https://api.x.ai/v1")]
/// Sampler base URL. REQUIRED: there is no default BYOK endpoint —
/// pass the base URL your API key is valid for explicitly.
#[arg(long)]
api_base_url: String,
/// API key. Overrides `$XAI_API_KEY` when set; falls back to
@@ -109,13 +110,23 @@ mod tests {
use super::*;
use clap::CommandFactory;
/// Minimal args for a valid invocation. `--api-base-url` is included
/// because it is REQUIRED — there is no default BYOK endpoint.
const BASE_ARGS: [&str; 5] = [
"trace_classify",
"--trace",
"foo.json",
"--api-base-url",
"https://byok.example/v1",
];
#[test]
fn cli_parses_minimal_args() {
let cli = Cli::try_parse_from(["trace_classify", "--trace", "foo.json", "--model", "bar"])
let cli = Cli::try_parse_from(BASE_ARGS.iter().copied().chain(["--model", "bar"]))
.expect("parse");
assert_eq!(cli.trace, PathBuf::from("foo.json"));
assert_eq!(cli.model, "bar");
assert_eq!(cli.api_base_url, "https://api.x.ai/v1");
assert_eq!(cli.api_base_url, "https://byok.example/v1");
assert!(cli.output.is_none());
assert!(cli.api_key.is_none());
assert!(cli.min_confidence.is_none());
@@ -123,45 +134,54 @@ mod tests {
assert!(cli.kigi_home.is_none());
}
/// Fail fast: the BYOK path has no default endpoint, so omitting
/// `--api-base-url` must be a parse error that names the flag.
#[test]
fn cli_requires_api_base_url() {
let err = Cli::try_parse_from(["trace_classify", "--trace", "foo.json"])
.expect_err("missing --api-base-url");
let msg = err.to_string();
assert!(
msg.contains("--api-base-url"),
"error mentions --api-base-url: {msg}"
);
}
/// Per-model knob (mirrored as a CLI override on the offline tool):
/// `--include-reasoning true` and `--include-reasoning false` both
/// parse; absent → `None` so the harness default applies.
#[test]
fn cli_include_reasoning_override_parses() {
let cli_true = Cli::try_parse_from([
"trace_classify",
"--trace",
"foo.json",
"--include-reasoning",
"true",
])
let cli_true = Cli::try_parse_from(
BASE_ARGS
.iter()
.copied()
.chain(["--include-reasoning", "true"]),
)
.expect("parse true");
assert_eq!(cli_true.include_reasoning, Some(true));
let cli_false = Cli::try_parse_from([
"trace_classify",
"--trace",
"foo.json",
"--include-reasoning",
"false",
])
let cli_false = Cli::try_parse_from(
BASE_ARGS
.iter()
.copied()
.chain(["--include-reasoning", "false"]),
)
.expect("parse false");
assert_eq!(cli_false.include_reasoning, Some(false));
let cli_absent =
Cli::try_parse_from(["trace_classify", "--trace", "foo.json"]).expect("parse absent");
let cli_absent = Cli::try_parse_from(BASE_ARGS).expect("parse absent");
assert!(cli_absent.include_reasoning.is_none());
}
#[test]
fn cli_kigi_home_override_parses() {
let cli = Cli::try_parse_from([
"trace_classify",
"--trace",
"foo.json",
"--kigi-home",
"/tmp/scratch-kigi",
])
let cli = Cli::try_parse_from(
BASE_ARGS
.iter()
.copied()
.chain(["--kigi-home", "/tmp/scratch-kigi"]),
)
.expect("parse");
assert_eq!(cli.kigi_home, Some(PathBuf::from("/tmp/scratch-kigi")));
}
@@ -186,8 +206,8 @@ mod tests {
.map(|v| v.to_string_lossy().into_owned())
.collect::<Vec<_>>()
};
assert_eq!(by_id("model"), vec!["grok-4.5"]);
assert_eq!(by_id("api_base_url"), vec!["https://api.x.ai/v1"]);
assert_eq!(by_id("model"), vec!["kimi-for-coding"]);
assert!(by_id("api_base_url").is_empty(), "no default BYOK endpoint");
assert!(by_id("min_confidence").is_empty(), "no default");
assert!(by_id("include_reasoning").is_empty(), "no default");
}
@@ -195,13 +215,12 @@ mod tests {
/// F6 — `--min-confidence 0.5` parses and lands in `RunArgs`.
#[test]
fn cli_min_confidence_override_parses() {
let cli = Cli::try_parse_from([
"trace_classify",
"--trace",
"foo.json",
"--min-confidence",
"0.42",
])
let cli = Cli::try_parse_from(
BASE_ARGS
.iter()
.copied()
.chain(["--min-confidence", "0.42"]),
)
.expect("parse");
assert_eq!(cli.min_confidence, Some(0.42));
}
@@ -214,7 +233,7 @@ mod tests {
fn cli_min_confidence_rejects_bad_values() {
for bad in ["1.5", "-0.1", "nan", "inf", "not-a-float"] {
let arg = format!("--min-confidence={bad}");
let err = Cli::try_parse_from(["trace_classify", "--trace", "foo.json", arg.as_str()])
let err = Cli::try_parse_from(BASE_ARGS.iter().copied().chain([arg.as_str()]))
.expect_err(bad);
// Parsing failed — that's all we need. Exact error text
// is clap-version-dependent.
@@ -1451,7 +1451,7 @@ mod tests {
reset_marker_cache_for_test();
// Also clear the workspace-side env-var override so it doesn't
// leak into subsequent tests.
unsafe { std::env::remove_var("_GROK_CLAUDE_MARKER_OVERRIDE") };
unsafe { std::env::remove_var("_KIGI_CLAUDE_MARKER_OVERRIDE") };
}
}
@@ -1649,7 +1649,7 @@ mod tests {
// cannot see the shell-side marker cache. Set the env-var override so
// the workspace-resident marker reader honours the gate; without it
// the function would read the developer's real ~/.claude settings.
unsafe { std::env::set_var("_GROK_CLAUDE_MARKER_OVERRIDE", "1") };
unsafe { std::env::set_var("_KIGI_CLAUDE_MARKER_OVERRIDE", "1") };
let dir = tempfile::tempdir().unwrap();
let env = kigi_workspace::permission::claude_settings::load_claude_env_with_project(
dir.path(),
@@ -2167,7 +2167,7 @@ extra_rule_dirs = ["/c/rules"]
refresh_marker_cache(true);
// Also set the env-var override so the workspace-resident marker
// reader (which can't see the shell-side cache) honours the gate.
unsafe { std::env::set_var("_GROK_CLAUDE_MARKER_OVERRIDE", "1") };
unsafe { std::env::set_var("_KIGI_CLAUDE_MARKER_OVERRIDE", "1") };
let dir = tempfile::tempdir().unwrap();
// Drop a Claude permissions file in the tempdir; with the marker set
// the gate should skip reading it.
+3 -3
View File
@@ -1,4 +1,4 @@
//! Data APIs for `grok models`. Clients own display.
//! Data APIs for `kigi models`. Clients own display.
use agent_client_protocol as acp;
use anyhow::Result;
@@ -6,11 +6,11 @@ use kigi_acp_lib::{AcpAgentTx, acp_send};
use crate::agent::config::Config as AgentConfig;
/// Status for the `grok models` banner (display order ≠ sampling priority; see [`AuthStatus::resolve`]).
/// Status for the `kigi models` banner (display order ≠ sampling priority; see [`AuthStatus::resolve`]).
#[derive(Debug, PartialEq, Eq)]
pub enum AuthStatus {
ApiKey,
/// Auth host from `grok_ws_origin` (scheme stripped).
/// Auth host from `kigi_ws_origin` (scheme stripped).
LoggedIn(String),
/// Catalog key of the first model with own `api_key`/`env_key`.
ModelCredentials(String),
+7 -7
View File
@@ -230,8 +230,8 @@ pub struct SubagentsConfig {
///
/// ```toml
/// [subagents.models]
/// explore = "grok-3-fast"
/// plan = "grok-3"
/// explore = "kigi-3-fast"
/// plan = "kigi-3"
/// ```
#[serde(default)]
pub models: std::collections::HashMap<String, String>,
@@ -252,7 +252,7 @@ pub struct SubagentsConfig {
/// [subagents.roles.researcher]
/// description = "Deep research agent"
/// default_capability_mode = "read-only"
/// model = "grok-3"
/// model = "kigi-3"
///
/// [subagents.roles.implementer]
/// description = "Implementation agent with full access"
@@ -735,7 +735,7 @@ fn walk_toml(
}
}
/// The `[skills]` table from an effective config, shared by the reload
/// dispatch and `grok inspect`.
/// dispatch and `kigi inspect`.
pub(crate) use crate::config::reloader::parse_skills_config;
/// Effective config: layers + campaign overlay (remote cache + `KIGI_CAMPAIGNS_OVERRIDE`).
pub use crate::util::config::load_effective_config;
@@ -946,9 +946,9 @@ fn apply_requirements_inner(
enforce_str!("cli", "channel", config.cli.channel);
enforce_str!("cli", "minimum_version", config.cli.minimum_version);
if let Some(val) = req_str(req, "endpoints", "api_base_url")
&& config.endpoints.api_base_url != val
&& config.endpoints.api_base_url.as_deref() != Some(val)
{
config.endpoints.api_base_url = val.to_owned();
config.endpoints.api_base_url = Some(val.to_owned());
push("endpoints.api_base_url", val.to_owned());
}
if let Some(val) = req_str(req, "endpoints", "coding_api_base_url")
@@ -1128,7 +1128,7 @@ pub use kigi_workspace::project_config::find_project_configs;
/// ([`find_project_configs`], extending `paths` and `disabled`) plus the
/// imported `enabledPlugins` merge.
///
/// Shared by `reload_plugins_impl`, `x.ai/commands/list`, and the agent's
/// Shared by `reload_plugins_impl`, `kigi/commands/list`, and the agent's
/// eager plugin-registry fan-out so all three discover the same plugins for a
/// given cwd. Centralizing it prevents the paths/disabled/discovered-command
/// drift those callers would otherwise accumulate.
@@ -44,7 +44,7 @@ pub enum ConfigUpdate {
/// Strictly additive to [`Self::McpServersChanged`] — the unit
/// variant continues to fire for global-config edits. The two
/// cases are split so per-project reloads don't
/// grok process sharing the home dir). The agent should consult the cache
/// kigi process sharing the home dir). The agent should consult the cache
/// thrash unrelated sessions.
ProjectMcpServersChanged {
/// The project root whose `.kigi/`, `.mcp.json`, or
@@ -70,7 +70,7 @@ pub enum ConfigUpdate {
/// drop redundant `ProjectMcpServersChanged` dispatches on
/// the reloader doesn't have.
ModelsCacheChanged,
/// Updated UI settings — agent broadcasts `x.ai/config_changed` to IPC clients.
/// Updated UI settings — agent broadcasts `kigi/config_changed` to IPC clients.
Ui {
theme: Option<String>,
yolo: bool,
@@ -494,7 +494,7 @@ pub(crate) fn hash_auth_key(key: &str) -> u64 {
/// Extract the `[skills]` table from an effective config.
///
/// Consumers: the reload dispatch above (change detection →
/// `ConfigUpdate::Skills`) and `grok inspect` (via the `crate::config`
/// `ConfigUpdate::Skills`) and `kigi inspect` (via the `crate::config`
/// re-export), so both honor the same paths/ignore/disabled as a live
/// session. Session spawn parses the same table separately through the typed
/// `Config.skills` (agent/config.rs) — keep these in sync rather than adding
@@ -906,14 +906,14 @@ ignore = ["/tmp"]
[ui]
theme = "dark"
yolo = true
fork_secondary_model = "grok-4.5"
fork_secondary_model = "kigi-4.5"
"#,
)
.unwrap();
let (theme, yolo, fork) = extract_ui_fields(&config);
assert_eq!(theme.as_deref(), Some("dark"));
assert!(yolo);
assert_eq!(fork.as_deref(), Some("grok-4.5"));
assert_eq!(fork.as_deref(), Some("kigi-4.5"));
}
#[test]
@@ -936,7 +936,7 @@ fork_secondary_model = "grok-4.5"
let b: toml::Value = toml::from_str(
r#"
[model.my-custom]
model = "grok-4.5"
model = "kigi-4.5"
base_url = "https://api.example.com/v1"
"#,
)
@@ -946,8 +946,8 @@ base_url = "https://api.example.com/v1"
#[test]
fn models_changed_detects_default_change() {
let a: toml::Value = toml::from_str("[models]\ndefault = \"grok-code-fast-1\"").unwrap();
let b: toml::Value = toml::from_str("[models]\ndefault = \"grok-code-slow-1\"").unwrap();
let a: toml::Value = toml::from_str("[models]\ndefault = \"kigi-code-fast-1\"").unwrap();
let b: toml::Value = toml::from_str("[models]\ndefault = \"kigi-code-slow-1\"").unwrap();
assert_ne!(a.get("models"), b.get("models"));
}
+102 -99
View File
@@ -111,18 +111,18 @@ fn with_env_var_opt<T>(name: &str, value: Option<&str>, f: impl FnOnce() -> T) -
result.unwrap_or_else(|p| std::panic::resume_unwind(p))
}
/// Run `f` with KIGI_MEMORY explicitly unset.
fn without_grok_memory<T>(f: impl FnOnce() -> T) -> T {
fn without_kigi_memory<T>(f: impl FnOnce() -> T) -> T {
let _guard = MEMORY_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_MEMORY", None, f)
}
/// Run `f` with KIGI_MEMORY set to a specific value.
fn with_grok_memory<T>(value: &str, f: impl FnOnce() -> T) -> T {
fn with_kigi_memory<T>(value: &str, f: impl FnOnce() -> T) -> T {
let _guard = MEMORY_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_MEMORY", Some(value), f)
}
#[test]
fn memory_config_default_disabled() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(false, false, &config, None);
assert!(! mem.enabled);
@@ -130,7 +130,7 @@ fn memory_config_default_disabled() {
}
#[test]
fn memory_config_cli_flag_enables() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(true, false, &config, None);
assert!(mem.enabled);
@@ -138,7 +138,7 @@ fn memory_config_cli_flag_enables() {
}
#[test]
fn memory_config_from_toml() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = true").unwrap();
let mem = MemoryConfig::resolve(false, false, &config, None);
assert!(mem.enabled);
@@ -146,7 +146,7 @@ fn memory_config_from_toml() {
}
#[test]
fn memory_config_toml_disabled() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = false").unwrap();
let mem = MemoryConfig::resolve(false, false, &config, None);
assert!(! mem.enabled);
@@ -154,7 +154,7 @@ fn memory_config_toml_disabled() {
}
#[test]
fn memory_config_env_var_enables() {
with_grok_memory(
with_kigi_memory(
"1",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -165,7 +165,7 @@ fn memory_config_env_var_enables() {
}
#[test]
fn memory_config_env_var_true_enables() {
with_grok_memory(
with_kigi_memory(
"true",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -176,7 +176,7 @@ fn memory_config_env_var_true_enables() {
}
#[test]
fn memory_config_env_var_zero_does_not_enable() {
with_grok_memory(
with_kigi_memory(
"0",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -187,7 +187,7 @@ fn memory_config_env_var_zero_does_not_enable() {
}
#[test]
fn memory_config_env_var_false_does_not_enable() {
with_grok_memory(
with_kigi_memory(
"false",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -198,7 +198,7 @@ fn memory_config_env_var_false_does_not_enable() {
}
#[test]
fn memory_config_cli_overrides_toml_disabled() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = false").unwrap();
let mem = MemoryConfig::resolve(true, false, &config, None);
assert!(mem.enabled, "CLI flag should override config file");
@@ -206,7 +206,7 @@ fn memory_config_cli_overrides_toml_disabled() {
}
#[test]
fn memory_config_env_zero_force_disables_toml_enabled() {
with_grok_memory(
with_kigi_memory(
"0",
|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = true")
@@ -221,7 +221,7 @@ fn memory_config_env_zero_force_disables_toml_enabled() {
}
#[test]
fn memory_config_env_false_force_disables_toml_enabled() {
with_grok_memory(
with_kigi_memory(
"false",
|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = true")
@@ -236,7 +236,7 @@ fn memory_config_env_false_force_disables_toml_enabled() {
}
#[test]
fn memory_config_cli_flag_overrides_env_disable() {
with_grok_memory(
with_kigi_memory(
"0",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -249,7 +249,7 @@ fn memory_config_cli_flag_overrides_env_disable() {
}
#[test]
fn memory_config_no_memory_overrides_all() {
with_grok_memory(
with_kigi_memory(
"1",
|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = true")
@@ -264,7 +264,7 @@ fn memory_config_no_memory_overrides_all() {
}
#[test]
fn memory_config_no_memory_alone_disables() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(false, true, &config, None);
assert!(! mem.enabled, "--no-memory alone should disable");
@@ -272,7 +272,7 @@ fn memory_config_no_memory_alone_disables() {
}
#[test]
fn memory_config_no_memory_overrides_env_enable() {
with_grok_memory(
with_kigi_memory(
"1",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -283,7 +283,7 @@ fn memory_config_no_memory_overrides_env_enable() {
}
#[test]
fn memory_config_no_memory_overrides_toml_enabled() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = true").unwrap();
let mem = MemoryConfig::resolve(false, true, &config, None);
assert!(! mem.enabled, "--no-memory should override TOML enabled=true");
@@ -291,7 +291,7 @@ fn memory_config_no_memory_overrides_toml_enabled() {
}
#[test]
fn memory_config_no_memory_overrides_remote_enabled() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_enabled: Some(true),
@@ -303,7 +303,7 @@ fn memory_config_no_memory_overrides_remote_enabled() {
}
#[test]
fn memory_config_defaults_are_correct() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(false, false, &config, None);
assert_eq!(mem.index.max_chunk_chars, 1600);
@@ -352,7 +352,7 @@ fn memory_config_defaults_are_correct() {
/// (unknown fields are silently ignored by serde default).
#[test]
fn memory_config_watcher_debounce_ms_in_toml_is_silently_ignored() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = "[memory.watcher]\nenabled = true\ndebounce_ms = 2000\n";
let config: toml::Value = toml::from_str(toml_str).unwrap();
let mem = MemoryConfig::resolve(false, false, &config, None);
@@ -362,7 +362,7 @@ fn memory_config_watcher_debounce_ms_in_toml_is_silently_ignored() {
}
#[test]
fn memory_config_full_toml_parsing() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory]
enabled = true
@@ -402,7 +402,7 @@ save_on_end = false
[compaction.memory_flush]
enabled = false
soft_threshold_tokens = 8000
flush_model = "grok-4"
flush_model = "kigi-4"
max_flush_write_chars = 16000
idle_timeout_secs = 300
semantic_dedup_threshold = 0.85
@@ -433,7 +433,7 @@ hard_clear_age_turns = 20
assert!(! mem.session.save_on_end);
assert!(! mem.flush.enabled);
assert_eq!(mem.flush.soft_threshold_tokens, 8000);
assert_eq!(mem.flush.flush_model.as_deref(), Some("grok-4"));
assert_eq!(mem.flush.flush_model.as_deref(), Some("kigi-4"));
assert_eq!(mem.flush.max_flush_write_chars, 16000);
assert_eq!(mem.flush.idle_timeout_secs, Some(300));
assert_eq!(mem.flush.semantic_dedup_threshold, Some(0.85));
@@ -444,7 +444,7 @@ hard_clear_age_turns = 20
}
#[test]
fn memory_config_partial_toml_uses_defaults_for_missing() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory]
enabled = true
@@ -465,7 +465,7 @@ max_chunk_chars = 3200
}
#[test]
fn memory_config_remote_settings_enable() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_enabled: Some(true),
@@ -477,7 +477,7 @@ fn memory_config_remote_settings_enable() {
}
#[test]
fn memory_config_remote_settings_pruning() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
pruning_enabled: Some(true),
@@ -491,7 +491,7 @@ fn memory_config_remote_settings_pruning() {
}
#[test]
fn memory_config_remote_settings_initial_injection() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_initial_injection_enabled: Some(false),
@@ -505,7 +505,7 @@ fn memory_config_remote_settings_initial_injection() {
}
#[test]
fn memory_config_local_initial_injection_overrides_remote() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory.initial_injection]
enabled = true
@@ -524,7 +524,7 @@ min_score = 0.25
}
#[test]
fn memory_config_local_disabled_blocks_remote_enable() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config: toml::Value = toml::from_str("[memory]\nenabled = false").unwrap();
let remote = crate::util::config::RemoteSettings {
memory_enabled: Some(true),
@@ -538,7 +538,7 @@ fn memory_config_local_disabled_blocks_remote_enable() {
}
#[test]
fn memory_config_local_overrides_remote() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory.search]
max_results = 20
@@ -554,7 +554,7 @@ max_results = 20
}
#[test]
fn memory_config_remote_none_is_noop() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem_without = MemoryConfig::resolve(false, false, &config, None);
let mem_with_empty = MemoryConfig::resolve(
@@ -569,7 +569,7 @@ fn memory_config_remote_none_is_noop() {
}
#[test]
fn flush_semantic_dedup_threshold_from_remote_when_no_local_flush() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
flush_semantic_dedup_threshold: Some(0.85),
@@ -584,7 +584,7 @@ fn flush_semantic_dedup_threshold_from_remote_when_no_local_flush() {
}
#[test]
fn flush_semantic_dedup_threshold_clamped_from_remote() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
flush_semantic_dedup_threshold: Some(1.5),
@@ -608,7 +608,7 @@ fn flush_semantic_dedup_threshold_clamped_from_remote() {
}
#[test]
fn flush_semantic_dedup_threshold_local_blocks_remote() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[compaction.memory_flush]
enabled = true
@@ -628,7 +628,7 @@ semantic_dedup_threshold = 0.88
}
#[test]
fn flush_semantic_dedup_threshold_defaults_to_none() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(false, false, &config, None);
assert_eq!(
@@ -639,7 +639,7 @@ fn flush_semantic_dedup_threshold_defaults_to_none() {
}
#[test]
fn memory_dream_config_defaults() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let mem = MemoryConfig::resolve(false, false, &config, None);
assert!(mem.dream.enabled);
@@ -651,7 +651,7 @@ fn memory_dream_config_defaults() {
}
#[test]
fn memory_dream_config_toml_parsing() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory.dream]
enabled = true
@@ -671,7 +671,7 @@ check_interval_secs = 600
}
#[test]
fn memory_dream_config_remote_override_when_toml_absent() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
dream_enabled: Some(true),
@@ -690,7 +690,7 @@ fn memory_dream_config_remote_override_when_toml_absent() {
}
#[test]
fn memory_dream_config_remote_ignored_when_toml_present() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory.dream]
enabled = false
@@ -853,7 +853,7 @@ fn effective_half_life_disabled_legacy_recency_out_of_range_ignored() {
}
#[test]
fn mmr_lambda_clamped_above_one() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory]
enabled = true
@@ -873,7 +873,7 @@ lambda = 2.0
}
#[test]
fn mmr_lambda_clamped_below_zero() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory]
enabled = true
@@ -893,7 +893,7 @@ lambda = -0.5
}
#[test]
fn memory_config_remote_temporal_decay() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_temporal_decay_enabled: Some(false),
@@ -907,7 +907,7 @@ fn memory_config_remote_temporal_decay() {
}
#[test]
fn memory_config_remote_mmr() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_mmr_enabled: Some(true),
@@ -921,7 +921,7 @@ fn memory_config_remote_mmr() {
}
#[test]
fn memory_config_remote_mmr_lambda_clamped() {
without_grok_memory(|| {
without_kigi_memory(|| {
let config = toml::Value::Table(toml::map::Map::new());
let remote = crate::util::config::RemoteSettings {
memory_mmr_lambda: Some(5.0),
@@ -936,7 +936,7 @@ fn memory_config_remote_mmr_lambda_clamped() {
}
#[test]
fn memory_config_local_search_blocks_remote_temporal_decay_and_mmr() {
without_grok_memory(|| {
without_kigi_memory(|| {
let toml_str = r#"
[memory.search]
max_results = 8
@@ -962,18 +962,18 @@ max_results = 8
/// Mutex to serialize tests that touch the KIGI_SUBAGENTS env var.
static SUBAGENTS_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// Run `f` with KIGI_SUBAGENTS explicitly unset.
fn without_grok_subagents<T>(f: impl FnOnce() -> T) -> T {
fn without_kigi_subagents<T>(f: impl FnOnce() -> T) -> T {
let _guard = SUBAGENTS_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_SUBAGENTS", None, f)
}
/// Run `f` with KIGI_SUBAGENTS set to a specific value.
fn with_grok_subagents<T>(value: &str, f: impl FnOnce() -> T) -> T {
fn with_kigi_subagents<T>(value: &str, f: impl FnOnce() -> T) -> T {
let _guard = SUBAGENTS_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_SUBAGENTS", Some(value), f)
}
#[test]
fn subagents_config_default_enabled() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config = toml::Value::Table(toml::map::Map::new());
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled);
@@ -981,7 +981,7 @@ fn subagents_config_default_enabled() {
}
#[test]
fn subagents_config_cli_flag_enables() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config = toml::Value::Table(toml::map::Map::new());
let sa = SubagentsConfig::resolve(true, &config, None);
assert!(sa.enabled);
@@ -989,7 +989,7 @@ fn subagents_config_cli_flag_enables() {
}
#[test]
fn subagents_config_env_var_enables() {
with_grok_subagents(
with_kigi_subagents(
"1",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -1000,7 +1000,7 @@ fn subagents_config_env_var_enables() {
}
#[test]
fn subagents_config_env_var_disables() {
with_grok_subagents(
with_kigi_subagents(
"0",
|| {
let config: toml::Value = toml::from_str("[subagents]\nenabled = true")
@@ -1012,7 +1012,7 @@ fn subagents_config_env_var_disables() {
}
#[test]
fn subagents_config_toml_enables() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str("[subagents]\nenabled = true").unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled);
@@ -1020,7 +1020,7 @@ fn subagents_config_toml_enables() {
}
#[test]
fn subagents_config_local_disabled_wins() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str("[subagents]\nenabled = false")
.unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
@@ -1029,7 +1029,7 @@ fn subagents_config_local_disabled_wins() {
}
#[test]
fn subagents_config_env_var_disables_default() {
with_grok_subagents(
with_kigi_subagents(
"0",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -1044,7 +1044,7 @@ fn subagents_config_env_var_disables_default() {
/// as an unknown key and have no effect on resolution.
#[test]
fn subagents_config_remote_settings_key_is_ignored() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let _settings: crate::util::config::RemoteSettings = serde_json::from_str(
r#"{"subagents_enabled": false}"#,
)
@@ -1056,7 +1056,7 @@ fn subagents_config_remote_settings_key_is_ignored() {
}
#[test]
fn subagents_config_cli_flag_overrides_env_var() {
with_grok_subagents(
with_kigi_subagents(
"0",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -1067,28 +1067,28 @@ fn subagents_config_cli_flag_overrides_env_var() {
}
#[test]
fn subagents_config_models_parsed() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str(
r#"
[subagents]
enabled = true
[subagents.models]
explore = "grok-3-fast"
plan = "grok-4.5"
explore = "kigi-3-fast"
plan = "kigi-4.5"
"#,
)
.unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled);
assert_eq!(sa.models.len(), 2);
assert_eq!(sa.models.get("explore").unwrap(), "grok-3-fast");
assert_eq!(sa.models.get("plan").unwrap(), "grok-4.5");
assert_eq!(sa.models.get("explore").unwrap(), "kigi-3-fast");
assert_eq!(sa.models.get("plan").unwrap(), "kigi-4.5");
});
}
#[test]
fn subagents_config_models_empty_when_missing() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str("[subagents]\nenabled = true").unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled);
@@ -1097,11 +1097,11 @@ fn subagents_config_models_empty_when_missing() {
}
#[test]
fn subagents_config_models_without_enabled() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str(
r#"
[subagents.models]
explore = "grok-3-fast"
explore = "kigi-3-fast"
"#,
)
.unwrap();
@@ -1110,30 +1110,30 @@ fn subagents_config_models_without_enabled() {
! sa.enabled, "explicit [subagents] section without enabled should be false"
);
assert_eq!(sa.models.len(), 1);
assert_eq!(sa.models.get("explore").unwrap(), "grok-3-fast");
assert_eq!(sa.models.get("explore").unwrap(), "kigi-3-fast");
});
}
#[test]
fn subagents_config_models_with_env_var_enables() {
with_grok_subagents(
with_kigi_subagents(
"1",
|| {
let config: toml::Value = toml::from_str(
r#"
[subagents.models]
explore = "grok-3-fast"
explore = "kigi-3-fast"
"#,
)
.unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled, "KIGI_SUBAGENTS=1 should enable");
assert_eq!(sa.models.get("explore").unwrap(), "grok-3-fast");
assert_eq!(sa.models.get("explore").unwrap(), "kigi-3-fast");
},
);
}
#[test]
fn subagents_config_toggle_mixed_values() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str(
r#"
[subagents]
@@ -1158,7 +1158,7 @@ fn subagents_config_toggle_mixed_values() {
}
#[test]
fn subagents_config_toggle_missing_defaults_to_empty() {
without_grok_subagents(|| {
without_kigi_subagents(|| {
let config: toml::Value = toml::from_str("[subagents]\nenabled = true").unwrap();
let sa = SubagentsConfig::resolve(false, &config, None);
assert!(sa.enabled);
@@ -1267,7 +1267,7 @@ fn model_overrides_local_image_description_wins_over_remote() {
);
}
#[test]
fn model_overrides_default_image_description_is_grok_build() {
fn model_overrides_default_image_description_is_kigi() {
with_model_overrides_env(
None,
None,
@@ -1282,7 +1282,7 @@ fn model_overrides_default_image_description_is_grok_build() {
);
}
#[test]
fn model_overrides_default_session_summary_is_grok_build() {
fn model_overrides_default_session_summary_is_kigi() {
with_model_overrides_env(
None,
None,
@@ -1632,17 +1632,17 @@ fn model_overrides_prompt_suggestion_blank_values_are_unset() {
/// Lock shared by every test that touches the env var read by
/// `ToolsConfig::resolve`, so tests can't race.
static TOOLS_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn without_grok_respect_gitignore<T>(f: impl FnOnce() -> T) -> T {
fn without_kigi_respect_gitignore<T>(f: impl FnOnce() -> T) -> T {
let _guard = TOOLS_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_RESPECT_GITIGNORE", None, f)
}
fn with_grok_respect_gitignore<T>(value: &str, f: impl FnOnce() -> T) -> T {
fn with_kigi_respect_gitignore<T>(value: &str, f: impl FnOnce() -> T) -> T {
let _guard = TOOLS_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
with_env_var_opt("KIGI_RESPECT_GITIGNORE", Some(value), f)
}
#[test]
fn tools_config_default_disabled() {
without_grok_respect_gitignore(|| {
without_kigi_respect_gitignore(|| {
let config = toml::Value::Table(toml::map::Map::new());
let tc = ToolsConfig::resolve(&config);
assert!(! tc.respect_gitignore);
@@ -1650,7 +1650,7 @@ fn tools_config_default_disabled() {
}
#[test]
fn tools_config_toml_disables() {
without_grok_respect_gitignore(|| {
without_kigi_respect_gitignore(|| {
let config: toml::Value = toml::from_str("[tools]\nrespect_gitignore = false")
.unwrap();
let tc = ToolsConfig::resolve(&config);
@@ -1659,7 +1659,7 @@ fn tools_config_toml_disables() {
}
#[test]
fn tools_config_env_var_disables() {
with_grok_respect_gitignore(
with_kigi_respect_gitignore(
"0",
|| {
let config = toml::Value::Table(toml::map::Map::new());
@@ -1670,7 +1670,7 @@ fn tools_config_env_var_disables() {
}
#[test]
fn tools_config_env_var_overrides_toml() {
with_grok_respect_gitignore(
with_kigi_respect_gitignore(
"1",
|| {
let config: toml::Value = toml::from_str(
@@ -1684,7 +1684,7 @@ fn tools_config_env_var_overrides_toml() {
}
#[test]
fn tools_config_env_false_overrides_toml_true() {
with_grok_respect_gitignore(
with_kigi_respect_gitignore(
"false",
|| {
let config: toml::Value = toml::from_str("[tools]\nrespect_gitignore = true")
@@ -1703,7 +1703,7 @@ fn roles_parse_from_toml() {
[roles.researcher]
description = "Deep research agent"
default_capability_mode = "read-only"
model = "grok-3"
model = "kigi-3"
[roles.implementer]
description = "Implementation agent"
@@ -1715,7 +1715,7 @@ fn roles_parse_from_toml() {
let researcher = cfg.get_role("researcher").unwrap();
assert_eq!(researcher.description, "Deep research agent");
assert_eq!(researcher.default_capability_mode.as_deref(), Some("read-only"));
assert_eq!(researcher.model.as_deref(), Some("grok-3"));
assert_eq!(researcher.model.as_deref(), Some("kigi-3"));
assert!(researcher.prompt_file.is_none());
let implementer = cfg.get_role("implementer").unwrap();
assert_eq!(implementer.description, "Implementation agent");
@@ -1777,7 +1777,7 @@ fn validate_roles_passes_valid_config() {
[roles.good]
description = "Valid role"
default_capability_mode = "read-write"
model = "grok-3"
model = "kigi-3"
"#;
let cfg: SubagentsConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.validate_roles().is_empty());
@@ -2068,7 +2068,7 @@ fn roles_coexist_with_models_and_toggle() {
let toml_str = r#"
enabled = true
[models]
explore = "grok-fast"
explore = "kigi-fast"
[toggle]
plan = false
[roles.researcher]
@@ -2077,7 +2077,7 @@ fn roles_coexist_with_models_and_toggle() {
"#;
let cfg: SubagentsConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.enabled);
assert_eq!(cfg.models.get("explore").map(| s | s.as_str()), Some("grok-fast"));
assert_eq!(cfg.models.get("explore").map(| s | s.as_str()), Some("kigi-fast"));
assert!(! cfg.is_subagent_enabled("plan"));
assert!(cfg.get_role("researcher").is_some());
}
@@ -2255,10 +2255,10 @@ coding_api_base_url = "https://cli-chat-proxy.kigi.com/v1"
[model.kigi-build]
base_url = "https://inference.acme-corp.example/xai/v1"
env_key = "ANTHROPIC_AUTH_TOKEN"
model = "grok-4.5"
model = "kigi-4.5"
[models]
default = "grok-4.5"
default = "kigi-4.5"
"#,
)
.unwrap();
@@ -2376,7 +2376,10 @@ fn apply_requirements_value_overrides_user_settings() {
Some("https://managed-proxy.example/v1"), cfg.endpoints.coding_api_base_url
.as_deref()
);
assert_eq!("https://managed-api.example/v1", cfg.endpoints.api_base_url);
assert_eq!(
Some("https://managed-api.example/v1"),
cfg.endpoints.api_base_url.as_deref()
);
assert_eq!(
Some("https://managed-models.example/v1"), cfg.endpoints.models_base_url
.as_deref()
@@ -2496,7 +2499,7 @@ fn validate_hooks_path_rejects_traversal_attack() {
);
}
#[test]
fn validate_hooks_path_accepts_grok_hooks_subdir() {
fn validate_hooks_path_accepts_kigi_hooks_subdir() {
let kigi_home = crate::util::kigi_home::kigi_home();
let valid_path = kigi_home.join("hooks").join("my-hooks");
let _ = std::fs::create_dir_all(&valid_path);
@@ -2532,7 +2535,7 @@ fn managed_settings_disables_features_and_requirements_overrides() {
assert!(cfg.ui.yolo);
}
/// REGRESSION: external managed-settings.json is advisory, not authoritative.
/// disableBypassPermissionsMode (-> features.disable_yolo) must NOT clamp the user's own grok yolo.
/// disableBypassPermissionsMode (-> features.disable_yolo) must NOT clamp the user's own kigi yolo.
#[test]
fn managed_settings_does_not_override_user_yolo() {
use kigi_workspace::permission::resolution::ManagedSettingsFeatures;
@@ -2582,10 +2585,10 @@ fn resolve_effective_plugins_config_gates_project_paths_on_folder_trust() {
let _sim = simulate_release_build();
let repo = tempfile::tempdir().unwrap();
git2::Repository::init(repo.path()).unwrap();
let grok = repo.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
let kigi = repo.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(
grok.join("config.toml"),
kigi.join("config.toml"),
"[plugins]\npaths = [\"./proj-plugin\"]\ndisabled = [\"proj-bad\"]\n",
)
.unwrap();
@@ -2648,10 +2651,10 @@ fn discover_plugins_excludes_untrusted_configpath_plugin_end_to_end() {
std::fs::create_dir_all(&plugin_dir).unwrap();
std::fs::write(plugin_dir.join("plugin.json"), r#"{"name":"cfgpath-probe"}"#)
.unwrap();
let grok = cwd.join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
let kigi = cwd.join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(
grok.join("config.toml"),
kigi.join("config.toml"),
format!("[plugins]\npaths = ['{}']\n", plugin_dir.display()),
)
.unwrap();
@@ -2711,9 +2714,9 @@ fn kill_switched_cold_cwd_stays_allowed_through_plugins_config_read() {
let _sim = simulate_release_build();
let repo = tempfile::tempdir().unwrap();
git2::Repository::init(repo.path()).unwrap();
let grok = repo.path().join(".kigi");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(grok.join("config.toml"), "[plugins]\npaths = [\"./proj-plugin\"]\n")
let kigi = repo.path().join(".kigi");
std::fs::create_dir_all(&kigi).unwrap();
std::fs::write(kigi.join("config.toml"), "[plugins]\npaths = [\"./proj-plugin\"]\n")
.unwrap();
let cwd = repo.path();
let remote = crate::util::config::RemoteSettings {
+15 -15
View File
@@ -71,7 +71,7 @@ pub enum ConfigChangeEvent {
AuthChanged,
GlobalConfigChanged,
/// `~/.kigi/models_cache.json` changed — the on-disk `/v1/models`
/// catalog cache was rewritten, possibly by **another** grok process
/// catalog cache was rewritten, possibly by **another** kigi process
/// sharing the same `~/.kigi` (the writer may also be this process;
/// the [`ModelsManager`](crate::agent::models::ModelsManager) dedupes
/// by content before applying).
@@ -229,7 +229,7 @@ impl ConfigFileWatcher {
tracing::warn!(
path = %kigi_home.display(),
error = %e,
"failed to watch grok home directory"
"failed to watch kigi home directory"
)
})
.ok()?;
@@ -357,10 +357,10 @@ fn watch_cwd_dirs(debouncer: &mut Debouncer<AccessFilteredWatcher>, cwd: &Path)
if let Err(e) = debouncer.watcher().watch(cwd, RecursiveMode::NonRecursive) {
log_watch_error(&e, "failed to watch project cwd (non-recursive)");
}
let grok_dir = cwd.join(".kigi");
let kigi_dir = cwd.join(".kigi");
if let Err(e) = debouncer
.watcher()
.watch(&grok_dir, RecursiveMode::NonRecursive)
.watch(&kigi_dir, RecursiveMode::NonRecursive)
{
log_watch_error(
&e,
@@ -376,8 +376,8 @@ fn unwatch_cwd_dirs(debouncer: &mut Debouncer<AccessFilteredWatcher>, cwd: &Path
if let Err(e) = debouncer.watcher().unwatch(cwd) {
tracing::debug!(error = %e, "failed to unwatch project cwd");
}
let grok_dir = cwd.join(".kigi");
if let Err(e) = debouncer.watcher().unwatch(&grok_dir) {
let kigi_dir = cwd.join(".kigi");
if let Err(e) = debouncer.watcher().unwatch(&kigi_dir) {
tracing::debug!(error = %e, "failed to unwatch project .kigi directory");
}
}
@@ -752,7 +752,7 @@ mod tests {
/// A write to `<kigi_home>/models_cache.json` must surface as
/// `ConfigChangeEvent::ModelsCacheChanged` so a long-running leader can
/// hot-load a catalog fetched by another grok process.
/// hot-load a catalog fetched by another kigi process.
#[test]
#[cfg_attr(
target_os = "macos",
@@ -845,11 +845,11 @@ mod tests {
fn project_cwd_toml_triggers_reload() {
let kigi_home = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
let project_grok = cwd.path().join(".kigi");
fs::create_dir_all(&project_grok).unwrap();
let project_kigi = cwd.path().join(".kigi");
fs::create_dir_all(&project_kigi).unwrap();
// Seed the file before the watcher starts so we observe the
// modification rather than the creation event.
fs::write(project_grok.join("config.toml"), "").unwrap();
fs::write(project_kigi.join("config.toml"), "").unwrap();
let (_w, mut rx) = ConfigFileWatcher::start(
kigi_home.path(),
@@ -860,7 +860,7 @@ mod tests {
.expect("watcher should start");
fs::write(
project_grok.join("config.toml"),
project_kigi.join("config.toml"),
"[mcp_servers.x]\ncommand = \"/bin/true\"",
)
.unwrap();
@@ -983,9 +983,9 @@ mod tests {
fn watch_path_dynamic_registration() {
let kigi_home = TempDir::new().unwrap();
let new_cwd = TempDir::new().unwrap();
let project_grok = new_cwd.path().join(".kigi");
fs::create_dir_all(&project_grok).unwrap();
fs::write(project_grok.join("config.toml"), "").unwrap();
let project_kigi = new_cwd.path().join(".kigi");
fs::create_dir_all(&project_kigi).unwrap();
fs::write(project_kigi.join("config.toml"), "").unwrap();
let (mut watcher, mut rx) = ConfigFileWatcher::start(
kigi_home.path(),
@@ -998,7 +998,7 @@ mod tests {
watcher.watch_path(new_cwd.path());
fs::write(
project_grok.join("config.toml"),
project_kigi.join("config.toml"),
"[mcp_servers.y]\ncommand = \"/bin/true\"",
)
.unwrap();
@@ -1,4 +1,4 @@
//! `x.ai/auth/*` and legacy `x.ai/{get,set}ApiKey` extension handlers.
//! `kigi/auth/*` and legacy `kigi/{get,set}ApiKey` extension handlers.
//!
//! These methods let the client read/write the API key via the agent and
//! drive the OAuth login flow. The agent is the single source of truth for
@@ -14,13 +14,13 @@ use crate::session::ExtMethodResult;
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/auth/getBearerToken" => handle_get_bearer_token(agent).await,
"x.ai/getApiKey" => handle_get_api_key(),
"x.ai/setApiKey" => handle_set_api_key(args),
"x.ai/auth/submit_code" => handle_submit_code(agent, args),
"x.ai/auth/get_url" => handle_get_url(agent).await,
"x.ai/auth/logout" => handle_logout(agent, args).await,
"x.ai/auth/info" => handle_info(agent),
"kigi/auth/getBearerToken" => handle_get_bearer_token(agent).await,
"kigi/getApiKey" => handle_get_api_key(),
"kigi/setApiKey" => handle_set_api_key(args),
"kigi/auth/submit_code" => handle_submit_code(agent, args),
"kigi/auth/get_url" => handle_get_url(agent).await,
"kigi/auth/logout" => handle_logout(agent, args).await,
"kigi/auth/info" => handle_info(agent),
_ => Err(acp::Error::method_not_found()),
}
}
@@ -1,4 +1,4 @@
//! `x.ai/billing` extension handler — Kimi Code usage/quota.
//! `kigi/billing` extension handler — Kimi Code usage/quota.
//!
//! Port of kimi-cli's `/usage` command (kimi-cli `src/kimi_cli/ui/shell/usage.py`):
//! `GET {coding_api_base_url}/usages` with the OAuth Bearer token, parsed into
@@ -17,7 +17,7 @@ use crate::agent::MvpAgent;
/// human-readable reset hint (e.g. "resets in 2h 5m").
///
/// `Deserialize` because the TUI parses this back out of the
/// `x.ai/billing` ext response.
/// `kigi/billing` ext response.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageRow {
@@ -28,7 +28,7 @@ pub struct UsageRow {
pub reset_hint: Option<String>,
}
/// Response for `x.ai/billing`: the parsed usage rows, in display order
/// Response for `kigi/billing`: the parsed usage rows, in display order
/// (summary row first when the payload carries one).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -55,7 +55,7 @@ pub enum UsageError {
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/billing" => {
"kigi/billing" => {
tracing::info!("handling usage request");
handle_get_usage(agent).await
}
@@ -1,4 +1,4 @@
//! `x.ai/session/load_history`: fetch one older page of a gateway-backed
//! `kigi/session/load_history`: fetch one older page of a gateway-backed
//! conversation by client-owned cursor (`beforeId` → `nextBeforeId`).
use super::ExtResult;
use crate::agent::MvpAgent;
@@ -7,11 +7,11 @@
//!
//! | Method | Description |
//! |--------|-------------|
//! | `x.ai/code/goto-definition` | Definition location(s) for symbol at position |
//! | `x.ai/code/goto-references` | Reference location(s) for symbol at position |
//! | `x.ai/code/find-definitions` | All definitions of a symbol by name |
//! | `x.ai/code/find-references` | All references to a symbol by name |
//! | `x.ai/code/status` | Indexing status |
//! | `kigi/code/goto-definition` | Definition location(s) for symbol at position |
//! | `kigi/code/goto-references` | Reference location(s) for symbol at position |
//! | `kigi/code/find-definitions` | All definitions of a symbol by name |
//! | `kigi/code/find-references` | All references to a symbol by name |
//! | `kigi/code/status` | Indexing status |
use std::path::{Path, PathBuf};
@@ -129,7 +129,7 @@ pub struct SymbolLocation {
pub matched_symbol: Option<String>,
}
/// Reason string for the `x.ai/code/status` response.
/// Reason string for the `kigi/code/status` response.
///
/// Serialised as a camelCase string so clients can pattern-match on it.
#[derive(Debug, Serialize)]
@@ -142,7 +142,7 @@ pub enum IndexStatusReason {
NotStarted,
/// Client type is not web (web-only for initial rollout).
ClientNotWeb,
/// Client did not advertise `x.ai/codeNavigation.enabled`.
/// Client did not advertise `kigi/codeNavigation.enabled`.
CapabilityNotAdvertised,
/// `codebase_indexing` feature is disabled in config.
DisabledByConfig,
@@ -181,7 +181,7 @@ pub async fn handle(
use kigi_workspace::workspace_ops::*;
match args.method.as_ref() {
"x.ai/code/goto-definition" => {
"kigi/code/goto-definition" => {
let req: GotoRequest = serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::invalid_params().data(format!("invalid params: {e}")))?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
@@ -209,7 +209,7 @@ pub async fn handle(
);
to_code_nav_ext_response(result)
}
"x.ai/code/goto-references" => {
"kigi/code/goto-references" => {
let req: GotoRequest = serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::invalid_params().data(format!("invalid params: {e}")))?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
@@ -238,7 +238,7 @@ pub async fn handle(
);
to_code_nav_ext_response(result)
}
"x.ai/code/find-definitions" => {
"kigi/code/find-definitions" => {
let req: FindSymbolRequest = serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::invalid_params().data(format!("invalid params: {e}")))?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
@@ -268,7 +268,7 @@ pub async fn handle(
);
to_code_nav_ext_response(result)
}
"x.ai/code/find-references" => {
"kigi/code/find-references" => {
let req: FindSymbolRequest = serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::invalid_params().data(format!("invalid params: {e}")))?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
@@ -298,7 +298,7 @@ pub async fn handle(
);
to_code_nav_ext_response(result)
}
"x.ai/code/status" => {
"kigi/code/status" => {
let req: StatusRequest = serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::invalid_params().data(format!("invalid params: {e}")))?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
@@ -422,10 +422,10 @@ fn resolve_cwd(
fn eligibility_error(reason: CodeNavEligibility) -> acp::Error {
let msg = match reason {
CodeNavEligibility::ClientNotWeb => {
"code navigation is currently only enabled for grok-web clients"
"code navigation is currently only enabled for kigi-web clients"
}
CodeNavEligibility::CapabilityNotAdvertised => {
"client must advertise x.ai/codeNavigation.enabled to use code navigation"
"client must advertise kigi/codeNavigation.enabled to use code navigation"
}
CodeNavEligibility::DisabledByConfig => "code navigation is disabled by configuration",
CodeNavEligibility::NotGitRepo => {
@@ -1,4 +1,4 @@
//! `x.ai/debug/*` extension handlers for local client testing.
//! `kigi/debug/*` extension handlers for local client testing.
//!
//! These methods bypass heuristics, sampling, cooldowns, and enabled checks
//! so client engineers can exercise notification → response flows without
@@ -17,11 +17,11 @@ use crate::session::{ExtMethodResult, SessionCommand};
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/debug/trigger_feedback" => {
"kigi/debug/trigger_feedback" => {
tracing::info!("debug: triggering test feedback request");
handle_trigger_feedback(agent, args).await
}
"x.ai/debug/arm_auto_compact" => handle_arm_auto_compact(agent, args),
"kigi/debug/arm_auto_compact" => handle_arm_auto_compact(agent, args),
_ => Err(acp::Error::method_not_found()),
}
}
@@ -1,4 +1,4 @@
//! `x.ai/feedback`, `x.ai/feedback/dismiss`, `x.ai/btw`, and `x.ai/review/*`
//! `kigi/feedback`, `kigi/feedback/dismiss`, `kigi/btw`, and `kigi/review/*`
//! extension handlers.
//!
//! - `feedback`/`feedback/dismiss`: persist user ratings/text locally; text
@@ -28,15 +28,15 @@ use crate::session::{
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/btw" => {
"kigi/btw" => {
tracing::info!("handling /btw side question");
handle_btw(agent, args).await
}
"x.ai/feedback" | "x.ai/feedback/dismiss" => {
"kigi/feedback" | "kigi/feedback/dismiss" => {
tracing::info!("handling user feedback");
handle_feedback(agent, args).await
}
m if m.starts_with("x.ai/review") => {
m if m.starts_with("kigi/review") => {
tracing::info!("handling review comment");
handle_review(args).await
}
@@ -44,7 +44,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
}
}
/// Handle `x.ai/btw` -- a side question that doesn't interrupt the current turn.
/// Handle `kigi/btw` -- a side question that doesn't interrupt the current turn.
async fn handle_btw(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -89,7 +89,7 @@ async fn handle_feedback(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult
}
match args.method.as_ref() {
"x.ai/feedback" => {
"kigi/feedback" => {
// Parse the input -- try the full ClientFeedbackInput first,
// then fall back to the simple FeedbackRequest (from /feedback slash command)
// which only has {session_id, feedback_text} and no client_type.
@@ -245,7 +245,7 @@ async fn handle_feedback(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
"x.ai/feedback/dismiss" => {
"kigi/feedback/dismiss" => {
let dismiss_input: FeedbackRequestDismiss = parse_params(args)?;
tracing::info!(
@@ -302,11 +302,11 @@ async fn handle_feedback(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult
/// Record inline code review events.
///
/// Methods:
/// - `x.ai/review/comment`: record a new inline code comment
/// - `x.ai/review/comment/delete`: record a tombstone event for a deleted comment
/// - `kigi/review/comment`: record a new inline code comment
/// - `kigi/review/comment/delete`: record a tombstone event for a deleted comment
async fn handle_review(args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/review/comment" => {
"kigi/review/comment" => {
let request: CommentRequest = parse_params(args)?;
let comment_id = uuid::Uuid::now_v7().to_string();
@@ -329,7 +329,7 @@ async fn handle_review(args: &acp::ExtRequest) -> ExtResult {
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
"x.ai/review/comment/delete" => {
"kigi/review/comment/delete" => {
let request: CommentDeleteRequest = parse_params(args)?;
tracing::info!(
@@ -169,11 +169,11 @@ async fn confine_local(
Err(acp::Error::invalid_params().data(workspace_err.to_string()))
}
pub(crate) fn is_fs_method(method: &str) -> bool {
method.starts_with("x.ai/fs/")
method.starts_with("kigi/fs/")
}
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/fs/list" => {
"kigi/fs/list" => {
let req = parse_params::<FsListRequest>(args)?;
let path = resolve_path(agent, &req.path, req.session_id.as_ref())?;
let (path, confine_root) = confine_local(agent, &path, req.session_id.as_ref()).await?;
@@ -181,7 +181,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
let result = fs::list(&path, &params, confine_root).await;
to_ext_response(result)
}
"x.ai/fs/exists" => {
"kigi/fs/exists" => {
let req = parse_params::<FsExistsRequest>(args)?;
let path = resolve_path(agent, &req.path, req.session_id.as_ref())?;
let (path, _) = match confine_local(agent, &path, req.session_id.as_ref()).await {
@@ -191,7 +191,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
let result = fs::exists(&path).await;
to_ext_response(result)
}
"x.ai/fs/read_file" => {
"kigi/fs/read_file" => {
let req = parse_params::<FsReadFileRequest>(args)?;
let max_lines = req.max_lines;
let path_str = req.path.clone();
@@ -231,7 +231,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
Err(e) => to_ext_response(Err::<FsReadFileData, _>(e)),
}
}
"x.ai/fs/write_file" => {
"kigi/fs/write_file" => {
let req = parse_params::<FsWriteFileRequest>(args)?;
let path = resolve_path(agent, &req.path, req.session_id.as_ref())?;
let (path, _) = confine_local(agent, &path, req.session_id.as_ref()).await?;
@@ -240,7 +240,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.map(|_| Empty {});
to_ext_response(result)
}
"x.ai/fs/delete_file" => {
"kigi/fs/delete_file" => {
let req = parse_params::<FsDeleteFileRequest>(args)?;
let path = resolve_path(agent, &req.path, req.session_id.as_ref())?;
let (path, _) = confine_local(agent, &path, req.session_id.as_ref()).await?;
+18 -18
View File
@@ -234,7 +234,7 @@ pub struct GitCurrentCommitRequest {
#[serde(default)]
pub git_root: Option<String>,
}
/// Request for x.ai/git/checkout_commit extension method.
/// Request for kigi/git/checkout_commit extension method.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitCheckoutCommitRequest {
@@ -331,18 +331,18 @@ pub async fn handle(
}
}
match args.method.as_ref() {
"x.ai/git/git_repo_root" => {
"kigi/git/git_repo_root" => {
let req: git::GitRepoRequest = parse_params(args)?;
let response = git::is_git_repo(&req).await?;
super::to_raw_response(&response)
}
"x.ai/git/serialize_changes" => {
"kigi/git/serialize_changes" => {
let _ = (args, ops);
to_ext_response::<()>(Err(anyhow::anyhow!(
"git serialize_changes is unavailable in this build"
)))
}
"x.ai/git/status" => {
"kigi/git/status" => {
let req = parse_params::<GitStatusRequest>(args)?;
let include_untracked = req.include_untracked.unwrap_or(true);
let include_stats = req.include_stats.unwrap_or(false);
@@ -410,7 +410,7 @@ pub async fn handle(
}
to_ext_response(Ok(result))
}
"x.ai/git/files" => {
"kigi/git/files" => {
let req = parse_params::<GitFilesRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -426,7 +426,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_ext_response(Ok(result))
}
"x.ai/git/diffs" => {
"kigi/git/diffs" => {
let req = parse_params::<GitDiffsRequest>(args)?;
let max_bytes = req.max_patch_bytes;
let max_lines = req.max_patch_lines;
@@ -455,7 +455,7 @@ pub async fn handle(
to_ext_response(Ok(data))
}
}
"x.ai/git/stage" => {
"kigi/git/stage" => {
let req = parse_params::<GitStageRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -473,7 +473,7 @@ pub async fn handle(
}
to_ext_response(Ok(result))
}
"x.ai/git/stage/content" => {
"kigi/git/stage/content" => {
let req = parse_params::<GitStageContentRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -491,7 +491,7 @@ pub async fn handle(
}
to_ext_response(Ok(Empty {}))
}
"x.ai/git/unstage" => {
"kigi/git/unstage" => {
let req = parse_params::<GitUnstageRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -508,7 +508,7 @@ pub async fn handle(
}
to_ext_response(Ok(Empty {}))
}
"x.ai/git/discard" => {
"kigi/git/discard" => {
let req = parse_params::<GitDiscardRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -527,7 +527,7 @@ pub async fn handle(
}
to_ext_response(Ok(Empty {}))
}
"x.ai/git/commit" => {
"kigi/git/commit" => {
let req = parse_params::<GitCommitRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -549,7 +549,7 @@ pub async fn handle(
}
to_ext_response_partial(Ok(commit_result.data), commit_result.warning)
}
"x.ai/git/checkout" => {
"kigi/git/checkout" => {
let req = parse_params::<GitCheckoutRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -567,7 +567,7 @@ pub async fn handle(
}
to_ext_response(Ok(Empty {}))
}
"x.ai/git/stash" => {
"kigi/git/stash" => {
let req = parse_params::<GitStashRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -584,7 +584,7 @@ pub async fn handle(
}
to_ext_response(Ok(Empty {}))
}
"x.ai/git/info" => {
"kigi/git/info" => {
let req = parse_params::<GitInfoRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -595,7 +595,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_ext_response(Ok(result))
}
"x.ai/git/branches" => {
"kigi/git/branches" => {
let req = parse_params::<GitBranchesRequest>(args)?;
let git_root = resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -606,7 +606,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_ext_response(Ok(result))
}
"x.ai/git/current_commit" => {
"kigi/git/current_commit" => {
let req = parse_params::<GitCurrentCommitRequest>(args)?;
let result = match resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref())
.await
@@ -620,7 +620,7 @@ pub async fn handle(
};
to_ext_response(Ok(result))
}
"x.ai/git/checkout_session_head" => {
"kigi/git/checkout_session_head" => {
let req = parse_params::<CheckoutSessionHeadRequest>(args)?;
let git_root =
resolve_git_root(agent, ops, req.git_root, Some(&req.session_id)).await?;
@@ -664,7 +664,7 @@ pub async fn handle(
invalidate_status_cache(&git_root);
super::to_raw_response(&result)
}
"x.ai/git/checkout_commit" => {
"kigi/git/checkout_commit" => {
let req = parse_params::<GitCheckoutCommitRequest>(args)?;
let git_root =
resolve_git_root(agent, ops, req.git_root, req.session_id.as_ref()).await?;
@@ -1,4 +1,4 @@
//! `x.ai/hooks/*` extension handlers.
//! `kigi/hooks/*` extension handlers.
//!
//! The file-hook list/action endpoints for the pager's hooks modal, plus the
//! client-registered hook wire types and `parse_client_hooks`.
@@ -76,7 +76,7 @@ pub fn hook_spec_to_info(spec: &kigi_hooks::config::HookSpec) -> HookInfo {
}
}
// Wire types for client-registered hooks (`x.ai/hooks/run`); the gate that uses
// Wire types for client-registered hooks (`kigi/hooks/run`); the gate that uses
// them lives in `session::acp_session::hooks`.
/// A matcher group from the client's registration: `{ matcher, hookCallbackIds, timeout }`.
@@ -97,7 +97,7 @@ pub type ClientHooks = HashMap<HookEventName, Vec<ClientHookGroup>>;
/// One hook dispatched to a client callback: the shared [`HookEventEnvelope`]
/// (flattened, camelCase) plus the `hookCallbackId` it targets. The same shape is sent
/// for both the `x.ai/hooks/run` request (gate) and the `x.ai/hooks/event` notification
/// for both the `kigi/hooks/run` request (gate) and the `kigi/hooks/event` notification
/// (observe-only), so the client decodes one payload for every hook.
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
@@ -118,7 +118,7 @@ pub(crate) enum ClientHookDecision {
Other,
}
/// Response payload for `x.ai/hooks/run` (client to agent). `Default` (used on
/// Response payload for `kigi/hooks/run` (client to agent). `Default` (used on
/// timeout, transport error, or a malformed reply) proceeds.
#[derive(Debug, Clone, Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -130,7 +130,7 @@ pub(crate) struct ClientHookResponse {
pub system_message: Option<String>,
}
/// Parse client hooks from `session/new` `_meta["x.ai/hooks"]`, shaped
/// Parse client hooks from `session/new` `_meta["kigi/hooks"]`, shaped
/// `{ "<Event>": [{ matcher, hookCallbackIds }] }` (PascalCase or snake_case
/// events). Each `matcher` is compiled with the agent's [`HookMatcher`] so client
/// and file hooks match identically. Unknown events, malformed groups, invalid
@@ -138,7 +138,7 @@ pub(crate) struct ClientHookResponse {
pub(crate) fn parse_client_hooks(meta: Option<&acp::Meta>) -> ClientHooks {
let mut hooks = ClientHooks::new();
let Some(map) = meta
.and_then(|m| m.get("x.ai/hooks"))
.and_then(|m| m.get("kigi/hooks"))
.and_then(|h| h.as_object())
else {
return hooks;
@@ -146,11 +146,11 @@ pub(crate) fn parse_client_hooks(meta: Option<&acp::Meta>) -> ClientHooks {
for (event_name, value) in map {
let de = serde::de::value::StrDeserializer::<serde::de::value::Error>::new(event_name);
let Ok(event) = HookEventName::deserialize(de) else {
tracing::warn!(event = %event_name, "ignoring unknown x.ai/hooks event");
tracing::warn!(event = %event_name, "ignoring unknown kigi/hooks event");
continue;
};
let Some(array) = value.as_array() else {
tracing::warn!(event = %event_name, "x.ai/hooks event value is not an array; skipping");
tracing::warn!(event = %event_name, "kigi/hooks event value is not an array; skipping");
continue;
};
let groups: Vec<ClientHookGroup> = array
@@ -167,10 +167,10 @@ pub(crate) fn parse_client_hooks(meta: Option<&acp::Meta>) -> ClientHooks {
}
/// Hooks to apply on a `load_session` reconnect: `Some` (possibly empty, an explicit
/// clear) when the request meta carries `x.ai/hooks`, else `None` so a reconnect that
/// clear) when the request meta carries `kigi/hooks`, else `None` so a reconnect that
/// omits the key leaves the live registrations from `session/new` untouched.
pub(crate) fn reconnect_client_hooks(meta: Option<&acp::Meta>) -> Option<ClientHooks> {
meta.and_then(|m| m.get("x.ai/hooks"))
meta.and_then(|m| m.get("kigi/hooks"))
.map(|_| parse_client_hooks(meta))
}
@@ -191,10 +191,10 @@ fn parse_hook_group(event: HookEventName, value: &serde_json::Value) -> Option<C
}
let group = WireGroup::deserialize(value)
.inspect_err(|err| tracing::warn!(%event, %err, "ignoring malformed x.ai/hooks group"))
.inspect_err(|err| tracing::warn!(%event, %err, "ignoring malformed kigi/hooks group"))
.ok()?;
if group.hook_callback_ids.is_empty() {
tracing::warn!(%event, "ignoring x.ai/hooks group with no hookCallbackIds");
tracing::warn!(%event, "ignoring kigi/hooks group with no hookCallbackIds");
return None;
}
// Drop a non-finite/non-positive timeout (fall back to the default gate timeout) and
@@ -211,7 +211,7 @@ fn parse_hook_group(event: HookEventName, value: &serde_json::Value) -> Option<C
Some(pattern) => match HookMatcher::new(pattern) {
Ok(matcher) => Some(matcher),
Err(err) => {
tracing::warn!(%event, pattern, %err, "ignoring x.ai/hooks group with invalid matcher");
tracing::warn!(%event, pattern, %err, "ignoring kigi/hooks group with invalid matcher");
return None;
}
},
@@ -225,7 +225,7 @@ fn parse_hook_group(event: HookEventName, value: &serde_json::Value) -> Option<C
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/hooks/list" => {
"kigi/hooks/list" => {
let req: ListRequest = super::parse_params(args)?;
let sid = acp::SessionId::new(req.session_id);
@@ -235,7 +235,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.ok_or_else(|| anyhow::anyhow!("session not found"));
super::to_ext_response(result)
}
"x.ai/hooks/action" => {
"kigi/hooks/action" => {
let req: kigi_hooks_plugins_types::HooksActionRequest = super::parse_params(args)?;
let sid = acp::SessionId::new(req.session_id);
@@ -317,7 +317,7 @@ mod tests {
#[test]
fn parse_client_hooks_parses_valid_groups() {
let meta = serde_json::json!({
"x.ai/hooks": {
"kigi/hooks": {
"PreToolUse": [
{ "matcher": "run_terminal_command", "hookCallbackIds": ["cb_0"] },
{ "matcher": null, "hookCallbackIds": ["cb_1"] },
@@ -349,7 +349,7 @@ mod tests {
let meta = serde_json::json!({
"NotARealEvent": [{ "hookCallbackIds": ["x"] }],
"x.ai/hooks": {
"kigi/hooks": {
"PreToolUse": [
{ "matcher": "[invalid", "hookCallbackIds": ["bad_regex"] },
{ "matcher": "run_terminal_command", "hookCallbackIds": [] },
@@ -367,7 +367,7 @@ mod tests {
#[test]
fn parse_client_hooks_reads_group_timeout() {
let meta = serde_json::json!({
"x.ai/hooks": {
"kigi/hooks": {
"PreToolUse": [
{ "hookCallbackIds": ["a"], "timeout": 5.0 },
{ "hookCallbackIds": ["b"], "timeout": 0 },
@@ -388,14 +388,14 @@ mod tests {
#[test]
fn parse_client_hooks_canonicalizes_subagent_alias() {
let meta = serde_json::json!({
"x.ai/hooks": { "SubagentEnd": [{ "hookCallbackIds": ["cb"] }] }
"kigi/hooks": { "SubagentEnd": [{ "hookCallbackIds": ["cb"] }] }
});
let hooks = parse_client_hooks(meta.as_object());
assert!(hooks.contains_key(&HookEventName::SubagentStop));
assert!(!hooks.contains_key(&HookEventName::SubagentEnd));
}
/// Reconnect refresh applies hooks only when the load meta carries `x.ai/hooks`:
/// Reconnect refresh applies hooks only when the load meta carries `kigi/hooks`:
/// an absent key returns `None` (don't wipe `session/new` registrations); a present
/// key returns `Some` (an empty object is an explicit clear).
#[test]
@@ -403,12 +403,12 @@ mod tests {
assert!(reconnect_client_hooks(None).is_none());
assert!(reconnect_client_hooks(serde_json::json!({ "other": true }).as_object()).is_none());
let cleared = reconnect_client_hooks(serde_json::json!({ "x.ai/hooks": {} }).as_object());
let cleared = reconnect_client_hooks(serde_json::json!({ "kigi/hooks": {} }).as_object());
assert!(cleared.is_some_and(|h| h.is_empty()));
let set = reconnect_client_hooks(
serde_json::json!({
"x.ai/hooks": { "PreToolUse": [{ "hookCallbackIds": ["cb"] }] }
"kigi/hooks": { "PreToolUse": [{ "hookCallbackIds": ["cb"] }] }
})
.as_object(),
);
@@ -311,7 +311,7 @@ pub async fn handle(
// ───────────────────────────────────────────────────────────────
// Queries
// ───────────────────────────────────────────────────────────────
"x.ai/hunk-tracker/get-hunks" => {
"kigi/hunk-tracker/get-hunks" => {
let req = parse_params::<GetHunksRequest>(args)?;
let ctx = get_hunk_tracker(agent, req.session_id.as_ref())?;
@@ -355,7 +355,7 @@ pub async fn handle(
}))
}
"x.ai/hunk-tracker/get-files" => {
"kigi/hunk-tracker/get-files" => {
let req = parse_params::<GetFilesRequest>(args)?;
let ctx = get_hunk_tracker(agent, req.session_id.as_ref())?;
@@ -371,7 +371,7 @@ pub async fn handle(
to_ext_response(Ok(GetFilesResponse { files }))
}
"x.ai/hunk-tracker/get-all-file-contents" => {
"kigi/hunk-tracker/get-all-file-contents" => {
let req = parse_params::<GetFilesRequest>(args)?;
let sid = req.session_id.as_ref().map(|s| s.0.as_ref());
@@ -398,7 +398,7 @@ pub async fn handle(
to_ext_response(Ok(GetAllFileContentsResponse { files }))
}
"x.ai/hunk-tracker/get-summary" => {
"kigi/hunk-tracker/get-summary" => {
let req = parse_params::<GetSummaryRequest>(args)?;
let sid = req.session_id.as_ref().map(|s| s.0.as_ref());
@@ -412,7 +412,7 @@ pub async fn handle(
// ───────────────────────────────────────────────────────────────
// Single Hunk Action
// ───────────────────────────────────────────────────────────────
"x.ai/hunk-tracker/hunk-action" => {
"kigi/hunk-tracker/hunk-action" => {
let req = parse_params::<HunkActionRequest>(args)?;
let action_kind = match req.action.as_str() {
@@ -448,7 +448,7 @@ pub async fn handle(
// ───────────────────────────────────────────────────────────────
// Bulk Actions
// ───────────────────────────────────────────────────────────────
"x.ai/hunk-tracker/file-action" => {
"kigi/hunk-tracker/file-action" => {
let req = parse_params::<FileActionRequest>(args)?;
let action_kind = match req.action.as_str() {
@@ -479,7 +479,7 @@ pub async fn handle(
}
}
"x.ai/hunk-tracker/turn-action" => {
"kigi/hunk-tracker/turn-action" => {
let req = parse_params::<TurnActionRequest>(args)?;
let action_kind = match req.action.as_str() {
@@ -510,7 +510,7 @@ pub async fn handle(
}
}
"x.ai/hunk-tracker/all-action" => {
"kigi/hunk-tracker/all-action" => {
let req = parse_params::<AllActionRequest>(args)?;
let action_kind = match req.action.as_str() {
@@ -1,4 +1,4 @@
//! `x.ai/interject` extension handler.
//! `kigi/interject` extension handler.
//!
//! Queues a mid-turn interjection into the active session's pending
//! interjection buffer. The session actor drains it at the next safe
@@ -37,7 +37,7 @@ fn split_content(content: Vec<acp::ContentBlock>) -> (Option<String>, Vec<acp::I
(text_override, crate::session::image_blocks(content))
}
/// Handle `x.ai/interject` — queue a mid-turn user interjection.
/// Handle `kigi/interject` — queue a mid-turn user interjection.
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
let req: InterjectRequest = parse_params(args)?;
let sid: acp::SessionId = req.session_id.clone().into();
+11 -11
View File
@@ -6,7 +6,7 @@ use super::{Empty, ExtResult, to_ext_response, to_ext_response_partial};
use kigi_workspace::session::git::{CommitData, StageData};
use kigi_workspace::session::jj;
/// Handle a `x.ai/git/*` method for a jj-colocated repo.
/// Handle a `kigi/git/*` method for a jj-colocated repo.
///
/// Returns `Some(result)` if handled, `None` to fall through to git.
pub async fn try_handle(
@@ -15,18 +15,18 @@ pub async fn try_handle(
raw_params: &serde_json::value::RawValue,
) -> Option<ExtResult> {
match method {
"x.ai/git/status" => Some(to_ext_response(jj::status(git_root).await)),
"x.ai/git/info" => Some(to_ext_response(jj::info(git_root).await)),
"kigi/git/status" => Some(to_ext_response(jj::status(git_root).await)),
"kigi/git/info" => Some(to_ext_response(jj::info(git_root).await)),
// git HEAD points at `@-` in a colocated repo; route to jj so we report
// the working-copy commit (`@`), consistent with `status`/`info`.
"x.ai/git/current_commit" => Some(to_ext_response(jj::current_commit(git_root).await)),
"x.ai/git/branches" => Some(to_ext_response(jj::list_bookmarks(git_root).await)),
"kigi/git/current_commit" => Some(to_ext_response(jj::current_commit(git_root).await)),
"kigi/git/branches" => Some(to_ext_response(jj::list_bookmarks(git_root).await)),
// jj has no staging area — stage/unstage are no-ops
"x.ai/git/stage" => Some(to_ext_response(Ok(StageData { paths: Vec::new() }))),
"x.ai/git/stage/content" | "x.ai/git/unstage" => Some(to_ext_response(Ok(Empty {}))),
"kigi/git/stage" => Some(to_ext_response(Ok(StageData { paths: Vec::new() }))),
"kigi/git/stage/content" | "kigi/git/unstage" => Some(to_ext_response(Ok(Empty {}))),
"x.ai/git/discard" => {
"kigi/git/discard" => {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct Req {
@@ -39,7 +39,7 @@ pub async fn try_handle(
))
}
"x.ai/git/commit" => {
"kigi/git/commit" => {
#[derive(serde::Deserialize)]
struct Req {
message: String,
@@ -53,9 +53,9 @@ pub async fn try_handle(
}
// Operations that don't apply to jj
"x.ai/git/checkout" => Some(Err(acp::Error::invalid_params()
"kigi/git/checkout" => Some(Err(acp::Error::invalid_params()
.data("checkout is not supported in jj repos; use `jj new` or `jj edit`"))),
"x.ai/git/stash" => Some(Err(acp::Error::invalid_params()
"kigi/git/stash" => Some(Err(acp::Error::invalid_params()
.data("stash is not supported in jj repos; changes are always committed"))),
// Everything else (diffs, files, serialize_changes) falls through to git
+23 -23
View File
@@ -1,9 +1,9 @@
//! MCP extension methods and business logic.
//!
//! - `x.ai/mcp/list` — list available MCP servers (agent-scoped or session-annotated)
//! - `x.ai/mcp/call` — invoke an MCP tool directly, outside the LLM loop
//! - `x.ai/mcp/servers_updated` — notification pushed when the server list changes
//! - `x.ai/mcp/server_status` — per-server delta pushed by the
//! - `kigi/mcp/list` — list available MCP servers (agent-scoped or session-annotated)
//! - `kigi/mcp/call` — invoke an MCP tool directly, outside the LLM loop
//! - `kigi/mcp/servers_updated` — notification pushed when the server list changes
//! - `kigi/mcp/server_status` — per-server delta pushed by the
//! `StatusDispatcher` (transport-closed pollers, handshake failures,
//! config diffs, server-pushed list-changed notifications). See
//! [`crate::session::mcp_dispatcher`] for the coalescing /
@@ -24,7 +24,7 @@ use kigi_mcp::wire;
use super::{ExtResult, parse_params, to_ext_response};
/// Agent-only `x.ai/mcp/*` ACP method/notification names.
/// Agent-only `kigi/mcp/*` ACP method/notification names.
///
/// Unlike [`wire::MCP_CALL`] (the cross-SDK contract, which stays in
/// `kigi_mcp::wire`), these methods are private to the agent↔client channel and
@@ -32,20 +32,20 @@ use super::{ExtResult, parse_params, to_ext_response};
/// same string literal across dispatch and notification send sites.
pub mod mcp_methods {
/// Shared prefix that routes every MCP ext method to this module's dispatcher.
pub const PREFIX: &str = "x.ai/mcp/";
pub const PREFIX: &str = "kigi/mcp/";
pub const LIST: &str = "x.ai/mcp/list";
pub const READ_RESOURCE: &str = "x.ai/mcp/read_resource";
pub const AUTH_STATUS: &str = "x.ai/mcp/auth_status";
pub const AUTH_TRIGGER: &str = "x.ai/mcp/auth_trigger";
pub const TOGGLE: &str = "x.ai/mcp/toggle";
pub const TOGGLE_TOOL: &str = "x.ai/mcp/toggle_tool";
pub const UPSERT: &str = "x.ai/mcp/upsert";
pub const DELETE: &str = "x.ai/mcp/delete";
pub const LIST: &str = "kigi/mcp/list";
pub const READ_RESOURCE: &str = "kigi/mcp/read_resource";
pub const AUTH_STATUS: &str = "kigi/mcp/auth_status";
pub const AUTH_TRIGGER: &str = "kigi/mcp/auth_trigger";
pub const TOGGLE: &str = "kigi/mcp/toggle";
pub const TOGGLE_TOOL: &str = "kigi/mcp/toggle_tool";
pub const UPSERT: &str = "kigi/mcp/upsert";
pub const DELETE: &str = "kigi/mcp/delete";
pub const SERVERS_UPDATED: &str = "x.ai/mcp/servers_updated";
pub const TOOLS_CHANGED: &str = "x.ai/mcp/tools_changed";
pub const INIT_PROGRESS: &str = "x.ai/mcp/init_progress";
pub const SERVERS_UPDATED: &str = "kigi/mcp/servers_updated";
pub const TOOLS_CHANGED: &str = "kigi/mcp/tools_changed";
pub const INIT_PROGRESS: &str = "kigi/mcp/init_progress";
}
use crate::agent::MvpAgent;
use crate::session::mcp_servers::{MCP_TOOL_NAME_DELIMITER, McpClient, McpServerName, McpState};
@@ -247,9 +247,9 @@ pub struct McpToolsChanged {
pub tools: Vec<McpToolEntry>,
}
// Re-export the `x.ai/mcp/server_status` schema +
// Re-export the `kigi/mcp/server_status` schema +
// method constant from the dispatcher module so external callers
// have a single import point alongside the other `x.ai/mcp/*`
// have a single import point alongside the other `kigi/mcp/*`
// types.
//
// The canonical definitions still live in
@@ -308,13 +308,13 @@ pub async fn notify_servers_updated(
if let Ok(params) = serde_json::value::to_raw_value(&payload) {
let notification = acp::ExtNotification::new(mcp_methods::SERVERS_UPDATED, params.into());
let _ = gateway.ext_notification(notification).await;
tracing::info!("Sent x.ai/mcp/servers_updated notification to client");
tracing::info!("Sent kigi/mcp/servers_updated notification to client");
}
}
// ── Dispatch ────────────────────────────────────────────────────────
/// Inbound `x.ai/mcp/*` methods this agent services, resolved from the wire string.
/// Inbound `kigi/mcp/*` methods this agent services, resolved from the wire string.
///
/// Single source of truth for forward-method routing: [`handle`] maps each variant to
/// its handler, and an unknown method yields `None` → `method_not_found`. The reverse
@@ -1403,7 +1403,7 @@ async fn handle_delete(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
mod tests {
use super::*;
/// The emit-only reverse method (`x.ai/mcp/sdk_call`) shares the `x.ai/mcp/`
/// The emit-only reverse method (`kigi/mcp/sdk_call`) shares the `kigi/mcp/`
/// prefix, so `mvp_agent`'s dispatcher routes an inbound copy of it to this
/// module's `handle`. It must NOT collide with any forward route — i.e. it has no
/// `McpRoute`, so `handle` returns `method_not_found` instead of misrouting a stray
@@ -1417,7 +1417,7 @@ mod tests {
assert_eq!(
route_mcp_method(wire::MCP_SDK_CALL),
None,
"inbound x.ai/mcp/sdk_call must not resolve to a forward handler"
"inbound kigi/mcp/sdk_call must not resolve to a forward handler"
);
// Sanity: the forward sibling on the same prefix DOES route.
assert_eq!(route_mcp_method(wire::MCP_CALL), Some(McpRoute::Call));
@@ -1,4 +1,4 @@
//! `x.ai/memory/flush`, `x.ai/memory/rewrite`, and `x.ai/compact_conversation`
//! `kigi/memory/flush`, `kigi/memory/rewrite`, and `kigi/compact_conversation`
//! extension handlers.
//!
//! - `compact_conversation`: trigger an on-demand compaction for a session.
@@ -17,9 +17,9 @@ use crate::session::{CompactConversationRequest, CompactConversationResponse, Se
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
m if m.starts_with("x.ai/compact_conversation") => handle_compact(agent, args).await,
"x.ai/memory/flush" => handle_flush(agent, args).await,
"x.ai/memory/rewrite" => handle_rewrite(agent, args).await,
m if m.starts_with("kigi/compact_conversation") => handle_compact(agent, args).await,
"kigi/memory/flush" => handle_flush(agent, args).await,
"kigi/memory/rewrite" => handle_rewrite(agent, args).await,
_ => Err(acp::Error::method_not_found()),
}
}
@@ -497,7 +497,7 @@ pub enum SessionUpdate {
},
/// A short "where was I" recap of the session so far.
///
/// Emitted by the `x.ai/recap` ext method: on demand via the `/recap`
/// Emitted by the `kigi/recap` ext method: on demand via the `/recap`
/// slash command (`auto = false`), or automatically when the user
/// returns to the terminal after being away (`auto = true`). The pager
/// renders it as an informational scrollback line; it is never added to
@@ -881,8 +881,8 @@ pub enum SessionUpdate {
/// pending ⏳ for this `tool_call_id`.
InteractionResolved { tool_call_id: String },
/// The durable, replayable signal that a turn reached its terminal
/// outcome. Rides the persisted `_x.ai/session/update` rail (unlike the
/// fire-and-forget `x.ai/session/prompt_complete` notification), so a
/// outcome. Rides the persisted `_kigi/session/update` rail (unlike the
/// fire-and-forget `kigi/session/prompt_complete` notification), so a
/// viewer that re-attaches mid-turn can finalize the turn from replay
/// instead of staying stuck on "Waiting…".
TurnCompleted {
@@ -1132,7 +1132,7 @@ pub struct CompactionRequestFile {
/// Schema version for forward compatibility.
pub schema_version: u32,
/// Unique artifact identifier (filename stem).
/// Note: this is a per-artifact ID, not the model API's `x_grok_req_id`
/// Note: this is a per-artifact ID, not the model API's `x_kigi_req_id`
/// (which is generated per-attempt inside the sampling layer).
pub request_id: String,
/// ISO 8601 timestamp of when the compaction call started.
@@ -1140,7 +1140,7 @@ pub struct CompactionRequestFile {
/// What kicked off the compaction: `"manual"` (user ran `/compact`) or `"auto"`.
pub trigger: String,
/// Which prompt template was used: `"short"` (concise self-summarization)
/// or `"detailed"` (10-section structured prompt for grok-build and similar agents).
/// or `"detailed"` (10-section structured prompt for kigi and similar agents).
pub prompt_variant: String,
/// The model id that ran the summarization.
pub model: String,
@@ -1189,7 +1189,7 @@ pub struct RecapRequestFile {
/// Schema version for forward compatibility.
pub schema_version: u32,
/// Unique artifact identifier (filename stem). Distinct from the model
/// API's `x_grok_req_id` (also recorded below for proxy correlation).
/// API's `x_kigi_req_id` (also recorded below for proxy correlation).
pub request_id: String,
/// ISO 8601 timestamp of when the recap model call started.
pub created_at: String,
@@ -1199,9 +1199,9 @@ pub struct RecapRequestFile {
/// The model id used for the recap side-call.
pub model: String,
/// Sampling request id sent to the proxy (`xai-recap-{uuid}`).
pub x_grok_req_id: String,
pub x_kigi_req_id: String,
/// Sampling conversation id (`recap-{uuid}`).
pub x_grok_conv_id: String,
pub x_kigi_conv_id: String,
/// Whether reasoning/thinking blocks were stripped from the prefix
/// (Anthropic Messages backend only; other backends keep reasoning
/// verbatim for prompt-cache warmth).
@@ -1238,8 +1238,8 @@ mod tests {
created_at: "2026-06-30T00:00:00Z".into(),
trigger: "auto".into(),
model: "v9-zingster".into(),
x_grok_req_id: "xai-recap-abc".into(),
x_grok_conv_id: "recap-abc".into(),
x_kigi_req_id: "xai-recap-abc".into(),
x_kigi_conv_id: "recap-abc".into(),
strip_reasoning: false,
reminder_tag: "system-reminder".into(),
chat_history: vec![],
@@ -1251,7 +1251,7 @@ mod tests {
let parsed: RecapRequestFile = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.schema_version, 1);
assert_eq!(parsed.trigger, "auto");
assert_eq!(parsed.x_grok_req_id, "xai-recap-abc");
assert_eq!(parsed.x_kigi_req_id, "xai-recap-abc");
assert_eq!(
parsed.summary.as_deref(),
Some("We fixed the flaky test in queue_worker.")
@@ -1268,7 +1268,7 @@ mod tests {
created_at: "2026-06-15T00:00:00Z".into(),
trigger: "auto".into(),
prompt_variant: "detailed".into(),
model: "grok".into(),
model: "kigi".into(),
user_context: None,
chat_history: vec![],
tools: vec![],
@@ -1317,7 +1317,7 @@ mod tests {
"created_at": "2026-06-01T00:00:00Z",
"trigger": "manual",
"prompt_variant": "detailed",
"model": "grok",
"model": "kigi",
"user_context": null,
"chat_history": [],
"summary": "ok",
@@ -1752,7 +1752,7 @@ mod tests {
total_worker_rounds: 4,
total_verify_rounds: 2,
live_subagent_tokens: Some(10_000),
live_tokens_by_model: vec![("grok-4".into(), 6_000), ("grok-3".into(), 4_000)],
live_tokens_by_model: vec![("kigi-4".into(), 6_000), ("kigi-3".into(), 4_000)],
live_context_pct: Some(35),
live_turn_count: Some(3),
live_tool_call_count: Some(8),
@@ -1826,7 +1826,7 @@ mod tests {
assert_eq!(json["total_worker_rounds"], 4);
assert_eq!(json["total_verify_rounds"], 2);
assert_eq!(json["live_subagent_tokens"], 10_000);
assert_eq!(json["live_tokens_by_model"][0][0], "grok-4");
assert_eq!(json["live_tokens_by_model"][0][0], "kigi-4");
assert_eq!(json["live_tokens_by_model"][0][1], 6_000);
assert_eq!(json["live_context_pct"], 35);
assert_eq!(json["last_event"], "worker_completed");
@@ -2001,21 +2001,21 @@ mod tests {
#[test]
fn model_changed_serializes_snake_case_with_optional_effort() {
let with_effort = SessionUpdate::ModelChanged {
model_id: "grok-4".into(),
model_id: "kigi-4".into(),
reasoning_effort: Some("high".into()),
};
let json = serde_json::to_value(&with_effort).unwrap();
assert_eq!(json["sessionUpdate"], "model_changed");
assert_eq!(json["model_id"], "grok-4");
assert_eq!(json["model_id"], "kigi-4");
assert_eq!(json["reasoning_effort"], "high");
let without_effort = SessionUpdate::ModelChanged {
model_id: "grok-3".into(),
model_id: "kigi-3".into(),
reasoning_effort: None,
};
let json = serde_json::to_value(&without_effort).unwrap();
assert_eq!(json["sessionUpdate"], "model_changed");
assert_eq!(json["model_id"], "grok-3");
assert_eq!(json["model_id"], "kigi-3");
assert!(
json.get("reasoning_effort").is_none(),
"reasoning_effort: None must be skipped on the wire so old pagers \
@@ -2031,7 +2031,7 @@ mod tests {
#[test]
fn model_changed_roundtrips_through_json() {
let original = SessionUpdate::ModelChanged {
model_id: "grok-4".into(),
model_id: "kigi-4".into(),
reasoning_effort: Some("medium".into()),
};
let json_str = serde_json::to_string(&original).unwrap();
@@ -2052,7 +2052,7 @@ mod tests {
let notif = SessionNotification {
session_id: acp::SessionId::new("sess-abc"),
update: SessionUpdate::ModelChanged {
model_id: "grok-4".into(),
model_id: "kigi-4".into(),
reasoning_effort: None,
},
meta: None,
@@ -2060,7 +2060,7 @@ mod tests {
let json = serde_json::to_value(&notif).unwrap();
assert_eq!(json["sessionId"], "sess-abc");
assert_eq!(json["update"]["sessionUpdate"], "model_changed");
assert_eq!(json["update"]["model_id"], "grok-4");
assert_eq!(json["update"]["model_id"], "kigi-4");
}
// ── TurnCompleted (durable, replayable turn-end signal) ──
@@ -1,4 +1,4 @@
//! `x.ai/plugins/*` extension handlers.
//! `kigi/plugins/*` extension handlers.
//!
//! Provides the plugins list endpoint for the pager's hooks/plugins modal.
@@ -79,9 +79,9 @@ fn origin_to_dto(origin: &kigi_agent::plugins::PluginOrigin) -> PluginOrigin {
use kigi_agent::plugins::PluginOrigin as AgentOrigin;
match origin {
AgentOrigin::CliOverride => PluginOrigin::CliOverride,
AgentOrigin::ProjectGrok => PluginOrigin::ProjectGrok,
AgentOrigin::ProjectKigi => PluginOrigin::ProjectKigi,
AgentOrigin::ProjectClaude => PluginOrigin::ProjectClaude,
AgentOrigin::UserGrok => PluginOrigin::UserGrok,
AgentOrigin::UserKigi => PluginOrigin::UserKigi,
AgentOrigin::UserClaude => PluginOrigin::UserClaude,
AgentOrigin::ClaudeMarketplace { marketplace } => PluginOrigin::ClaudeMarketplace {
marketplace: marketplace.clone(),
@@ -123,7 +123,7 @@ fn marketplace_source_label(origin: &PluginOrigin) -> Option<String> {
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/plugins/list" => {
"kigi/plugins/list" => {
let req: ListRequest = super::parse_params(args)?;
// A known session answers from its own registry, which includes
@@ -149,7 +149,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
};
super::to_ext_response(Ok::<_, anyhow::Error>(response))
}
"x.ai/plugins/action" => {
"kigi/plugins/action" => {
let req: kigi_hooks_plugins_types::PluginsActionRequest = super::parse_params(args)?;
let sid = acp::SessionId::new(req.session_id);
@@ -159,7 +159,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.ok_or_else(|| anyhow::anyhow!("session not found"));
super::to_ext_response(result)
}
"x.ai/plugins/notify-updates" => {
"kigi/plugins/notify-updates" => {
// Broadcast a PluginUpdatesInstalled notification to the session.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -56,7 +56,7 @@ struct GhGraphqlPullRequest {
pub async fn handle(_agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/pr/status" => {
"kigi/pr/status" => {
let req = parse_params::<PrStatusRequest>(args)?;
to_ext_response(handle_pr_status(&req.cwd, &req.branch).await)
}
@@ -1,4 +1,4 @@
//! `x.ai/prompt_history` extension handler.
//! `kigi/prompt_history` extension handler.
//!
//! Returns the user-prompt history for a given cwd. Three paths:
//! - **fast path** (no ids): reads the per-CWD `prompt_history.jsonl` file
@@ -45,7 +45,7 @@ struct PromptHistoryResponse {
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(_agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/prompt_history" => handle_prompt_history(args).await,
"kigi/prompt_history" => handle_prompt_history(args).await,
_ => Err(acp::Error::method_not_found()),
}
}
@@ -1,4 +1,4 @@
//! `x.ai/recap` extension handler.
//! `kigi/recap` extension handler.
//!
//! Triggers generation of a session recap — a short "where was I" summary of
//! the session so far — via [`SessionCommand::Recap`]. This is fire-and-forget:
@@ -1,4 +1,4 @@
//! `x.ai/session/repair` — out-of-band recovery for sessions bricked by
//! `kigi/session/repair` — out-of-band recovery for sessions bricked by
//! corrupted tool-pairing history.
//!
//! A `ToolResult` whose owning assistant `tool_call` is missing (e.g. a
@@ -32,7 +32,7 @@ struct RepairSessionRequest {
dry_run: bool,
}
/// Response payload for `x.ai/session/repair`.
/// Response payload for `kigi/session/repair`.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepairSessionResponse {
@@ -67,7 +67,7 @@ impl RepairSessionResponse {
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/session/repair" => handle_session_repair(agent, args).await,
"kigi/session/repair" => handle_session_repair(agent, args).await,
_ => Err(acp::Error::method_not_found()),
}
}
@@ -105,18 +105,18 @@ async fn handle_session_repair(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtR
/// Repair a non-resident session's history on disk: load via the resume
/// path's corruption-tolerant reader (legacy upgrades apply), repair, write
/// back atomically. `grok_root` is injectable for tests.
async fn repair_on_disk(grok_root: &std::path::Path, session_id: &str, dry_run: bool) -> ExtResult {
/// back atomically. `kigi_root` is injectable for tests.
async fn repair_on_disk(kigi_root: &std::path::Path, session_id: &str, dry_run: bool) -> ExtResult {
let summary = crate::session::persistence::find_summary_by_session_id_in_root(
session_id,
&grok_root.join("sessions"),
&kigi_root.join("sessions"),
)
.ok_or_else(|| {
acp::Error::resource_not_found(Some(format!("session not found: {session_id}")))
})?;
let info = summary.info.clone();
let storage = JsonlStorageAdapter::with_root(grok_root.to_path_buf());
let storage = JsonlStorageAdapter::with_root(kigi_root.to_path_buf());
let mut chat_history = storage
.load_session_without_updates(&info)
.await
@@ -1,4 +1,4 @@
//! `x.ai/rewind/*` extension handlers.
//! `kigi/rewind/*` extension handlers.
//!
//! - `rewind/execute`: rewind a session to a target prompt index, optionally
//! forcing past in-flight prompts and choosing a `RewindMode`.
@@ -19,8 +19,8 @@ use tokio::sync::oneshot;
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
tracing::info!("handling rewind request: {}", args.method);
match args.method.as_ref() {
"x.ai/rewind/execute" => handle_execute(agent, args).await,
"x.ai/rewind/points" => handle_points(agent, args).await,
"kigi/rewind/execute" => handle_execute(agent, args).await,
"kigi/rewind/points" => handle_points(agent, args).await,
_ => Err(acp::Error::method_not_found()),
}
}
@@ -1,4 +1,4 @@
//! `x.ai/rollout/survey` extension handler.
//! `kigi/rollout/survey` extension handler.
//!
//! Logs a rollout-survey submission via telemetry (Mixpanel + BigQuery).
@@ -11,7 +11,7 @@ use crate::session::{RolloutSurveyRequest, RolloutSurveyResponse};
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(_agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/rollout/survey" => {
"kigi/rollout/survey" => {
let req: RolloutSurveyRequest = parse_params(args)?;
tracing::info_span!(
@@ -114,7 +114,7 @@ pub struct ContentSearchRequest {
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/search/fuzzy/open" => {
"kigi/search/fuzzy/open" => {
let req: FuzzyOpenRequest = parse(args.params.get())?;
let cwd = resolve_cwd(agent, req.cwd, req.session_id.as_ref())?;
let search_root = match &req.root {
@@ -149,13 +149,13 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.to_ext_response()
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
}
"x.ai/search/fuzzy/change" => {
"kigi/search/fuzzy/change" => {
let req: FuzzyChangeRequest = parse(args.params.get())?;
let ops = agent
.resolve_workspace_ops()
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
// The workspace owns the manager and spawns the status driver, which
// streams `x.ai/search/fuzzy/status` through the client sink.
// streams `kigi/search/fuzzy/status` through the client sink.
let found = ops
.dispatch(
&FuzzyChangeReq {
@@ -182,7 +182,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.to_ext_response()
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
}
"x.ai/search/fuzzy/close" => {
"kigi/search/fuzzy/close" => {
let req: FuzzyCloseRequest = parse(args.params.get())?;
let ops = agent
.resolve_workspace_ops()
@@ -206,7 +206,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.to_ext_response()
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
}
"x.ai/search/content" => {
"kigi/search/content" => {
let req: ContentSearchRequest = parse(args.params.get())?;
let cwd = resolve_cwd(agent, req.cwd.clone(), req.session_id.as_ref())?;
let context_id = req
@@ -219,7 +219,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
// The workspace runs the streaming search and emits
// `x.ai/search/content/status` batches through the client sink.
// `kigi/search/content/status` batches through the client sink.
let mut op = req.params;
op.cwd = Some(cwd);
op.context_id = Some(context_id);
@@ -4,17 +4,17 @@
//! persistent or shared agent state but are not part of the per-turn prompt
//! lifecycle:
//!
//! - `x.ai/session/rename` rename a session locally
//! - `x.ai/session/delete` delete a session locally
//! - `x.ai/session/update_mcp_servers` mid-session MCP server swap
//! - `x.ai/session/fork` fork a session into a new one
//! - `x.ai/internal/reload_all_mcp_servers` config hot-reload, all sessions
//! - `x.ai/internal/reload_project_mcp_servers` config hot-reload, cwd-scoped
//! - `x.ai/internal/reload_skills` skills file watcher fan-out
//! - `x.ai/internal/reload_models` model list hot-reload from config.toml
//! - `x.ai/internal/reload_models_cache` model catalog hot-reload from disk cache
//! - `x.ai/plugins/reload` rebuild shared plugin registry
//! - `x.ai/commands/list` list slash commands
//! - `kigi/session/rename` rename a session locally
//! - `kigi/session/delete` delete a session locally
//! - `kigi/session/update_mcp_servers` mid-session MCP server swap
//! - `kigi/session/fork` fork a session into a new one
//! - `kigi/internal/reload_all_mcp_servers` config hot-reload, all sessions
//! - `kigi/internal/reload_project_mcp_servers` config hot-reload, cwd-scoped
//! - `kigi/internal/reload_skills` skills file watcher fan-out
//! - `kigi/internal/reload_models` model list hot-reload from config.toml
//! - `kigi/internal/reload_models_cache` model catalog hot-reload from disk cache
//! - `kigi/plugins/reload` rebuild shared plugin registry
//! - `kigi/commands/list` list slash commands
use std::path::Path;
use std::sync::Arc;
@@ -34,19 +34,19 @@ use crate::session::{ExtMethodResult, SessionCommand};
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/session/rename" => handle_session_rename(agent, args).await,
"x.ai/session/delete" => handle_session_delete(agent, args).await,
"x.ai/session/update_mcp_servers" => handle_update_mcp_servers(agent, args).await,
"x.ai/session/fork" => handle_session_fork(agent, args).await,
"x.ai/internal/reload_all_mcp_servers" => handle_reload_all_mcp_servers(agent).await,
"x.ai/internal/reload_project_mcp_servers" => {
"kigi/session/rename" => handle_session_rename(agent, args).await,
"kigi/session/delete" => handle_session_delete(agent, args).await,
"kigi/session/update_mcp_servers" => handle_update_mcp_servers(agent, args).await,
"kigi/session/fork" => handle_session_fork(agent, args).await,
"kigi/internal/reload_all_mcp_servers" => handle_reload_all_mcp_servers(agent).await,
"kigi/internal/reload_project_mcp_servers" => {
handle_reload_project_mcp_servers(agent, args).await
}
"x.ai/internal/reload_skills" => handle_reload_skills(agent),
"x.ai/internal/reload_models" => handle_reload_models(agent),
"x.ai/internal/reload_models_cache" => handle_reload_models_cache(agent),
"x.ai/plugins/reload" => handle_plugins_reload(agent).await,
"x.ai/commands/list" => handle_commands_list(agent, args).await,
"kigi/internal/reload_skills" => handle_reload_skills(agent),
"kigi/internal/reload_models" => handle_reload_models(agent),
"kigi/internal/reload_models_cache" => handle_reload_models_cache(agent),
"kigi/plugins/reload" => handle_plugins_reload(agent).await,
"kigi/commands/list" => handle_commands_list(agent, args).await,
_ => Err(acp::Error::method_not_found()),
}
}
@@ -147,7 +147,7 @@ async fn notify_session_title(agent: &MvpAgent, session_id: acp::SessionId, titl
};
if let Ok(params) = serde_json::value::to_raw_value(&notification) {
let ext_notification =
acp::ExtNotification::new("x.ai/session_notification", params.into());
acp::ExtNotification::new("kigi/session_notification", params.into());
let _ = agent.gateway.ext_notification(ext_notification).await;
}
}
@@ -509,7 +509,7 @@ async fn handle_plugins_reload(agent: &MvpAgent) -> ExtResult {
let remote_settings = agent.cfg.borrow().remote_settings.clone();
crate::agent::folder_trust::resolve_and_record(c, remote_settings.as_ref(), false)
});
// Explicit desktop `x.ai/plugins/reload`: force a full local-install re-copy.
// Explicit desktop `kigi/plugins/reload`: force a full local-install re-copy.
agent
.plugin_registry_handle()
.reload(session_cwd.as_deref(), &disk_cfg, project_trusted, true);
@@ -534,7 +534,7 @@ async fn handle_commands_list(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtRe
// For a given cwd, compute the plugin registry the same way a session would
// at spawn time (via build_for_cwd) and the same way reload_plugins_impl does
// (ancestor project config walk + vendor compat merge). This is required so
// that `x.ai/commands/list` (the pull used by grok-desktop after session
// that `kigi/commands/list` (the pull used by kigi-desktop after session
// start) returns plugin-provided slash commands for the target cwd.
//
// The shared snapshot is only populated at agent boot (using process CWD)
@@ -1,4 +1,4 @@
//! ACP extension handler for session search (`x.ai/session/search`).
//! ACP extension handler for session search (`kigi/session/search`).
//!
//! Exposes session full-text search as an ACP extension method.
//! The client sends a query and receives ranked results across all
@@ -67,10 +67,10 @@ pub struct SearchSessionHit {
pub snippet: Option<String>,
}
/// Route `x.ai/session/search` extension method calls.
/// Route `kigi/session/search` extension method calls.
pub async fn handle(args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/session/search" => {
"kigi/session/search" => {
let req: SearchSessionsRequest = super::parse_params(args)?;
let internal_req = SessionSearchRequest {
query: req.query,
@@ -1,4 +1,4 @@
//! ACP extension handler for bulk session updates (`x.ai/session/updates`).
//! ACP extension handler for bulk session updates (`kigi/session/updates`).
//!
//! Returns session updates in a single response with rewind dead branches
//! filtered out. Supports optional pagination (`offset`/`limit`) for large
@@ -30,7 +30,7 @@
//! Each element in the `updates` array is the full JSONL storage envelope
//! (with `timestamp`, `method`, and `params` wrapper), not just the inner
//! notification params. Clients should parse the `method` field to determine
//! the update type (`"session/update"` for ACP, `"_x.ai/session/update"` for
//! the update type (`"session/update"` for ACP, `"_kigi/session/update"` for
//! xAI extensions) and extract the notification payload from `params`.
use std::io::{self, BufRead, BufReader};
@@ -285,7 +285,7 @@ fn extract_last_event_id<T: AsRef<str>>(lines: &[T]) -> Option<String> {
None
}
/// Send updates as chunked `_x.ai/session/updates/chunk` notifications.
/// Send updates as chunked `_kigi/session/updates/chunk` notifications.
/// Injects routing metadata when `target_client_id` is set.
fn send_streamed_chunks<T: AsRef<str>>(
gateway: &kigi_acp_lib::AcpAgentGatewaySender,
@@ -317,7 +317,7 @@ fn send_streamed_chunks<T: AsRef<str>>(
if let Ok(raw) = serde_json::value::to_raw_value(&params) {
gateway.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/session/updates/chunk",
"kigi/session/updates/chunk",
std::sync::Arc::from(raw),
));
}
@@ -523,7 +523,7 @@ mod tests {
}
let raw = serde_json::value::to_raw_value(&serde_json::Value::Object(map)).unwrap();
acp::ExtRequest::new("x.ai/session/updates", std::sync::Arc::from(raw))
acp::ExtRequest::new("kigi/session/updates", std::sync::Arc::from(raw))
}
#[tokio::test]
@@ -583,7 +583,7 @@ mod tests {
r#"{"timestamp":2,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"resp1"}}}}"#,
r#"{"timestamp":3,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"user_message_chunk","content":{"type":"text","text":"dead-branch"}}}}"#,
r#"{"timestamp":4,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"dead-resp"}}}}"#,
r#"{"timestamp":5,"method":"_x.ai/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"rewind_marker","target_prompt_index":1}}}"#,
r#"{"timestamp":5,"method":"_kigi/session/update","params":{"sessionId":"s","update":{"sessionUpdate":"rewind_marker","target_prompt_index":1}}}"#,
r#"{"timestamp":6,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"user_message_chunk","content":{"type":"text","text":"replacement"}}}}"#,
r#"{"timestamp":7,"method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"replacement-resp"}}}}"#,
]
@@ -659,7 +659,7 @@ mod tests {
);
assert_eq!(json["updates"].as_array().unwrap().len(), 2);
// Clean up the dir written under the real grok home.
// Clean up the dir written under the real kigi home.
let _ = std::fs::remove_dir_all(&child_dir);
}
@@ -689,7 +689,7 @@ mod tests {
map.insert("offset".into(), serde_json::json!(off));
}
let raw = serde_json::value::to_raw_value(&serde_json::Value::Object(map)).unwrap();
acp::ExtRequest::new("x.ai/session/updates", std::sync::Arc::from(raw))
acp::ExtRequest::new("kigi/session/updates", std::sync::Arc::from(raw))
}
fn extract_chunk_params(
@@ -798,7 +798,7 @@ mod tests {
map.insert("limit".into(), serde_json::json!(lim));
}
let raw = serde_json::value::to_raw_value(&serde_json::Value::Object(map)).unwrap();
acp::ExtRequest::new("x.ai/session/updates", std::sync::Arc::from(raw))
acp::ExtRequest::new("kigi/session/updates", std::sync::Arc::from(raw))
}
fn user_chunk(text: &str) -> String {
@@ -815,7 +815,7 @@ mod tests {
fn xai_rewind(target: usize) -> String {
format!(
r#"{{"timestamp":0,"method":"_x.ai/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"rewind_marker","target_prompt_index":{target}}}}}}}"#
r#"{{"timestamp":0,"method":"_kigi/session/update","params":{{"sessionId":"s","update":{{"sessionUpdate":"rewind_marker","target_prompt_index":{target}}}}}}}"#
)
}
@@ -280,7 +280,7 @@ pub async fn handle(
compat: CompatConfig,
) -> ExtResult {
match args.method.as_ref() {
"x.ai/skills/add" => {
"kigi/skills/add" => {
let req: SkillsAddRequest = serde_json::from_str(args.params.get())?;
let cwd = req.cwd.as_deref().unwrap_or(".");
@@ -326,7 +326,7 @@ pub async fn handle(
}))
}
"x.ai/skills/remove" => {
"kigi/skills/remove" => {
let req: SkillsRemoveRequest = serde_json::from_str(args.params.get())?;
let cwd = req.cwd.as_deref().unwrap_or(".");
@@ -360,7 +360,7 @@ pub async fn handle(
}))
}
"x.ai/skills/reset" => {
"kigi/skills/reset" => {
let params: CwdParams =
serde_json::from_str(args.params.get()).unwrap_or(CwdParams { cwd: None });
let cwd = params.cwd.as_deref().unwrap_or(".");
@@ -381,13 +381,13 @@ pub async fn handle(
super::to_ext_response(Ok(SkillsResetResponse { skills, message }))
}
"x.ai/skills/list" => {
"kigi/skills/list" => {
let req: SkillsListRequest = serde_json::from_str(args.params.get())?;
let skills = reload_skills(&req.cwd, plugin_registry, compat).await;
super::to_ext_response(Ok(SkillsListResponse { skills }))
}
"x.ai/skills/config" => {
"kigi/skills/config" => {
let params: CwdParams =
serde_json::from_str(args.params.get()).unwrap_or(CwdParams { cwd: None });
let cwd = params.cwd.as_deref().unwrap_or(".");
@@ -450,7 +450,7 @@ pub async fn handle(
}))
}
"x.ai/skills/toggle" => {
"kigi/skills/toggle" => {
let req: SkillsToggleRequest = serde_json::from_str(args.params.get())?;
let cwd = req.cwd.as_deref().unwrap_or(".");
@@ -42,7 +42,7 @@ impl HistoryProvider {
/// Rank history matches from three tiers of history sources.
///
/// Priority order: local grok bash history > shell history > cross-CWD history.
/// Priority order: local kigi bash history > shell history > cross-CWD history.
fn rank_history_matches(
prefix: &str,
local: &[String],
@@ -176,13 +176,13 @@ fn splice_token_into_line(results: &mut [RankedSuggestion], text: &str, range: (
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/suggest" => handle_suggest(agent, args).await,
"x.ai/suggestPrompt" => handle_suggest_prompt(agent, args).await,
"kigi/suggest" => handle_suggest(agent, args).await,
"kigi/suggestPrompt" => handle_suggest_prompt(agent, args).await,
_ => Err(acp::Error::method_not_found()),
}
}
/// Request/response for `x.ai/suggestPrompt` — predict the user's likely next
/// Request/response for `kigi/suggestPrompt` — predict the user's likely next
/// prompt after a completed turn (tab-autocomplete ghost text).
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -213,7 +213,7 @@ struct SuggestPromptResponse {
/// Upper bound on the suggestion round-trip. Turn-end prediction is not
/// latency-critical (the user is reading the agent's reply — the idle window
/// after a turn is typically long), but a hung call must not pin the oneshot
/// forever. Reasoning models (e.g. `grok-build`) can take ~30s on a cold
/// forever. Reasoning models (e.g. `kigi`) can take ~30s on a cold
/// cache; a late suggestion is still useful (the pager's generation guard
/// and empty-prompt gating discard it if the user moved on).
const SUGGEST_PROMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);
@@ -2,7 +2,7 @@ use agent_client_protocol as acp;
use kigi_tools::types::{KillOutcome, TaskSnapshot};
use serde::{Deserialize, Serialize};
use kigi_tools::implementations::grok_build::task::types::{
use kigi_tools::implementations::kigi::task::types::{
SubagentCancelOutcome, SubagentSnapshot, SubagentSnapshotStatus,
};
@@ -12,7 +12,7 @@ use crate::session::ExtMethodResult;
type ExtResult = Result<acp::ExtResponse, acp::Error>;
/// Wire DTO for the `x.ai/task/kill` ext request.
/// Wire DTO for the `kigi/task/kill` ext request.
///
/// `pub` (with both serde directions) so ACP clients (kigi-tui) build
/// the request from the same type the agent parses — keeping the wire
@@ -24,7 +24,7 @@ pub struct KillTaskRequest {
pub task_id: String,
}
/// Wire DTO for the `x.ai/task/kill` ext response payload (nested under
/// Wire DTO for the `kigi/task/kill` ext response payload (nested under
/// `result` in the `ExtMethodResult` envelope).
///
/// `pub` (with both serde directions) so ACP clients deserialize the typed
@@ -48,7 +48,7 @@ struct ListTasksResponse {
tasks: Vec<TaskSnapshot>,
}
/// Wire DTO for the `x.ai/subagent/cancel` ext request.
/// Wire DTO for the `kigi/subagent/cancel` ext request.
///
/// `pub` (with both serde directions) so ACP clients (kigi-tui) build
/// the request from the same type the agent parses.
@@ -94,7 +94,7 @@ impl From<SubagentCancelOutcome> for SubagentCancelOutcomeDto {
}
}
/// Wire DTO for the `x.ai/subagent/cancel` response payload (under `result` in
/// Wire DTO for the `kigi/subagent/cancel` response payload (under `result` in
/// the `ExtMethodResult` envelope). `pub` + both serde dirs so clients read it typed.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -328,7 +328,7 @@ fn respond<T: Serialize>(result: Result<T, impl std::fmt::Display>) -> ExtResult
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/task/kill" => {
"kigi/task/kill" => {
let req: KillTaskRequest = parse(args)?;
let result = agent
.kill_background_task(&req.session_id, &req.task_id)
@@ -339,7 +339,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
});
respond(result)
}
"x.ai/task/list" => {
"kigi/task/list" => {
let req: ListTasksRequest = parse(args)?;
let result = agent
.list_tasks(&req.session_id)
@@ -368,10 +368,10 @@ struct DeleteScheduledTaskResponse {
deleted: bool,
}
/// Handle `x.ai/scheduler/*` extension methods.
/// Handle `kigi/scheduler/*` extension methods.
pub async fn handle_scheduler(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/scheduler/delete" => {
"kigi/scheduler/delete" => {
let req: DeleteScheduledTaskRequest = parse(args)?;
let result = agent
.delete_scheduled_task(&req.session_id, &req.task_id)
@@ -386,10 +386,10 @@ pub async fn handle_scheduler(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtRe
}
}
/// Handle `x.ai/subagent/*` extension methods.
/// Handle `kigi/subagent/*` extension methods.
pub async fn handle_subagent(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/subagent/cancel" => {
"kigi/subagent/cancel" => {
let req: CancelSubagentRequest = parse(args)?;
tracing::info!(subagent_id = %req.subagent_id, "Cancelling subagent via ext method");
let outcome = SubagentCancelOutcomeDto::from(agent.cancel_subagent(&req.subagent_id));
@@ -399,7 +399,7 @@ pub async fn handle_subagent(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtRes
outcome: Some(outcome),
}))
}
"x.ai/subagent/get" => {
"kigi/subagent/get" => {
let req: GetSubagentRequest = parse(args)?;
let block = req.block.unwrap_or(false);
let timeout_ms = req.timeout_ms.unwrap_or(30_000);
@@ -442,7 +442,7 @@ pub async fn handle_subagent(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtRes
}))
}
}
"x.ai/subagent/list_running" => {
"kigi/subagent/list_running" => {
let req: ListRunningSubagentsRequest = parse(args)?;
// Sync: collect seeds from coordinator, drop borrow.
let seeds = agent.list_running_subagents(&req.session_id);
@@ -956,7 +956,7 @@ mod tests {
assert_eq!(json["forkParentPromptId"], "prompt-5");
}
// ── x.ai/subagent/cancel outcome wire DTO ──────────────────────────
// ── kigi/subagent/cancel outcome wire DTO ──────────────────────────
#[test]
fn subagent_cancel_outcome_dto_maps_from_coordinator_outcome() {
@@ -184,7 +184,7 @@ impl From<KillOutcome> for KillOutcomeResponse {
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/terminal/create" => {
"kigi/terminal/create" => {
let req: CreateTerminalRequest = parse(args)?;
let env: HashMap<String, String> = req
.env
@@ -206,7 +206,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
respond(result)
}
"x.ai/terminal/kill" => {
"kigi/terminal/kill" => {
// Try PTY registry first, then piped terminal registry.
let req: KillTerminalRequest = parse(args)?;
@@ -249,7 +249,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
}
}
"x.ai/terminal/output" => {
"kigi/terminal/output" => {
let req: TerminalIdRequest = parse(args)?;
let result = terminal::get_terminal_output(&req.session_id, &req.terminal_id)
.await
@@ -258,7 +258,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
respond(result)
}
"x.ai/terminal/wait_for_exit" => {
"kigi/terminal/wait_for_exit" => {
let req: TerminalIdRequest = parse(args)?;
let result = terminal::wait_for_terminal_exit(&req.session_id, &req.terminal_id)
.await
@@ -267,7 +267,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
respond(result)
}
"x.ai/terminal/release" => {
"kigi/terminal/release" => {
let req: TerminalIdRequest = parse(args)?;
terminal::release_terminal(&req.session_id, &req.terminal_id).await;
ExtMethodResult::success(ReleaseTerminalResponse {})
@@ -275,7 +275,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
}
"x.ai/terminal/background" => {
"kigi/terminal/background" => {
// Mark a terminal as backgrounded - the process keeps running but
// waiting callers are notified so the agent can continue.
//
@@ -292,7 +292,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
}
"x.ai/terminal/pty/create" => {
"kigi/terminal/pty/create" => {
let req: PtyCreateRequest = parse(args)?;
let env: HashMap<String, String> = req
.env
@@ -323,7 +323,7 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
respond_pty(result)
}
"x.ai/terminal/pty/load" => {
"kigi/terminal/pty/load" => {
let req: PtyLoadRequest = parse(args)?;
let target_client_id = req.meta.map(|m| m.client_id).unwrap_or_default();
let result =
@@ -332,14 +332,14 @@ pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
respond_pty(result)
}
"x.ai/terminal/pty/resize" => {
"kigi/terminal/pty/resize" => {
let req: PtyResizeRequest = parse(args)?;
respond_pty(
terminal::pty_session::resize_pty(&req.terminal_id, req.rows, req.cols).await,
)
}
"x.ai/terminal/list" => {
"kigi/terminal/list" => {
let terminals = terminal::list_terminals().await;
respond(Ok::<_, String>(TerminalListResponse { terminals }))
}
@@ -1,4 +1,4 @@
//! Handler for x.ai/git/worktree/* extension methods.
//! Handler for kigi/git/worktree/* extension methods.
use agent_client_protocol as acp;
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
@@ -34,7 +34,7 @@ impl WorktreeNotificationSender for GatewayWorktreeNotifier {
return;
}
};
let notification = acp::ExtNotification::new("x.ai/git/worktree/status", params.into());
let notification = acp::ExtNotification::new("kigi/git/worktree/status", params.into());
if let Err(e) = self.gateway.send(notification).await {
tracing::warn!("Failed to send worktree progress notification: {}", e);
}
@@ -155,7 +155,7 @@ pub async fn handle(
let restore_code_default = agent.restore_code;
match args.method.as_ref() {
"x.ai/git/worktree/create" => {
"kigi/git/worktree/create" => {
let mut req = serde_json::from_str::<CreateWorktreeRequest>(args.params.get())?;
// Pre-dispatch: apply worktree_type default
let request_worktree_type = req.worktree_type;
@@ -163,7 +163,7 @@ pub async fn handle(
req.worktree_type = Some(worktree_type_default.into());
}
log_effective_worktree_type(
"x.ai/git/worktree/create",
"kigi/git/worktree/create",
request_worktree_type,
worktree_type_default,
req.worktree_type.unwrap_or(worktree_type_default.into()),
@@ -187,7 +187,7 @@ pub async fn handle(
}
to_response(Ok(result))
}
"x.ai/git/worktree/remove" => {
"kigi/git/worktree/remove" => {
let req = serde_json::from_str::<RemoveWorktreeRequest>(args.params.get())?;
let result = ops
.dispatch(&req, None)
@@ -195,7 +195,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/apply" => {
"kigi/git/worktree/apply" => {
let req = serde_json::from_str::<ApplyWorktreeRequest>(args.params.get())?;
let result = ops
.dispatch(&req, None)
@@ -204,7 +204,7 @@ pub async fn handle(
to_response(Ok(result))
}
// Create a worktree from an existing worktree (used during session fork)
"x.ai/git/worktree/create_from_worktree" => {
"kigi/git/worktree/create_from_worktree" => {
let mut req =
serde_json::from_str::<CreateWorktreeFromWorktreeRequest>(args.params.get())?;
let request_worktree_type = req.worktree_type;
@@ -213,7 +213,7 @@ pub async fn handle(
req.worktree_type = Some(worktree_type_default.into());
}
log_effective_worktree_type(
"x.ai/git/worktree/create_from_worktree",
"kigi/git/worktree/create_from_worktree",
request_worktree_type,
worktree_type_default,
req.worktree_type.unwrap_or(worktree_type_default.into()),
@@ -255,7 +255,7 @@ pub async fn handle(
to_response(Ok(response))
}
// Synchronous variant - waits for worktree creation to complete
"x.ai/git/worktree/create_from_worktree_sync" => {
"kigi/git/worktree/create_from_worktree_sync" => {
let mut req =
serde_json::from_str::<CreateWorktreeFromWorktreeRequest>(args.params.get())?;
@@ -293,7 +293,7 @@ pub async fn handle(
req.worktree_type = Some(worktree_type_default.into());
}
log_effective_worktree_type(
"x.ai/git/worktree/create_from_worktree_sync",
"kigi/git/worktree/create_from_worktree_sync",
request_worktree_type,
worktree_type_default,
req.worktree_type.unwrap_or(worktree_type_default.into()),
@@ -310,10 +310,10 @@ pub async fn handle(
to_response(Ok(result))
}
// Resume a session in a fresh worktree.
"x.ai/git/worktree/resume_session" => {
"kigi/git/worktree/resume_session" => {
let req = serde_json::from_str::<ResumeSessionInWorktreeRequest>(args.params.get())?;
log_effective_worktree_type(
"x.ai/git/worktree/resume_session",
"kigi/git/worktree/resume_session",
req.worktree_type,
worktree_type_default,
req.worktree_type.unwrap_or(worktree_type_default.into()),
@@ -335,7 +335,7 @@ pub async fn handle(
)
}
// ── Repo-wide session resolution ─────────────────────────────────
"x.ai/session/resolve_local_for_worktree_resume" => {
"kigi/session/resolve_local_for_worktree_resume" => {
let req =
serde_json::from_str::<ResolveLocalForWorktreeResumeRequest>(args.params.get())?;
let result = resolve_session_repo_wide(&req.session_id, std::path::Path::new(&req.cwd));
@@ -359,14 +359,14 @@ pub async fn handle(
}
}
// ── Session rehydration (devbox recovery) ─────────────────────────
"x.ai/session/rehydrate" => {
"kigi/session/rehydrate" => {
let req = serde_json::from_str::<RehydrateSessionRequest>(args.params.get())?;
let registry_client = agent.session_registry_client();
to_response(rehydrate_session_in_worktree(&req, ops, registry_client.as_ref()).await)
}
// ── Worktree management methods ──────────────────────────────────
"x.ai/git/worktree/list" => {
"kigi/git/worktree/list" => {
let req: kigi_workspace::workspace_ops::WorktreeListReq =
serde_json::from_str(args.params.get())
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
@@ -376,7 +376,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/show" => {
"kigi/git/worktree/show" => {
let req = serde_json::from_str::<ShowWorktreeRequest>(args.params.get())?;
let op = kigi_workspace::workspace_ops::WorktreeShowReq {
id_or_path: req.id_or_path,
@@ -387,7 +387,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/gc" => {
"kigi/git/worktree/gc" => {
let req = serde_json::from_str::<GcWorktreeRequest>(args.params.get())?;
let max_age_secs = req.max_age.as_deref().map(parse_duration).transpose()?;
let op = kigi_workspace::workspace_ops::WorktreeGcReq {
@@ -401,14 +401,14 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/db/stats" => {
"kigi/git/worktree/db/stats" => {
let result = ops
.dispatch(&kigi_workspace::workspace_ops::WorktreeDbStatsReq {}, None)
.await
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/db/rebuild" => {
"kigi/git/worktree/db/rebuild" => {
let result = ops
.dispatch(
&kigi_workspace::workspace_ops::WorktreeDbRebuildReq {},
@@ -418,7 +418,7 @@ pub async fn handle(
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
to_response(Ok(result))
}
"x.ai/git/worktree/db/path" => {
"kigi/git/worktree/db/path" => {
let result = ops
.dispatch(&kigi_workspace::workspace_ops::WorktreeDbPathReq {}, None)
.await
@@ -1,4 +1,4 @@
//! Vendor-compat resolution for `grok inspect`.
//! Vendor-compat resolution for `kigi inspect`.
//!
//! Resolves the local env/config/default stack into a diagnostic report.
+13 -13
View File
@@ -1,6 +1,6 @@
//! `grok inspect` — configuration introspection.
//! `kigi inspect` — configuration introspection.
//!
//! Shows everything Grok discovers in the current directory: project
//! Shows everything Kigi discovers in the current directory: project
//! instructions, permissions, hooks, skills, agents, plugins, MCP servers,
//! LSP config, and config.toml sources. Supports `--json` for machine output.
@@ -249,7 +249,7 @@ pub struct ConfigSources {
pub layers: Vec<ConfigLayer>,
}
/// A single config layer entry for `grok inspect`.
/// A single config layer entry for `kigi inspect`.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigLayer {
@@ -503,7 +503,7 @@ async fn list_instructions(cwd: &Path) -> Vec<InstructionFile> {
}
/// Calls the production permission resolver (`resolve_permissions_with_provenance`)
/// which handles both Grok TOML and vendor settings fallback in one codepath.
/// which handles both Kigi TOML and vendor settings fallback in one codepath.
async fn list_permissions(cwd: &Path) -> PermissionsReport {
use kigi_workspace::permission::resolution;
@@ -733,7 +733,7 @@ async fn list_skills(
/// stays non-bundled. Runtime discovery scopes/precedence are untouched.
///
/// `Bundled`/`Server` sources are constructed only here, never by runtime
/// discovery: deployed pagers parse `x.ai/skills/list` into a typed
/// discovery: deployed pagers parse `kigi/skills/list` into a typed
/// `ConfigSource` and reject unknown tags, so runtime stamping must wait
/// until clients without these variants have aged out. Until then this
/// mapping is the single owner of the scope→source translation.
@@ -924,7 +924,7 @@ fn list_lsp_servers(
// Folder-trust gate (display-only): inspect never spawns servers, but mark the
// repo-local (project-scoped) entries a session would skip in an untrusted
// clone so the listing matches the live gate. `remote = None` mirrors
// `grok mcp doctor` (no loaded RemoteSettings in a standalone command).
// `kigi mcp doctor` (no loaded RemoteSettings in a standalone command).
crate::agent::folder_trust::resolve_and_record(cwd, None, false);
let project_allowed = crate::agent::folder_trust::project_scope_allowed(cwd);
@@ -1359,7 +1359,7 @@ fn print_human(r: &InspectReport) {
if r.mcp_servers.is_empty() {
println!();
println!(" MCP Servers (0)");
println!(" {TREE} (none) \u{2014} see `grok mcp add --help`");
println!(" {TREE} (none) \u{2014} see `kigi mcp add --help`");
} else {
print_columns(
"MCP Servers",
@@ -1663,8 +1663,8 @@ mod tests {
fn model_override_warnings_inspect_smoke() {
let effective: toml::Value = toml::from_str(
r#"
[model."grok-4.5"]
model = "grok-4.5"
[model."kigi-4.5"]
model = "kigi-4.5"
env_key = "ANTHROPIC_AUTH_TOKEN"
compactions_remaining = 1
send_compactions_remaining = true
@@ -1686,16 +1686,16 @@ mod tests {
.any(|w| w.field.as_deref() == Some("reasoning_effort")),
"invalid enum should warn: {warnings:?}"
);
assert!(cfg.config_models.contains_key("grok-4.5"));
assert!(cfg.config_models.contains_key("kigi-4.5"));
let human = render_model_override_warnings(&warnings);
assert!(human.contains("Model Overrides"), "{human}");
assert!(
human.contains("[model.\"grok-4.5\"] send_compactions_remaining"),
human.contains("[model.\"kigi-4.5\"] send_compactions_remaining"),
"{human}"
);
assert!(
human.contains("[model.\"grok-4.5\"] reasoning_effort"),
human.contains("[model.\"kigi-4.5\"] reasoning_effort"),
"{human}"
);
assert_eq!(render_model_override_warnings(&[]), "");
@@ -1707,7 +1707,7 @@ mod tests {
.iter()
.find(|w| w["field"] == "send_compactions_remaining")
.expect("alias warning present in JSON");
assert_eq!(alias_warning["modelKey"], "grok-4.5");
assert_eq!(alias_warning["modelKey"], "kigi-4.5");
assert_eq!(alias_warning["kind"], "duplicate-alias");
assert!(
alias_warning["reason"]
+3 -3
View File
@@ -56,7 +56,7 @@ pub fn default_lock_path_in(root: &Path) -> PathBuf {
}
/// Effective leader lock path: the [`LEADER_SOCKET_ENV`] override's sibling
/// `.lock` when set, else the default under grok home.
/// `.lock` when set, else the default under kigi home.
pub fn default_lock_path() -> PathBuf {
resolve_lock_path(leader_socket_override(), &kigi_home())
}
@@ -67,7 +67,7 @@ pub fn default_socket_path_in(root: &Path) -> PathBuf {
}
/// Effective leader socket path: the [`LEADER_SOCKET_ENV`] override when set,
/// else the default under grok home.
/// else the default under kigi home.
pub fn default_socket_path() -> PathBuf {
resolve_socket_path(leader_socket_override(), &kigi_home())
}
@@ -131,7 +131,7 @@ pub struct LeaderLock {
}
impl LeaderLock {
/// Create a new LeaderLock using the default paths in grok home
/// Create a new LeaderLock using the default paths in kigi home
/// (or the [`LEADER_SOCKET_ENV`] override when set).
pub fn new() -> Self {
Self {
+22 -22
View File
@@ -1,4 +1,4 @@
//! Leader-follower IPC architecture for grok-shell.
//! Leader-follower IPC architecture for kigi-shell.
//!
//! This module implements a single-leader-per-machine architecture where one leader
//! process manages the agent state while multiple clients (TUI, IDE extensions, headless)
@@ -40,7 +40,7 @@
//! // Connect to existing leader or spawn a new one
//! let caps = ClientCapabilities {
//! yolo_mode: true,
//! default_model: Some("grok-3-fast".to_string()),
//! default_model: Some("kigi-3-fast".to_string()),
//! };
//! let conn = connect_or_spawn("my-client", ClientMode::Stdio, caps).await?;
//!
@@ -106,7 +106,7 @@ fn should_evict(leader_version: Option<&str>, client_version: &str) -> bool {
const RECONNECT_BASE_DELAY: Duration = Duration::from_secs(1);
/// Maximum delay between reconnection attempts (caps exponential backoff).
const RECONNECT_MAX_DELAY: Duration = Duration::from_secs(30);
/// Maximum reconnection attempts for bounded mode (headless/`grok -p`).
/// Maximum reconnection attempts for bounded mode (headless/`kigi -p`).
/// TUI mode uses unlimited retries controlled by a cancellation token.
const RECONNECT_MAX_ATTEMPTS_BOUNDED: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -228,7 +228,7 @@ fn build_live_leader_info(payload: ControlPayload) -> Result<LiveLeaderInfo, Lea
async fn fetch_live_leader_info(socket_path: &Path) -> Result<LiveLeaderInfo, LeaderTargetError> {
let client = LeaderClient::connect(
socket_path.to_path_buf(),
"grok-leader-discovery",
"kigi-leader-discovery",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -805,7 +805,7 @@ pub enum ReconnectPolicy {
/// Suitable for interactive TUI sessions where the user expects persistence.
Unbounded,
/// Retry up to a fixed number of attempts, then fail.
/// Suitable for headless/`grok -p` where hanging forever is unacceptable.
/// Suitable for headless/`kigi -p` where hanging forever is unacceptable.
Bounded { max_attempts: u32 },
}
impl ReconnectPolicy {
@@ -831,7 +831,7 @@ impl ReconnectPolicy {
/// ```ignore
/// let (status_tx, status_rx) = LeaderReconnector::status_channel();
/// let reconnector = LeaderReconnector::new(
/// "grok-tui", ClientMode::Stdio, caps, status_tx,
/// "kigi-tui", ClientMode::Stdio, caps, status_tx,
/// );
///
/// // When connection dies:
@@ -1112,7 +1112,7 @@ async fn evict_leader(conn: LeaderConnection, lock: &LeaderLock) {
///
/// # Arguments
///
/// * `client_type` - Identifier for the client type (e.g., "grok-tui", "vscode")
/// * `client_type` - Identifier for the client type (e.g., "kigi-tui", "vscode")
/// * `mode` - Communication mode (Stdio)
/// * `capabilities` - Client capabilities (e.g., yolo_mode) to register with the leader
pub async fn connect_or_spawn(
@@ -1248,8 +1248,8 @@ pub async fn connect_or_spawn(
/// Resolve the binary to spawn as the leader subprocess.
///
/// For a **managed install** — the running binary lives under `kigi_home`
/// (e.g. `~/.kigi/...`) — prefer the managed `~/.kigi/bin/grok` symlink. After an
/// auto-update or `grok update` atomically swaps that symlink, `current_exe()`
/// (e.g. `~/.kigi/...`) — prefer the managed `~/.kigi/bin/kigi` symlink. After an
/// auto-update or `kigi update` atomically swaps that symlink, `current_exe()`
/// still resolves (via `/proc/self/exe` on Linux) to the *old* versioned target,
/// so spawning it would relaunch the stale binary. The symlink always points to
/// the freshly-installed version. This mirrors
@@ -1259,23 +1259,23 @@ pub async fn connect_or_spawn(
/// not under `kigi_home`), keep `current_exe()` so the spawned leader matches the
/// calling binary.
///
/// Falls back to `~/.kigi/bin/grok` only when `current_exe()` is unavailable.
/// Falls back to `~/.kigi/bin/kigi` only when `current_exe()` is unavailable.
fn resolve_exe_for_spawn() -> Result<std::path::PathBuf, ConnectionError> {
resolve_binary_with_home(&crate::util::kigi_home::kigi_home())
}
fn resolve_binary_with_home(kigi_home: &Path) -> Result<std::path::PathBuf, ConnectionError> {
resolve_binary_impl(kigi_home, std::env::current_exe().ok())
}
/// Binary file name for the managed grok install (`grok` / `grok.exe`).
fn managed_grok_bin_name() -> &'static str {
if cfg!(windows) { "grok.exe" } else { "grok" }
/// Binary file name for the managed kigi install (`kigi` / `kigi.exe`).
fn managed_kigi_bin_name() -> &'static str {
if cfg!(windows) { "kigi.exe" } else { "kigi" }
}
/// Core leader-binary resolution with the current-exe path injected, for testability.
fn resolve_binary_impl(
kigi_home: &Path,
current_exe: Option<std::path::PathBuf>,
) -> Result<std::path::PathBuf, ConnectionError> {
let managed_bin = kigi_home.join("bin").join(managed_grok_bin_name());
let managed_bin = kigi_home.join("bin").join(managed_kigi_bin_name());
if let Some(ref exe) = current_exe
&& path_is_under(exe, kigi_home)
&& managed_bin.exists()
@@ -1992,7 +1992,7 @@ mod tests {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
std::fs::write(bin_dir.join("grok"), "fake-binary").unwrap();
std::fs::write(bin_dir.join("kigi"), "fake-binary").unwrap();
let result = resolve_binary_with_home(temp.path()).unwrap();
let current = std::env::current_exe().unwrap();
assert_eq!(result, current);
@@ -2009,9 +2009,9 @@ mod tests {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let target_v2 = bin_dir.join("grok-v2");
let target_v2 = bin_dir.join("kigi-v2");
std::fs::write(&target_v2, "new-binary").unwrap();
std::os::unix::fs::symlink(&target_v2, bin_dir.join("grok")).unwrap();
std::os::unix::fs::symlink(&target_v2, bin_dir.join("kigi")).unwrap();
let result = resolve_binary_with_home(temp.path()).unwrap();
let current = std::env::current_exe().unwrap();
assert_eq!(result, current);
@@ -2022,11 +2022,11 @@ mod tests {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let new_target = bin_dir.join("grok-v2");
let new_target = bin_dir.join("kigi-v2");
std::fs::write(&new_target, "new-binary").unwrap();
let managed = bin_dir.join("grok");
let managed = bin_dir.join("kigi");
std::os::unix::fs::symlink(&new_target, &managed).unwrap();
let stale_target = bin_dir.join("grok-v1");
let stale_target = bin_dir.join("kigi-v1");
std::fs::write(&stale_target, "old-binary").unwrap();
let result = resolve_binary_impl(temp.path(), Some(stale_target)).unwrap();
assert_eq!(result, managed);
@@ -2036,7 +2036,7 @@ mod tests {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
std::fs::write(bin_dir.join(managed_grok_bin_name()), "managed").unwrap();
std::fs::write(bin_dir.join(managed_kigi_bin_name()), "managed").unwrap();
let dev_exe = std::env::current_exe().unwrap();
let result = resolve_binary_impl(temp.path(), Some(dev_exe.clone())).unwrap();
assert_eq!(result, dev_exe);
@@ -2046,7 +2046,7 @@ mod tests {
let temp = TempDir::new().unwrap();
let bin_dir = temp.path().join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let managed = bin_dir.join(managed_grok_bin_name());
let managed = bin_dir.join(managed_kigi_bin_name());
std::fs::write(&managed, "managed").unwrap();
let result = resolve_binary_impl(temp.path(), None).unwrap();
assert_eq!(result, managed);
@@ -108,7 +108,7 @@ impl Default for ClientId {
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientMode {
/// Stdio mode (grok agent stdio, grok -p) - uses local IPC.
/// Stdio mode (kigi agent stdio, kigi -p) - uses local IPC.
/// Client sends/receives ACP messages directly via IPC.
Stdio,
}
@@ -143,7 +143,7 @@ pub struct ClientCapabilities {
#[serde(default)]
pub client_version: Option<String>,
/// Whether this client has advertised `x.ai/codeNavigation.enabled`.
/// Whether this client has advertised `kigi/codeNavigation.enabled`.
/// When true, the leader injects `codeNavEnabled: true` into `session/new`
/// and `session/load` requests so the agent can gate code-nav startup on a
/// per-client basis rather than reading from shared last-initialized state.
@@ -177,7 +177,7 @@ pub struct LeaderCapabilities {
pub profile_formats: Vec<ProfileArtifactFormat>,
/// Whether the leader supports [`ControlCommand::RelaunchForUpdate`] — a
/// disruptive, bounded-grace relaunch onto a freshly-installed binary
/// (driven by `grok update`). Old leaders default to `false`, so a new
/// (driven by `kigi update`). Old leaders default to `false`, so a new
/// client falls back to advising a manual restart (graceful degradation).
#[serde(default)]
pub relaunch_v1: bool,
@@ -196,12 +196,12 @@ pub enum ControlCommand {
},
StopCpuProfile,
/// Ask the leader to relaunch onto a freshly-installed binary (driven by
/// `grok update`). The leader stops admitting new turns, waits a bounded
/// `kigi update`). The leader stops admitting new turns, waits a bounded
/// grace period for in-flight turns to finish, flushes session state, then
/// exits with [`ShutdownReason::AutoUpdate`] so connected clients reconnect
/// onto the new binary and restore their sessions via `session/load`.
///
/// `to_version` is the version `grok update` just installed; the leader uses
/// `to_version` is the version `kigi update` just installed; the leader uses
/// it to decline if it is already running that version or newer.
RelaunchForUpdate {
to_version: String,
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
//! `tokio::net::UnixStream` / `UnixListener`. Zero wrapper, no unsafe.
//! - **Windows:** wraps `tokio::net::windows::named_pipe::*` (tokio doesn't
//! expose AF_UNIX on Windows). The leader's filesystem path is hashed
//! into `\\.\pipe\grok-leader-<hash>` so callers keep their path-based API.
//! into `\\.\pipe\kigi-leader-<hash>` so callers keep their path-based API.
//!
#[cfg(unix)]
pub use tokio::net::UnixListener as LeaderListener;
@@ -233,7 +233,7 @@ mod windows_impl {
name
}
/// Deterministic leaf name (`grok-leader-<hash>`) for a filesystem path.
/// Deterministic leaf name (`kigi-leader-<hash>`) for a filesystem path.
///
/// Uses SipHash-1-3 with fixed keys so the hash is stable across Rust
/// versions (unlike `DefaultHasher`, whose algorithm is unspecified).
@@ -245,7 +245,7 @@ mod windows_impl {
let mut hasher = SipHasher13::new_with_keys(0x67726f6b_6c656164, 0x65725f70_69706521);
path.hash(&mut hasher);
let hash = hasher.finish();
std::ffi::OsString::from(format!("grok-leader-{hash:016x}"))
std::ffi::OsString::from(format!("kigi-leader-{hash:016x}"))
}
#[cfg(test)]
@@ -255,8 +255,8 @@ mod windows_impl {
#[test]
fn pipe_name_is_deterministic() {
let a = path_to_pipe_name(Path::new("/tmp/grok.sock"));
let b = path_to_pipe_name(Path::new("/tmp/grok.sock"));
let a = path_to_pipe_name(Path::new("/tmp/kigi.sock"));
let b = path_to_pipe_name(Path::new("/tmp/kigi.sock"));
assert_eq!(a, b);
}
@@ -271,14 +271,14 @@ mod windows_impl {
fn pipe_name_has_correct_prefix() {
let name = path_to_pipe_name(Path::new("/tmp/test.sock"));
let s = name.to_string_lossy();
assert!(s.starts_with(r"\\.\pipe\grok-leader-"), "got: {s}");
assert!(s.starts_with(r"\\.\pipe\kigi-leader-"), "got: {s}");
}
#[test]
fn pipe_name_is_bounded() {
let long_path = "/".to_owned() + &"a".repeat(500);
let name = path_to_pipe_name(Path::new(&long_path));
// \\.\pipe\grok-leader- (20 chars) + 16 hex chars = 36 total
// \\.\pipe\kigi-leader- (20 chars) + 16 hex chars = 36 total
assert!(name.len() <= 256, "pipe name too long: {}", name.len());
}
@@ -287,7 +287,7 @@ mod windows_impl {
// Unique path per process so parallel test binaries don't collide on
// the derived pipe name.
let path =
std::env::temp_dir().join(format!("grok-ready-probe-{}.sock", std::process::id()));
std::env::temp_dir().join(format!("kigi-ready-probe-{}.sock", std::process::id()));
// Nothing bound yet -> ERROR_FILE_NOT_FOUND -> not ready.
assert!(!listener_is_ready(&path));
+14 -14
View File
@@ -116,7 +116,7 @@ fn try_lock_managed_config(home: &std::path::Path) -> Option<std::fs::File> {
/// Retry budget for a sync, pairing the attempt count with a wall-clock cap.
#[derive(Clone, Copy)]
enum SyncBudget {
/// Background loop and explicit `grok setup`; runs retries to completion.
/// Background loop and explicit `kigi setup`; runs retries to completion.
Standard,
/// Post-login sync; capped because login latency is user-visible.
Login,
@@ -343,7 +343,7 @@ pub(crate) fn spawn_sync(cancel: tokio_util::sync::CancellationToken) {
}
/// Deployment id reported for `deployment_key` on chat requests, credential
/// snapshots, and OTel: the **server** GrokBuildDeployment UUID (the id
/// snapshots, and OTel: the **server** KigiDeployment UUID (the id
/// server-side dashboards filter on) when the managed-config sync marker was
/// written by this same key (fingerprint match), else UUIDv5 of the key.
/// `None` key (team/OAuth) → `None`, never a stale marker value.
@@ -380,7 +380,7 @@ fn deployment_key_fingerprint(key: &str) -> String {
}
/// Whether managed config fetching is enabled (env > config.toml > default true).
/// Callers doing auto-fetch should check this; explicit user actions (grok setup) skip it.
/// Callers doing auto-fetch should check this; explicit user actions (kigi setup) skip it.
pub fn is_fetch_enabled() -> bool {
if let Some(v) = crate::agent::config::env_bool("KIGI_MANAGED_CONFIG") {
return v;
@@ -401,7 +401,7 @@ struct SyncOutcome {
wrote: bool,
/// The server returned non-empty config for the consulted principal — true
/// even when a concurrent writer held the lock and our write was skipped, so
/// `grok setup` doesn't misreport a lock skip as "no config".
/// `kigi setup` doesn't misreport a lock skip as "no config".
served: bool,
/// Which credential was consulted, so callers word team-vs-deployment
/// messages by what actually served, not just what's configured.
@@ -483,7 +483,7 @@ enum FetchedConfig {
/// Fetches the configuration for the current principal without touching disk:
/// the deployment key first, then a signed-in team. The installing sync and the
/// read-only `grok setup --json` both build on this.
/// read-only `kigi setup --json` both build on this.
async fn fetch_for_principal(budget: SyncBudget) -> Result<FetchedConfig, ManagedConfigError> {
let max_attempts = budget.max_attempts();
// Resolve from the merged config (managed_config_url override) so endpoint
@@ -690,7 +690,7 @@ pub async fn post_login_sync(_authenticated: Option<KimiAuth>) -> ManagedConfigS
}
}
/// Whether a credential exists that `grok setup` could install config for.
/// Whether a credential exists that `kigi setup` could install config for.
pub fn has_principal() -> bool {
resolve_deployment_key().is_some()
}
@@ -770,7 +770,7 @@ network access: reconnect and start again. If you can't reconnect, contact your
/// policy can't be established gets no unmanaged session. With no signing key it reads the user-writable
/// marker (a local user can disarm it by editing one field); non-forgeable enforcement is the trust-rooted
/// layers (root-owned path, MDM, signed cache). No client env disables it; recovery stays open (reconnect /
/// `grok setup`); ceasing to serve `fail_closed` rolls back.
/// `kigi setup`); ceasing to serve `fail_closed` rolls back.
pub fn managed_policy_gate() -> Result<(), String> {
// Skip under the lib unit-test build only: `bootstrap` reaches this without a staged
// `KIGI_SHARE_DIR` and would flake on the dev machine's real marker/auth. The pure decision
@@ -824,7 +824,7 @@ fn managed_policy_gate_decision(
Ok(())
}
/// Outcome of the `grok setup` sync. The caller renders it — CLI presentation
/// Outcome of the `kigi setup` sync. The caller renders it — CLI presentation
/// and exit codes stay out of the library.
#[derive(Debug)]
pub enum SetupOutcome {
@@ -836,9 +836,9 @@ pub enum SetupOutcome {
Failed(ManagedConfigError),
}
/// Result of `grok setup --json`: what the server serves for the current
/// Result of `kigi setup --json`: what the server serves for the current
/// principal, verbatim. `managed_config` may embed the enforced deployment key,
/// exactly as `grok setup` would write it to disk.
/// exactly as `kigi setup` would write it to disk.
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetupReport {
@@ -849,20 +849,20 @@ pub struct SetupReport {
pub configured: bool,
pub deployment_id: Option<String>,
pub team_id: Option<String>,
/// TOML documents exactly as `grok setup` would install them.
/// TOML documents exactly as `kigi setup` would install them.
pub managed_config: Option<String>,
pub requirements: Option<String>,
pub fail_closed: bool,
}
/// Fetches the report behind `grok setup --json` without writing anything:
/// Fetches the report behind `kigi setup --json` without writing anything:
/// no artifacts, no signature sidecar, no sync marker.
pub async fn fetch_setup_report() -> Result<SetupReport, ManagedConfigError> {
let (source, body) = match fetch_for_principal(SyncBudget::Standard).await? {
FetchedConfig::DeploymentKey { body, .. } => (Some("deploymentKey"), body),
FetchedConfig::NoPrincipal => (None, ManagedConfigResponse::default()),
};
// Match the installer's trust decision: a payload `grok setup` would refuse
// Match the installer's trust decision: a payload `kigi setup` would refuse
// is reported as an error, not printed as installable config.
if source.is_some()
&& kigi_config::signed_policy::verification_active()
@@ -882,7 +882,7 @@ pub async fn fetch_setup_report() -> Result<SetupReport, ManagedConfigError> {
})
}
/// Run the `grok setup` sync for the current principal. The caller must check
/// Run the `kigi setup` sync for the current principal. The caller must check
/// [`has_principal`] first and render the no-principal guidance.
pub async fn run_setup() -> SetupOutcome {
match sync_with_budget(SyncBudget::Standard).await {
+5 -5
View File
@@ -1,4 +1,4 @@
//! `grok mcp doctor` -- runtime health check for MCP servers.
//! `kigi mcp doctor` -- runtime health check for MCP servers.
use std::collections::HashMap;
use std::path::Path;
@@ -510,13 +510,13 @@ pub async fn run_doctor(cwd: &Path, name_filter: Option<&str>) -> DoctorReport {
let disabled_names = crate::util::config::disabled_mcp_server_names(cwd);
// Folder-trust gate: `grok mcp doctor` actually STARTS each server
// Folder-trust gate: `kigi mcp doctor` actually STARTS each server
// (`check_server_start`), so in an untrusted clone it would spawn the repo's
// project-scoped servers. Resolve the doctor cwd once (no prompt), then skip
// (do not start) any project-scoped server when untrusted. Reuses the same
// name primitive as the session/agent-pool gates.
//
// `remote = None` is intentional: standalone `grok mcp doctor` has no loaded
// `remote = None` is intentional: standalone `kigi mcp doctor` has no loaded
// `RemoteSettings`, so a remote-only org `folder_trust_enabled = false`
// opt-out isn't seen here — gating conservatively (treating the feature as
// enabled) is the deliberate fail-secure direction. Local env/user/managed
@@ -616,7 +616,7 @@ pub fn print_report(report: &DoctorReport) {
if report.servers.is_empty() {
println!(" No MCP servers configured.");
println!(" Run `grok mcp add --help` to get started.");
println!(" Run `kigi mcp add --help` to get started.");
println!();
return;
}
@@ -646,7 +646,7 @@ pub fn print_report(report: &DoctorReport) {
report.healthy_count,
report.failing_count,
if report.failing_count > 0 {
" Run `grok mcp doctor --json` for full diagnostics."
" Run `kigi mcp doctor --json` for full diagnostics."
} else {
""
}
+2 -2
View File
@@ -89,7 +89,7 @@ impl std::fmt::Display for UninstallError {
write!(
f,
"Plugin \"{name}\" not found.\n\
Run `grok plugin list` to see installed plugins."
Run `kigi plugin list` to see installed plugins."
)
}
Self::NeedsConfirm {
@@ -211,7 +211,7 @@ impl std::fmt::Display for UpdateError {
write!(
f,
"Plugin \"{name}\" not found.\n\
Run `grok plugin list` to see installed plugins."
Run `kigi plugin list` to see installed plugins."
)
}
}
@@ -2,7 +2,7 @@
//!
//! The canonical error types now live in `kigi_sampling_types::error`.
//! This module re-exports them and adds `map_sampling_err_to_acp` which
//! depends on `agent_client_protocol::Error` (a grok-shell dependency).
//! depends on `agent_client_protocol::Error` (a kigi-shell dependency).
// Re-export everything from the standalone crate.
pub use kigi_sampling_types::error::*;
@@ -72,7 +72,7 @@ pub fn map_sampling_err_to_acp(err: SamplingError) -> acp::Error {
// explanation visible to the user without triggering the client's
// re-auth flow on -32000.
StatusCode::FORBIDDEN => {
let message = if message.contains("requires a Grok subscription")
let message = if message.contains("requires a Kigi subscription")
&& crate::agent::auth_method::has_xai_api_key_env()
{
format!(
@@ -477,7 +477,7 @@ mod tests {
with_api_key_env(Some("xai-test"), || {
let err = SamplingError::Api {
status: StatusCode::FORBIDDEN,
message: "The model 'grok-build' requires a Grok subscription.".into(),
message: "The model 'kigi' requires a Kigi subscription.".into(),
model_metadata: None,
retry_after_secs: None,
};
@@ -501,7 +501,7 @@ mod tests {
with_api_key_env(None, || {
let err = SamplingError::Api {
status: StatusCode::FORBIDDEN,
message: "The model 'grok-build' requires a Grok subscription.".into(),
message: "The model 'kigi' requires a Kigi subscription.".into(),
model_metadata: None,
retry_after_secs: None,
};
@@ -759,7 +759,7 @@ mod tests {
let output = ToolOutput::Todo(TodoWriteOutput::TodosUpdated(TodoWriteSuccess {
summary_for_prompt: "tasks".to_string(),
todos: vec![],
state: kigi_tools::implementations::grok_build::todo::TodoState::default(),
state: kigi_tools::implementations::kigi::todo::TodoState::default(),
}));
let update = acp_tool_update(&output, "call-1", None, None).unwrap();
assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Completed));
@@ -768,7 +768,7 @@ mod tests {
#[test]
fn test_turn_end_plan_cleanup_preserves_semantics_and_priority() {
use crate::tools::todo::plan_entry_from_todo_item;
use kigi_tools::implementations::grok_build::todo::{TodoItem, TodoPriority, TodoStatus};
use kigi_tools::implementations::kigi::todo::{TodoItem, TodoPriority, TodoStatus};
// Simulate a mixed todo list at turn end.
let items = [
@@ -836,13 +836,13 @@ mod tests {
fn test_acp_plan_update_todo() {
let output = ToolOutput::Todo(TodoWriteOutput::TodosUpdated(TodoWriteSuccess {
summary_for_prompt: "tasks".to_string(),
todos: vec![kigi_tools::implementations::grok_build::todo::TodoItem {
todos: vec![kigi_tools::implementations::kigi::todo::TodoItem {
content: "Task 1".to_string(),
priority: kigi_tools::implementations::grok_build::todo::TodoPriority::Medium,
status: kigi_tools::implementations::grok_build::todo::TodoStatus::Completed,
priority: kigi_tools::implementations::kigi::todo::TodoPriority::Medium,
status: kigi_tools::implementations::kigi::todo::TodoStatus::Completed,
meta: None,
}],
state: kigi_tools::implementations::grok_build::todo::TodoState::default(),
state: kigi_tools::implementations::kigi::todo::TodoState::default(),
}));
let plan = acp_plan_update(&output).unwrap();
assert_eq!(plan.entries.len(), 1);
@@ -1,14 +1,14 @@
//! In-process SDK MCP servers over the ACP reverse channel (`x.ai/mcp/sdk_call`).
//! In-process SDK MCP servers over the ACP reverse channel (`kigi/mcp/sdk_call`).
//!
//! The official `grok-agent-sdk` lets a host define in-process tools (`@tool` /
//! The official `kigi-agent-sdk` lets a host define in-process tools (`@tool` /
//! `create_sdk_mcp_server`). When `transport="acp"`, the SDK registers them in
//! `session/new` `_meta["x.ai/mcp/servers"] = [{ "name", "serverId" }]` and the agent
//! `session/new` `_meta["kigi/mcp/servers"] = [{ "name", "serverId" }]` and the agent
//! invokes their tools by sending each MCP JSON-RPC message back to the client as a
//! reverse `x.ai/mcp/sdk_call` request — handled here by [`GatewayAcpInvoker`].
//! reverse `kigi/mcp/sdk_call` request — handled here by [`GatewayAcpInvoker`].
//!
//! NOTE: the *reverse* route (agent -> client, `x.ai/mcp/sdk_call`) invokes a tool that
//! NOTE: the *reverse* route (agent -> client, `kigi/mcp/sdk_call`) invokes a tool that
//! lives in the SDK's process. It is the zero-IPC mirror of the *forward* route (client
//! -> agent, `x.ai/mcp/call` in `extensions::mcp`), which invokes a tool on a server the
//! -> agent, `kigi/mcp/call` in `extensions::mcp`), which invokes a tool on a server the
//! AGENT is connected to. They use distinct method strings and sit on opposite request
//! handlers, so they never collide.
@@ -20,7 +20,7 @@ use kigi_mcp::acp_transport::AcpReverseInvoker;
use kigi_mcp::servers::AcpServerEntry;
use kigi_mcp::wire;
/// Parse `_meta["x.ai/mcp/servers"]` into [`AcpServerEntry`] registrations. Each entry
/// Parse `_meta["kigi/mcp/servers"]` into [`AcpServerEntry`] registrations. Each entry
/// is deserialized directly into the canonical type (so the `serverId` wire field is
/// serde-checked, not hand-read); entries missing `name`/`serverId` are skipped with a
/// warning. A name seen twice keeps the first (server names are the tool namespace, so a
@@ -38,12 +38,12 @@ pub fn parse_acp_mcp_servers(meta: Option<&acp::Meta>) -> Vec<AcpServerEntry> {
let server: AcpServerEntry = match serde_json::from_value(entry.clone()) {
Ok(server) => server,
Err(err) => {
tracing::warn!(entry = %entry, %err, "ignoring malformed x.ai/mcp/servers entry");
tracing::warn!(entry = %entry, %err, "ignoring malformed kigi/mcp/servers entry");
continue;
}
};
if !seen.insert(server.name.clone()) {
tracing::warn!(name = %server.name, "ignoring duplicate x.ai/mcp/servers entry");
tracing::warn!(name = %server.name, "ignoring duplicate kigi/mcp/servers entry");
continue;
}
servers.push(server);
@@ -53,7 +53,7 @@ pub fn parse_acp_mcp_servers(meta: Option<&acp::Meta>) -> Vec<AcpServerEntry> {
/// Reverse-RPC invoker for in-process SDK MCP servers.
///
/// Each [`invoke`](AcpReverseInvoker::invoke) sends one `x.ai/mcp/sdk_call` reverse request
/// Each [`invoke`](AcpReverseInvoker::invoke) sends one `kigi/mcp/sdk_call` reverse request
/// straight through the gateway. `AcpAgentGatewaySender::send` returns a `Send` future
/// (unlike the `?Send` `acp::Client::ext_method` trait method), so the rmcp transport's
/// `Send` invoker bound is satisfied with no relay task. Calls are independent and may
@@ -68,7 +68,7 @@ impl GatewayAcpInvoker {
}
}
/// Reverse `x.ai/mcp/sdk_call` params. Declares the on-wire field names once (mirrors
/// Reverse `kigi/mcp/sdk_call` params. Declares the on-wire field names once (mirrors
/// the forward side's typed `McpCallRequest`) so the `serverId` literal isn't hand-spelled.
#[derive(serde::Serialize)]
struct SdkCallParams<'a> {
@@ -111,7 +111,7 @@ mod tests {
#[test]
fn parses_valid_entries_and_skips_malformed() {
let meta = serde_json::json!({
"x.ai/mcp/servers": [
"kigi/mcp/servers": [
{ "name": "harness-tools", "serverId": "srv_0" },
{ "name": "missing-id" },
{ "serverId": "no_name" },
@@ -126,7 +126,7 @@ mod tests {
#[test]
fn duplicate_names_keep_the_first() {
let meta = serde_json::json!({
"x.ai/mcp/servers": [
"kigi/mcp/servers": [
{ "name": "tools", "serverId": "srv_0" },
{ "name": "tools", "serverId": "srv_1" },
]
@@ -61,7 +61,7 @@ use kigi_sampler::SamplerConfig as SamplingConfig;
use kigi_sampling_types::truncate_bytes;
use kigi_tools::computer::local::LocalTerminalBackend;
use kigi_tools::implementations::BashToolInput;
use kigi_tools::implementations::grok_build::web_fetch::WebFetchConfig;
use kigi_tools::implementations::kigi::web_fetch::WebFetchConfig;
use kigi_tools::types::ToolInput;
use kigi_tools::types::compat::CompatConfig;
use kigi_tools::types::output::{
@@ -178,7 +178,7 @@ mod spawn;
use super::acp_types::*;
pub use spawn::SessionThread;
pub(crate) use spawn::*;
/// Client-registered hook gates (the `x.ai/hooks/run` reverse request).
/// Client-registered hook gates (the `kigi/hooks/run` reverse request).
mod hooks;
pub(crate) struct InputItem {
pub(crate) prompt_id: String,
@@ -461,7 +461,7 @@ pub(crate) struct SessionActor {
/// Server-side doom-loop check policy, resolved once at spawn by
/// `Config::resolve_doom_loop_recovery`; `None` = disabled.
/// `reconstruct_full_config` threads it into the sampler config, and the
/// sampler itself sends the matching `x-grok-doom-loop-check` header.
/// sampler itself sends the matching `x-kigi-doom-loop-check` header.
pub(crate) doom_loop_recovery: Option<kigi_sampling_types::DoomLoopRecoveryPolicy>,
/// Telemetry-only per-turn doom-loop recovery tally (attempts, whether a
/// budget-spent accept happened, tightest trigger label). Accumulated by
@@ -524,10 +524,10 @@ pub(crate) struct SessionActor {
/// Wrapped in `RefCell` for mid-session mutation (skill refresh, prompt regen).
/// Safe: session actor is single-threaded (LocalSet), no concurrent access.
pub(crate) agent: std::cell::RefCell<kigi_agent::Agent>,
/// Dedup slot for `x.ai/git_head_changed`, shared with the fs-watch
/// Dedup slot for `kigi/git_head_changed`, shared with the fs-watch
/// `GitHead` consumer (see `git_head_dedup_key`).
pub(crate) last_reported_branch: Arc<parking_lot::Mutex<Option<String>>>,
/// Client opted into `x.ai/gitHeadChanged`. When false (headless/SDK),
/// Client opted into `kigi/gitHeadChanged`. When false (headless/SDK),
/// `maybe_notify_git_branch` no-ops — no git subprocess.
git_head_enabled: bool,
/// Shared models manager for etag-triggered refresh from response headers.
@@ -615,7 +615,7 @@ pub(crate) struct SessionActor {
pub(crate) goal_update_rx: std::cell::RefCell<
Option<
tokio::sync::mpsc::UnboundedReceiver<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalEnvelope,
kigi_tools::implementations::kigi::update_goal::UpdateGoalEnvelope,
>,
>,
>,
@@ -624,7 +624,7 @@ pub(crate) struct SessionActor {
/// empty ToolBridge. The `rx` half is owned by the drainer task (see
/// `goal_update_rx`).
pub(crate) goal_update_tx: tokio::sync::mpsc::UnboundedSender<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalEnvelope,
kigi_tools::implementations::kigi::update_goal::UpdateGoalEnvelope,
>,
/// Resolved master kill-switch for the verification stage (the
/// adversarial skeptic panel). `false` short-circuits
@@ -690,7 +690,7 @@ pub(crate) struct SessionActor {
/// time; only the input is parked here for the TurnEnd drain to
/// run through the verification stage.
pub(crate) pending_classifier_completions: parking_lot::Mutex<
VecDeque<kigi_tools::implementations::grok_build::update_goal::UpdateGoalInput>,
VecDeque<kigi_tools::implementations::kigi::update_goal::UpdateGoalInput>,
>,
/// Per-session re-entry guard for the verification stage. Set with
/// `compare_exchange(false, true)` at fire-entry and cleared on
@@ -742,7 +742,7 @@ pub(crate) struct SessionActor {
/// Wrapped in `RefCell` for mid-session reload from `&self` methods.
/// Safe: session actor is single-threaded (LocalSet), no concurrent access.
pub(crate) hook_registry: std::cell::RefCell<Option<Arc<kigi_hooks::discovery::HookRegistry>>>,
/// Client hooks from `session/new` `_meta["x.ai/hooks"]`; gated in
/// Client hooks from `session/new` `_meta["kigi/hooks"]`; gated in
/// [`crate::session::acp_session::hooks`]. `RefCell` so `load_session` reconnect can
/// replace the set on the live actor (see `SessionCommand::SetClientHooks`).
pub(crate) client_hooks: std::cell::RefCell<crate::extensions::hooks::ClientHooks>,
@@ -981,7 +981,7 @@ impl SessionActor {
memory_configured: self.memory.backend_params.is_some(),
scheduler: tool_names
.iter()
.any(|n| n == kigi_tools::implementations::grok_build::SCHEDULER_CREATE_TOOL_NAME),
.any(|n| n == kigi_tools::implementations::kigi::SCHEDULER_CREATE_TOOL_NAME),
hooks: self.hook_registry.borrow().is_some(),
plugins: self.plugin_registry.borrow().is_some(),
goal,
@@ -1039,7 +1039,7 @@ const PROMPT_CONTEXT_FILENAME: &str = "prompt_context.json";
/// Persist the structured prompt context to `{session_dir}/prompt_context.json`.
///
/// This is best-effort: failures are logged but do not block session creation.
/// The saved JSON enables deterministic re-rendering, `grok prompt --json`
/// The saved JSON enables deterministic re-rendering, `kigi prompt --json`
/// inspection, and post-hoc debugging of what went into a session's system prompt.
fn save_prompt_context(session_info: &SessionInfo, prompt_context: &kigi_agent::PromptContext) {
let dir = crate::session::persistence::session_dir(session_info);
@@ -1232,7 +1232,7 @@ mod turn_completion_emit_tests;
mod usage_categories_tests;
#[cfg(test)]
mod tool_meta_stamp_tests {
//! Pin the `x.ai/tool` stamps on the harness emission paths: the early
//! Pin the `kigi/tool` stamps on the harness emission paths: the early
//! ToolCall registered by `prepare_tool_call` and the permission-request
//! ToolCallUpdate (a dropped `stamp_tool_meta` call would regress silently).
use super::replay_buffer_send_update_tests::make_replay_send_update_fixture;
@@ -1252,7 +1252,7 @@ mod tool_meta_stamp_tests {
},
}
}
/// The `x.ai/tool` object from an event's `_meta`, if present.
/// The `kigi/tool` object from an event's `_meta`, if present.
fn tool_meta(meta: Option<&acp::Meta>) -> Option<&serde_json::Value> {
meta.and_then(|m| m.get(TOOL_META_KEY))
}
@@ -1263,10 +1263,8 @@ mod tool_meta_stamp_tests {
.run_until(async {
let mut fixture = make_replay_send_update_fixture().await;
fixture.actor.agent = std::cell::RefCell::new(
test_agent_with_tools(vec![ToolConfig::from_id(
"GrokBuild:read_file".to_string(),
)])
.await,
test_agent_with_tools(vec![ToolConfig::from_id("Kigi:read_file".to_string())])
.await,
);
let prepared = fixture
.actor
@@ -1289,13 +1287,13 @@ mod tool_meta_stamp_tests {
}
}
let early = early.expect("early ToolCall emitted");
let t = tool_meta(early.as_ref()).expect("early ToolCall carries x.ai/tool");
let t = tool_meta(early.as_ref()).expect("early ToolCall carries kigi/tool");
assert_eq!(t["name"], "read_file");
assert_eq!(t["kind"], "read");
assert_eq!(t["namespace"], "grok_build");
assert_eq!(t["namespace"], "kigi");
assert!(t.get("input").is_none(), "identity-only before parse");
let refined = refined.expect("refinement ToolCallUpdate emitted");
let t = tool_meta(refined.as_ref()).expect("refinement carries x.ai/tool");
let t = tool_meta(refined.as_ref()).expect("refinement carries kigi/tool");
assert_eq!(t["input"]["path"], "/tmp/stamp.txt");
})
.await;
@@ -1307,10 +1305,8 @@ mod tool_meta_stamp_tests {
.run_until(async {
let mut fixture = make_replay_send_update_fixture().await;
fixture.actor.agent = std::cell::RefCell::new(
test_agent_with_tools(vec![ToolConfig::from_id(
"GrokBuild:read_file".to_string(),
)])
.await,
test_agent_with_tools(vec![ToolConfig::from_id("Kigi:read_file".to_string())])
.await,
);
let (perm_tx, mut perm_rx) = mpsc::unbounded_channel();
fixture.actor.permissions = PermissionHandle::Actor {
@@ -1350,7 +1346,7 @@ mod tool_meta_stamp_tests {
.take()
.expect("permission request must have been issued");
let t = tool_meta(update.meta.as_ref())
.expect("permission-request ToolCallUpdate carries x.ai/tool");
.expect("permission-request ToolCallUpdate carries kigi/tool");
assert_eq!(t["name"], "read_file");
assert_eq!(t["kind"], "read");
assert_eq!(t["input"]["path"], "/tmp/stamp.txt");
@@ -1,11 +1,11 @@
//! Client-registered hooks for [`SessionActor`].
//!
//! Hooks registered at `session/new` (`_meta["x.ai/hooks"]`) come in two flavors,
//! Hooks registered at `session/new` (`_meta["kigi/hooks"]`) come in two flavors,
//! both matched by the agent ([`kigi_hooks::matcher::HookMatcher`], shared with
//! file hooks):
//! - **`PreToolUse` gate**: an awaited reverse *request* `x.ai/hooks/run`; a `deny`
//! - **`PreToolUse` gate**: an awaited reverse *request* `kigi/hooks/run`; a `deny`
//! blocks the tool.
//! - **All other events**: fire-and-forget *notifications* `x.ai/hooks/event`,
//! - **All other events**: fire-and-forget *notifications* `kigi/hooks/event`,
//! observe-only (the callback's return is ignored). Sent per matching callback.
use std::sync::Arc;
@@ -23,10 +23,10 @@ use crate::extensions::hooks::{
};
use crate::sampling::types::ToolCallResponse;
const HOOK_EVENT_METHOD: &str = "x.ai/hooks/event";
const HOOK_RUN_METHOD: &str = "x.ai/hooks/run";
const HOOK_EVENT_METHOD: &str = "kigi/hooks/event";
const HOOK_RUN_METHOD: &str = "kigi/hooks/run";
/// Default per-callback bound for a client's `x.ai/hooks/run` reply; on timeout the gate
/// Default per-callback bound for a client's `kigi/hooks/run` reply; on timeout the gate
/// fails open (the tool proceeds).
///
/// Some external hosts default to 600s per hook; we default to 30s because our gate sits
@@ -47,7 +47,7 @@ enum ClientHookGateOutcome {
UnknownDecision,
}
/// Outcome of the `x.ai/hooks/run` reverse request, before interpreting it as a
/// Outcome of the `kigi/hooks/run` reverse request, before interpreting it as a
/// decision. Separate so [`classify`] stays pure and unit-testable.
enum ReverseOutcome {
Responded(Arc<RawValue>),
@@ -68,7 +68,7 @@ fn classify(outcome: ReverseOutcome) -> (ClientHookResponse, ClientHookGateOutco
ClientHookDecision::Deny => ClientHookGateOutcome::Denied,
ClientHookDecision::Other => {
tracing::warn!(
"x.ai/hooks/run returned an unknown decision value; failing open"
"kigi/hooks/run returned an unknown decision value; failing open"
);
ClientHookGateOutcome::UnknownDecision
}
@@ -77,7 +77,7 @@ fn classify(outcome: ReverseOutcome) -> (ClientHookResponse, ClientHookGateOutco
(resp, label)
}
Err(err) => {
tracing::warn!(%err, "malformed x.ai/hooks/run response; failing open");
tracing::warn!(%err, "malformed kigi/hooks/run response; failing open");
(
ClientHookResponse::default(),
ClientHookGateOutcome::Malformed,
@@ -86,14 +86,14 @@ fn classify(outcome: ReverseOutcome) -> (ClientHookResponse, ClientHookGateOutco
}
}
ReverseOutcome::Transport(err) => {
tracing::warn!(%err, "x.ai/hooks/run transport error (no client wired?); failing open");
tracing::warn!(%err, "kigi/hooks/run transport error (no client wired?); failing open");
(
ClientHookResponse::default(),
ClientHookGateOutcome::TransportError,
)
}
ReverseOutcome::Timeout => {
tracing::warn!("x.ai/hooks/run timed out; failing open");
tracing::warn!("kigi/hooks/run timed out; failing open");
(
ClientHookResponse::default(),
ClientHookGateOutcome::TimedOut,
@@ -206,7 +206,7 @@ impl SessionActor {
}
/// Run the client-registered `PreToolUse` hooks for `call`, firing
/// `x.ai/hooks/run` once per matching callback with the shared `envelope` (the
/// `kigi/hooks/run` once per matching callback with the shared `envelope` (the
/// same payload file hooks and observe events receive).
///
/// Returns `Some(ToolLoop::HookDenied)` on the first deny, else `None`.
@@ -285,7 +285,7 @@ impl SessionActor {
Ok(None)
}
/// Issue one `x.ai/hooks/run` reverse request, bounded by a per-callback `timeout`.
/// Issue one `kigi/hooks/run` reverse request, bounded by a per-callback `timeout`.
async fn send_hook_run(
&self,
dispatch: &ClientHookDispatch<'_>,
@@ -305,7 +305,7 @@ impl SessionActor {
}
/// Fire observe-only client hooks for `envelope`'s event: send an
/// `x.ai/hooks/event` notification to each matching registered callback.
/// `kigi/hooks/event` notification to each matching registered callback.
/// Fire-and-forget (no decision is consumed); independent of file hooks, so it
/// runs even when no on-disk hook registry exists. No-op when nothing is registered.
pub(super) fn notify_client_hooks(&self, envelope: &HookEventEnvelope) {
@@ -22,7 +22,7 @@ impl RoleCapability {
/// `can_execute` for terminal/bash).
fn is_satisfied(
self,
summary: &kigi_tools::implementations::grok_build::task::types::SubagentTypeSummary,
summary: &kigi_tools::implementations::kigi::task::types::SubagentTypeSummary,
) -> bool {
match self {
Self::Skeptic => summary.can_read && summary.can_search,
@@ -42,7 +42,7 @@ pub(crate) struct PanelResolveCache {
/// result for the role's `general-purpose` toolset on that harness).
describe: std::collections::HashMap<
String,
kigi_tools::implementations::grok_build::task::types::SubagentDescribeOutcome,
kigi_tools::implementations::kigi::task::types::SubagentDescribeOutcome,
>,
}
@@ -57,7 +57,7 @@ fn role_tool_names_from(
cache: &PanelResolveCache,
inherit: &crate::session::goal_role_tools::RoleToolNames,
) -> crate::session::goal_role_tools::RoleToolNames {
use kigi_tools::implementations::grok_build::task::types::SubagentDescribeOutcome;
use kigi_tools::implementations::kigi::task::types::SubagentDescribeOutcome;
// `override_.agent_type` is the committed harness; the cache is keyed on it.
match override_.agent_type.as_deref() {
Some(harness) => match cache.describe.get(harness) {
@@ -115,9 +115,9 @@ impl SessionActor {
&self,
current_tokens: i64,
purpose: DrainPurpose,
extra: Vec<kigi_tools::implementations::grok_build::update_goal::UpdateGoalEnvelope>,
extra: Vec<kigi_tools::implementations::kigi::update_goal::UpdateGoalEnvelope>,
) {
use kigi_tools::implementations::grok_build::update_goal::{RejectReason, UpdateGoalAck};
use kigi_tools::implementations::kigi::update_goal::{RejectReason, UpdateGoalAck};
// The `update_goal` tool and its `GoalUpdateHandle` are always
// registered (see `spawn_session_actor`), so a model can call
// `update_goal` in a session that never entered goal mode — e.g. any
@@ -650,10 +650,10 @@ impl SessionActor {
attempt: u32,
outcome: crate::session::goal_classifier::GoalClassifierOutcome,
notify: &crate::session::goal_orchestrator::GoalNotifySender,
) -> kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck {
) -> kigi_tools::implementations::kigi::update_goal::UpdateGoalAck {
use crate::session::goal_classifier::GoalClassifierOutcome;
use crate::session::goal_tracker::GoalClassifierVerdict;
use kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck;
use kigi_tools::implementations::kigi::update_goal::UpdateGoalAck;
let current_tokens = self.chat_state_handle.get_total_tokens().await as i64;
let (tokens_used, finished_marginal) = self.goal_tokens(current_tokens);
@@ -1299,7 +1299,7 @@ impl SessionActor {
choice: &crate::agent::config::GoalRoleModelChoice,
capability: RoleCapability,
event_tx: &tokio::sync::mpsc::UnboundedSender<
kigi_tools::implementations::grok_build::task::types::SubagentEvent,
kigi_tools::implementations::kigi::task::types::SubagentEvent,
>,
) -> (
crate::session::goal_planner::RoleSpawnOverride,
@@ -1372,17 +1372,15 @@ impl SessionActor {
pair: &crate::util::config::GoalRoleModel,
capability: RoleCapability,
event_tx: &tokio::sync::mpsc::UnboundedSender<
kigi_tools::implementations::grok_build::task::types::SubagentEvent,
kigi_tools::implementations::kigi::task::types::SubagentEvent,
>,
available_models: &indexmap::IndexMap<String, crate::agent::config::ModelEntry>,
cache: &mut PanelResolveCache,
) -> crate::session::goal_planner::RoleSpawnOverride {
use crate::session::events::{Event, GoalRoleModelFailOpenReason as Reason};
use crate::session::goal_planner::RoleSpawnOverride;
use kigi_tools::implementations::grok_build::task::backend::{
ChannelBackend, SubagentBackend,
};
use kigi_tools::implementations::grok_build::task::types::SubagentDescribeOutcome;
use kigi_tools::implementations::kigi::task::backend::{ChannelBackend, SubagentBackend};
use kigi_tools::implementations::kigi::task::types::SubagentDescribeOutcome;
let fail_open = |reason: Reason| {
self.emit_event(Event::GoalRoleModelFailOpen {
@@ -1408,7 +1406,7 @@ impl SessionActor {
}
// 2b. Reject a STRICT harness whose flavor isn't representable (e.g.
// `codex`): it resolves, but `resolve_subagent_toolset` would
// silently run grok-build flavor. Non-strict names (grok-build
// silently run kigi flavor. Non-strict names (kigi
// family) run the default flavor and pass; unresolvable names fall
// through to the describe `Unknown` arm below.
if kigi_agent::config::is_strict_harness_agent_type(&pair.agent_type)
@@ -1511,9 +1509,9 @@ impl SessionActor {
/// toolset; [`RoleToolNames::from_parent`] applies the literal fallback for
/// any kind the bridge lacks, and resolves `{WRITE_TOOL}` from the parent
/// `Edit` tool when the bridge has no `Write` (so the inherit / retry render
/// agrees with `from_summary` — `search_replace` on the default grok-build
/// agrees with `from_summary` — `search_replace` on the default kigi
/// host, not the literal `write`). The `{TOOLSET_TOOLS}` block is empty on
/// this path (no per-role toolset enumeration); on the default grok-build
/// this path (no per-role toolset enumeration); on the default kigi
/// host the bridge resolves the parent's real tool ids (`read_file`, …).
pub(crate) async fn resolve_inherit_role_tool_names(
&self,
@@ -2340,7 +2338,7 @@ impl SessionActor {
#[cfg(test)]
mod role_capability_tests {
use super::RoleCapability;
use kigi_tools::implementations::grok_build::task::types::SubagentTypeSummary;
use kigi_tools::implementations::kigi::task::types::SubagentTypeSummary;
fn summary(can_read: bool, can_search: bool, can_execute: bool) -> SubagentTypeSummary {
SubagentTypeSummary {
@@ -2371,7 +2369,7 @@ mod role_tool_names_tests {
use super::{PanelResolveCache, role_tool_names_from};
use crate::session::goal_planner::RoleSpawnOverride;
use crate::session::goal_role_tools::RoleToolNames;
use kigi_tools::implementations::grok_build::task::types::{
use kigi_tools::implementations::kigi::task::types::{
SubagentDescribeOutcome, SubagentTypeSummary,
};
use kigi_tools::types::tool::ToolKind;
@@ -15,10 +15,10 @@ impl DrainSource {
pub(super) fn into_parts(
self,
) -> (
kigi_tools::implementations::grok_build::update_goal::UpdateGoalInput,
kigi_tools::implementations::kigi::update_goal::UpdateGoalInput,
Option<
tokio::sync::oneshot::Sender<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck,
kigi_tools::implementations::kigi::update_goal::UpdateGoalAck,
>,
>,
) {
@@ -33,11 +33,9 @@ impl DrainSource {
/// source). No-op for `Pending` source — its ack was already resolved.
pub(super) fn try_send_ack(
ack_tx: Option<
tokio::sync::oneshot::Sender<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck,
>,
tokio::sync::oneshot::Sender<kigi_tools::implementations::kigi::update_goal::UpdateGoalAck>,
>,
ack: kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck,
ack: kigi_tools::implementations::kigi::update_goal::UpdateGoalAck,
) {
if let Some(tx) = ack_tx {
send_ack(tx, ack);
@@ -154,9 +152,9 @@ impl<F: FnOnce(&mut crate::session::goal_tracker::GoalTracker)> Drop for Tracker
/// the receiver was dropped (benign — tool future aborted).
pub(super) fn send_ack(
ack_tx: tokio::sync::oneshot::Sender<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck,
kigi_tools::implementations::kigi::update_goal::UpdateGoalAck,
>,
ack: kigi_tools::implementations::grok_build::update_goal::UpdateGoalAck,
ack: kigi_tools::implementations::kigi::update_goal::UpdateGoalAck,
) {
if ack_tx.send(ack).is_err() {
tracing::debug!("update_goal ack receiver dropped before harness could respond");
@@ -702,14 +700,14 @@ mod fold_tokens_by_model_tests {
#[test]
fn mixed_models_sum_marginals_sorted_desc() {
let records = vec![
rec(Some("g1"), 0, 100, Some("grok-3")),
rec(Some("g1"), 100, 500, Some("grok-4")), // marginal 400
rec(Some("g1"), 0, 50, Some("grok-3")), // grok-3 total 150
rec(Some("g1"), 0, 100, Some("kigi-3")),
rec(Some("g1"), 100, 500, Some("kigi-4")), // marginal 400
rec(Some("g1"), 0, 50, Some("kigi-3")), // kigi-3 total 150
];
let out = fold_tokens_by_model(&records, "g1", "cur");
assert_eq!(
out,
vec![("grok-4".to_owned(), 400), ("grok-3".to_owned(), 150)]
vec![("kigi-4".to_owned(), 400), ("kigi-3".to_owned(), 150)]
);
}
@@ -736,38 +734,38 @@ mod fold_tokens_by_model_tests {
#[test]
fn single_distinct_model_collapses_to_one_entry() {
let records = vec![
rec(Some("g1"), 0, 100, Some("grok-4")),
rec(Some("g1"), 0, 200, None), // folds under current = grok-4
rec(Some("g1"), 0, 100, Some("kigi-4")),
rec(Some("g1"), 0, 200, None), // folds under current = kigi-4
];
let out = fold_tokens_by_model(&records, "g1", "grok-4");
assert_eq!(out, vec![("grok-4".to_owned(), 300)]);
let out = fold_tokens_by_model(&records, "g1", "kigi-4");
assert_eq!(out, vec![("kigi-4".to_owned(), 300)]);
}
#[test]
fn other_goal_records_excluded() {
let records = vec![
rec(Some("g1"), 0, 100, Some("grok-4")),
rec(Some("g2"), 0, 999, Some("grok-4")),
rec(None, 0, 999, Some("grok-4")),
rec(Some("g1"), 0, 100, Some("kigi-4")),
rec(Some("g2"), 0, 999, Some("kigi-4")),
rec(None, 0, 999, Some("kigi-4")),
];
let out = fold_tokens_by_model(&records, "g1", "cur");
assert_eq!(out, vec![("grok-4".to_owned(), 100)]);
assert_eq!(out, vec![("kigi-4".to_owned(), 100)]);
}
#[test]
fn last_below_anchor_does_not_underflow() {
let records = vec![rec(Some("g1"), 500, 100, Some("grok-4"))];
let records = vec![rec(Some("g1"), 500, 100, Some("kigi-4"))];
// marginal saturates to 0 -> skipped as a zero-token entry.
assert!(fold_tokens_by_model(&records, "g1", "cur").is_empty());
}
#[test]
fn captured_model_survives_mid_goal_current_model_switch() {
// A record captured `grok-4` at spawn keeps it even though the
// current model at aggregation time is `grok-3`.
let records = vec![rec(Some("g1"), 0, 100, Some("grok-4"))];
let out = fold_tokens_by_model(&records, "g1", "grok-3");
assert_eq!(out, vec![("grok-4".to_owned(), 100)]);
// A record captured `kigi-4` at spawn keeps it even though the
// current model at aggregation time is `kigi-3`.
let records = vec![rec(Some("g1"), 0, 100, Some("kigi-4"))];
let out = fold_tokens_by_model(&records, "g1", "kigi-3");
assert_eq!(out, vec![("kigi-4".to_owned(), 100)]);
}
#[test]
@@ -788,11 +786,11 @@ mod fold_tokens_by_model_tests {
// An empty id folds into the SAME bucket as records that
// explicitly captured the current model id.
let records = vec![
rec(Some("g1"), 0, 100, Some("grok-4")),
rec(Some("g1"), 0, 100, Some("kigi-4")),
rec(Some("g1"), 0, 200, Some("")),
];
let out = fold_tokens_by_model(&records, "g1", "grok-4");
assert_eq!(out, vec![("grok-4".to_owned(), 300)]);
let out = fold_tokens_by_model(&records, "g1", "kigi-4");
assert_eq!(out, vec![("kigi-4".to_owned(), 300)]);
}
}
@@ -822,7 +820,7 @@ pub(crate) fn planner_failure_pause_message() -> String {
}
pub(crate) fn goal_slash_and_harness_available(goal_enabled: bool, tool_names: &[String]) -> bool {
use kigi_tools::implementations::grok_build::UPDATE_GOAL_TOOL_NAME;
use kigi_tools::implementations::kigi::UPDATE_GOAL_TOOL_NAME;
goal_enabled && tool_names.iter().any(|n| n == UPDATE_GOAL_TOOL_NAME)
}
@@ -1468,9 +1466,7 @@ impl SessionActor {
self.agent
.borrow()
.tool_bridge()
.update_resource(
kigi_tools::implementations::grok_build::task::types::GoalLoopActive(active),
)
.update_resource(kigi_tools::implementations::kigi::task::types::GoalLoopActive(active))
.await;
}
}
@@ -22,7 +22,7 @@ pub(crate) type PendingInterjection =
/// converted into standalone prompt turns (arrived while idle, or after the
/// running turn's final drain). The prefix keeps the turn's user echo
/// persist-only: every pane already rendered the text from the
/// `x.ai/session/interjection` broadcast, so a live echo would duplicate it.
/// `kigi/session/interjection` broadcast, so a live echo would duplicate it.
pub(crate) const INTERJECT_FALLBACK_PROMPT_PREFIX: &str = "interject-fallback-";
impl SessionActor {
@@ -164,7 +164,7 @@ impl SessionActor {
self.notifications
.gateway
.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/session/interjection",
"kigi/session/interjection",
params.into(),
));
}
@@ -323,7 +323,7 @@ impl SessionActor {
/// the sampler call goes through `prepare_chat_completion().conversation_collect()`
/// — a side-channel direct-HTTP call that does NOT publish events
/// on the per-session shared sampler channel. The client never
/// sees "Grok is thinking", streaming token chunks, or any other
/// sees "Kigi is thinking", streaming token chunks, or any other
/// session update from a classifier fire. Stalled verdicts only
/// queue a `<system-reminder>` into chat state via
/// `push_system_reminder`; no `InputItem` is pushed into
@@ -549,18 +549,18 @@ impl SessionActor {
model: Some(model_id.clone()),
temperature: Some(0.0),
max_output_tokens: Some(LAZINESS_MAX_OUTPUT_TOKENS),
// Don't pass `reasoning_effort` — `grok-4.5` (and
// other tool-flavoured Grok variants) reject the field at
// Don't pass `reasoning_effort` — `kigi-4.5` (and
// other tool-flavoured Kigi variants) reject the field at
// the proxy with `400 Bad Request: Model does not support
// parameter reasoningEffort`. Omitting it lets each model
// apply its own default. The classifier task is one short
// JSON object — even on reasoning-capable models the
// default suffices; no need to force it off.
reasoning_effort: None,
x_grok_conv_id: Some(session_id_str.clone()),
x_grok_req_id: Some(format!("{LAZINESS_REQ_ID_PREFIX}{}", uuid::Uuid::new_v4())),
x_grok_session_id: Some(session_id_str),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(session_id_str.clone()),
x_kigi_req_id: Some(format!("{LAZINESS_REQ_ID_PREFIX}{}", uuid::Uuid::new_v4())),
x_kigi_session_id: Some(session_id_str),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..ConversationRequest::default()
};
@@ -110,7 +110,7 @@ impl LazinessAbortReason {
/// Prompt-structure mitigations against motivated reasoning:
/// "Do not roleplay", JSON-only, no chain-of-thought,
/// no role context, transcript framed as third-party data.
/// Prefix on `x_grok_req_id` for laziness-classifier sampler calls.
/// Prefix on `x_kigi_req_id` for laziness-classifier sampler calls.
/// Centralised here so the production producer
/// (`maybe_fire_laziness_check`) AND the offline replay harness
/// (`crate::trace_classifier::build_classifier_request`) share a
@@ -150,7 +150,7 @@ impl SessionActor {
);
}
}
/// Emit per-server `x.ai/mcp/tools_changed` notifications.
/// Emit per-server `kigi/mcp/tools_changed` notifications.
///
/// Each emission carries the owning
/// `sessionId` so the pager can route via `find_session_match`
@@ -182,7 +182,7 @@ impl SessionActor {
}
}
}
/// Handle explicit auth trigger from the client (x.ai/mcp/auth_trigger).
/// Handle explicit auth trigger from the client (kigi/mcp/auth_trigger).
///
/// Runs force_reauth (browser flow), then re-initializes the server
/// and registers its tools.
@@ -580,7 +580,7 @@ impl SessionActor {
/// On success: the new `Arc<McpClient>` is in
/// `mcp_state.owned_clients[server]` with `ClientState::Ready`,
/// the dispatcher's `notify_tx` is wired to its
/// `GrokClientHandler`, and the liveness watcher is armed —
/// `KigiClientHandler`, and the liveness watcher is armed —
/// matching the post-handshake state produced by
/// [`Self::ensure_mcp_tools_initialized`] for a fresh server.
///
@@ -603,7 +603,7 @@ impl SessionActor {
/// `Reason::Initialized` from the dispatcher's mapping, one
/// `Reason::RestartSucceeded` from the restart task).
///
/// The `GrokClientHandler` constructed inside `try_handshake`
/// The `KigiClientHandler` constructed inside `try_handshake`
/// holds the SHARED `Arc<Mutex<Option<Sender>>>` slot
/// (`SharedEventTx`), so wiring the sender AFTER the handshake
/// still routes subsequent `tools/list_changed` /
@@ -814,7 +814,7 @@ impl SessionActor {
self.notifications
.gateway
.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/mcp_initialized",
"kigi/mcp_initialized",
params.into(),
));
}
@@ -884,7 +884,7 @@ impl SessionActor {
self.notifications
.gateway
.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/mcp_initialized",
"kigi/mcp_initialized",
params.into(),
));
}
@@ -1432,7 +1432,7 @@ impl SessionActor {
"elapsedMs" : elapsed.as_millis() as u64, }
)) {
gateway.forward_fire_and_forget(acp::ExtNotification::new(
"x.ai/mcp_initialized",
"kigi/mcp_initialized",
params.into(),
));
}
@@ -314,10 +314,10 @@ impl SessionActor {
ConversationItem::user(user_message),
],
model: Some(model),
x_grok_conv_id: Some(session_id.clone()),
x_grok_req_id: Some(format!("xai-dream-{}", uuid::Uuid::new_v4())),
x_grok_session_id: Some(session_id),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(session_id.clone()),
x_kigi_req_id: Some(format!("xai-dream-{}", uuid::Uuid::new_v4())),
x_kigi_session_id: Some(session_id),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..Default::default()
};
let response = sampling_client
@@ -413,10 +413,10 @@ impl SessionActor {
let request = ConversationRequest {
items,
model: Some(model),
x_grok_conv_id: Some(session_id.clone()),
x_grok_req_id: Some(format!("xai-flush-{}", uuid::Uuid::new_v4())),
x_grok_session_id: Some(session_id.clone()),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(session_id.clone()),
x_kigi_req_id: Some(format!("xai-flush-{}", uuid::Uuid::new_v4())),
x_kigi_session_id: Some(session_id.clone()),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..Default::default()
};
@@ -608,7 +608,7 @@ impl SessionActor {
}
/// Rewrite a raw memory note into well-structured markdown via a one-shot
/// LLM call using the `grok-build` model.
/// LLM call using the `kigi` model.
///
/// Follows the same streaming pattern as [`handle_ai_suggest`]: prepares
/// a sampling client, builds a system+user prompt, streams the response,
@@ -207,7 +207,7 @@ impl SessionActor {
}
bridge
.update_resource(
kigi_tools::implementations::grok_build::update_goal::GoalUpdateHandle(
kigi_tools::implementations::kigi::update_goal::GoalUpdateHandle(
self.goal_update_tx.clone(),
),
)
@@ -94,7 +94,7 @@ impl SessionActor {
.borrow()
.tool_bridge()
.update_resource(
kigi_tools::implementations::grok_build::task::types::CurrentPromptIdResource(
kigi_tools::implementations::kigi::task::types::CurrentPromptIdResource(
prompt_id.clone(),
),
)
@@ -249,7 +249,7 @@ impl SessionActor {
let Some(buffer) = &self.tool_context.monitor_event_buffer else {
return;
};
for event in kigi_tools::implementations::grok_build::task::types::drain_owned(
for event in kigi_tools::implementations::kigi::task::types::drain_owned(
buffer,
Some(self.session_info.id.0.as_ref()),
) {
@@ -308,7 +308,7 @@ impl SessionActor {
notifications: Vec<PendingNotification>,
task_output_tool_name: &str,
) -> bool {
use kigi_tools::implementations::grok_build::task::types::MonitorEventNotification;
use kigi_tools::implementations::kigi::task::types::MonitorEventNotification;
// Collapse monitor entries: collect their text into events, remember
// where the first one sat so the batch lands in arrival position.
@@ -277,7 +277,7 @@ pub(super) fn build_truncated_prompt_message(
}
/// Replace the file-referencing offload `notice` embedded in `message` with the
/// no-file [`OFFLOAD_FAILED_NOTICE`]. Position-independent (the notice sits at the
/// end for grok ordering), so a failed offload never
/// end for kigi ordering), so a failed offload never
/// leaves the model chasing a "read this file" pointer to a file that does not
/// exist. Returns `message` unchanged if the notice is absent (defensive).
pub(super) fn strip_offload_notice(message: &str, notice: &str) -> String {
@@ -319,7 +319,7 @@ impl SessionActor {
entry_count = payload.entries.len(),
entries = ?payload.entries.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(),
session = self.session_info.id.0.as_ref(),
"broadcasting x.ai/queue/changed to subscribers",
"broadcasting kigi/queue/changed to subscribers",
);
if let Ok(params) = serde_json::value::to_raw_value(&payload) {
self.notifications
@@ -426,7 +426,7 @@ impl SessionActor {
/// stale `expected_version`, or is owned by another client, or
/// - the row is not a plain prompt (it would reach the model as prompt text).
///
/// Always re-broadcasts `x.ai/queue/changed` so every client reconciles
/// Always re-broadcasts `kigi/queue/changed` so every client reconciles
/// (the row vanishes on success, is unchanged on a no-op).
/// `new_text` (when `Some`) replaces the stored queue text in the
/// interjection — the client edited the row before interjecting. It rides
@@ -609,7 +609,7 @@ impl SessionActor {
/// user has explicitly typed replacement text).
/// 2. Update `queue_meta.text`, bump `queue_meta.version`, and record
/// `last_editor` (the original `owner` attribution is preserved).
/// 3. Re-broadcast `x.ai/queue/changed` so every subscriber renders the
/// 3. Re-broadcast `kigi/queue/changed` so every subscriber renders the
/// new text and version.
///
/// **No-op cases** (each is a benign discard with no rebroadcast — nothing
@@ -110,10 +110,10 @@ impl SessionActor {
tools: tool_specs,
model: Some(model.clone()),
temperature: None,
x_grok_conv_id: Some(btw_session_id.clone()),
x_grok_req_id: Some(format!("xai-btw-{}", uuid::Uuid::new_v4())),
x_grok_session_id: Some(parent_session_id.clone()),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(btw_session_id.clone()),
x_kigi_req_id: Some(format!("xai-btw-{}", uuid::Uuid::new_v4())),
x_kigi_session_id: Some(parent_session_id.clone()),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..Default::default()
};
@@ -238,8 +238,8 @@ impl SessionActor {
// ~2540 words, and `clean_recap_text` caps it at a generous
// RECAP_MAX_CHARS safety net, so an explicit token cap isn't needed.
let started_at = chrono::Utc::now().to_rfc3339();
let x_grok_conv_id = format!("recap-{}", uuid::Uuid::new_v4());
let x_grok_req_id = format!("xai-recap-{}", uuid::Uuid::new_v4());
let x_kigi_conv_id = format!("recap-{}", uuid::Uuid::new_v4());
let x_kigi_req_id = format!("xai-recap-{}", uuid::Uuid::new_v4());
// Clone the exact request items for the on-disk artifact (recap never
// mutates conversation state, so this file is the only durable record).
let chat_history_for_artifact = items.clone();
@@ -248,10 +248,10 @@ impl SessionActor {
tools: vec![],
model: Some(model.clone()),
temperature: None,
x_grok_conv_id: Some(x_grok_conv_id.clone()),
x_grok_req_id: Some(x_grok_req_id.clone()),
x_grok_session_id: Some(self.session_info.id.to_string()),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(x_kigi_conv_id.clone()),
x_kigi_req_id: Some(x_kigi_req_id.clone()),
x_kigi_session_id: Some(self.session_info.id.to_string()),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..Default::default()
};
@@ -265,8 +265,8 @@ impl SessionActor {
auto,
strip_reasoning,
tag,
&x_grok_req_id,
&x_grok_conv_id,
&x_kigi_req_id,
&x_kigi_conv_id,
started_at,
None,
None,
@@ -291,8 +291,8 @@ impl SessionActor {
auto,
strip_reasoning,
tag,
&x_grok_req_id,
&x_grok_conv_id,
&x_kigi_req_id,
&x_kigi_conv_id,
started_at,
None,
Some(raw_response.as_str()).filter(|s| !s.is_empty()),
@@ -307,7 +307,7 @@ impl SessionActor {
}
// New prompt while generating: keep artifact, skip display, leave watermark.
// Applies to manual `/recap` too: spinner-less clients (e.g. Grok
// Applies to manual `/recap` too: spinner-less clients (e.g. Kigi
// Desktop) would otherwise append the late recap mid-turn.
if self.recap_was_cancelled(recap_epoch) {
tracing::info!(
@@ -322,8 +322,8 @@ impl SessionActor {
auto,
strip_reasoning,
tag,
&x_grok_req_id,
&x_grok_conv_id,
&x_kigi_req_id,
&x_kigi_conv_id,
started_at,
Some(summary.as_str()),
Some(raw_response.as_str()),
@@ -346,8 +346,8 @@ impl SessionActor {
auto,
strip_reasoning,
tag,
&x_grok_req_id,
&x_grok_conv_id,
&x_kigi_req_id,
&x_kigi_conv_id,
started_at,
Some(summary.as_str()),
Some(raw_response.as_str()),
@@ -365,8 +365,8 @@ impl SessionActor {
auto,
strip_reasoning,
tag,
&x_grok_req_id,
&x_grok_conv_id,
&x_kigi_req_id,
&x_kigi_conv_id,
started_at,
Some(summary.as_str()),
Some(raw_response.as_str()),
@@ -433,8 +433,8 @@ impl SessionActor {
auto: bool,
strip_reasoning: bool,
reminder_tag: &str,
x_grok_req_id: &str,
x_grok_conv_id: &str,
x_kigi_req_id: &str,
x_kigi_conv_id: &str,
started_at: String,
summary: Option<&str>,
raw_response: Option<&str>,
@@ -449,8 +449,8 @@ impl SessionActor {
created_at: started_at,
trigger: if auto { "auto" } else { "manual" }.to_owned(),
model: model.to_owned(),
x_grok_req_id: x_grok_req_id.to_owned(),
x_grok_conv_id: x_grok_conv_id.to_owned(),
x_kigi_req_id: x_kigi_req_id.to_owned(),
x_kigi_conv_id: x_kigi_conv_id.to_owned(),
strip_reasoning,
reminder_tag: reminder_tag.to_owned(),
chat_history,
@@ -578,7 +578,7 @@ impl SessionActor {
/// Temperature, max_output_tokens, and
/// reasoning_effort are left unset — mirrors [`Self::handle_recap`]: the
/// proxy may inject provider defaults, a small token cap silently empties
/// a reasoning model's response, and some models (e.g. `grok-build`)
/// a reasoning model's response, and some models (e.g. `kigi`)
/// reject an explicit `reasoningEffort` with a 400. Output is filtered
/// through [`prompt_suggest::sanitize_suggestion`]; any failure returns
/// through [`prompt_suggest::sanitize_suggestion`]; any failure returns
@@ -643,10 +643,10 @@ impl SessionActor {
tools: vec![],
model: Some(model),
temperature: None,
x_grok_conv_id: Some(format!("promptsuggest-{}", uuid::Uuid::new_v4())),
x_grok_req_id: Some(format!("xai-promptsuggest-{}", uuid::Uuid::new_v4())),
x_grok_session_id: Some(self.session_info.id.to_string()),
x_grok_agent_id: Some(crate::util::agent_id::agent_id()),
x_kigi_conv_id: Some(format!("promptsuggest-{}", uuid::Uuid::new_v4())),
x_kigi_req_id: Some(format!("xai-promptsuggest-{}", uuid::Uuid::new_v4())),
x_kigi_session_id: Some(self.session_info.id.to_string()),
x_kigi_agent_id: Some(crate::util::agent_id::agent_id()),
..Default::default()
};

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