M0: compilable skeleton — Kigi 0.1.0 fork surgery

Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,330 @@
//! Repro + regression test: leader process dies → connected clients must
//! re-elect a leader and transparently restore their sessions.
//!
//! Scenario (mirrors the field report "clients see `unknown session id` after
//! the leader dies"):
//!
//! 1. Two stdio clients (`grok agent --leader stdio`) share one leader.
//! 2. Each creates its own session and completes a prompt round-trip.
//! 3. The leader is killed with SIGKILL (crash, no graceful shutdown).
//! 4. Each client's bridge must reconnect (re-electing / spawning a fresh
//! leader), replay `initialize` + `session/load`, and then prompts against
//! the ORIGINAL session IDs must succeed again.
//!
//! Tests are `#[ignore]`d by default — they require a pre-built binary:
//!
//! ```bash
//! cargo test -p kigi-shell --test test_leader_death_repro -- --ignored --nocapture
//! ```
#![cfg(unix)]
use std::time::Duration;
use agent_client_protocol::{self as acp, Agent as _};
use kigi_test_support::leader::{
LeaderStdioClient, leader_log, wait_for_live_leader, wait_for_new_leader,
wait_for_replay_notifications,
};
use kigi_test_support::*;
/// THE repro. Kill the shared leader with SIGKILL while two clients are
/// connected; both must recover their sessions on the re-elected leader.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_leader_sigkill_clients_recover_sessions() {
tokio::task::LocalSet::new()
.run_until(async {
let server = MockInferenceServer::start().await.unwrap();
let workdir = git_workdir();
let home = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".kigi")).unwrap();
// ── Phase 1: two clients, one leader, two sessions ────────────
let client_a = LeaderStdioClient::spawn(&server, workdir.path(), home.path()).await;
client_a.initialize().await;
let session_a = client_a.create_session(workdir.path()).await;
let r = client_a.prompt(&session_a, "hello from A").await;
assert!(
r.is_ok(),
"pre-crash prompt A failed: {:?}\nstderr:\n{}\nleader log:\n{}",
r.err(),
client_a.stderr_text(),
leader_log(home.path()),
);
let client_b = LeaderStdioClient::spawn(&server, workdir.path(), home.path()).await;
client_b.initialize().await;
let session_b = client_b.create_session(workdir.path()).await;
let r = client_b.prompt(&session_b, "hello from B").await;
assert!(
r.is_ok(),
"pre-crash prompt B failed: {:?}\nstderr:\n{}\nleader log:\n{}",
r.err(),
client_b.stderr_text(),
leader_log(home.path()),
);
let leader_pid = wait_for_live_leader(home.path(), Duration::from_secs(5))
.await
.expect("no live leader PID in lock file");
assert_ne!(leader_pid, client_a.child.id().unwrap_or(0));
assert_ne!(leader_pid, client_b.child.id().unwrap_or(0));
// ── Phase 2: SIGKILL the leader (simulated crash) ─────────────
let base_a = client_a.notification_count();
let base_b = client_b.notification_count();
eprintln!("killing leader pid {leader_pid}");
unsafe {
libc::kill(leader_pid as i32, libc::SIGKILL);
}
// ── Phase 3: clients must re-elect a leader and reconnect ─────
let new_pid = wait_for_new_leader(home.path(), leader_pid, Duration::from_secs(60))
.await
.unwrap_or_else(|| {
panic!(
"no new leader was elected after SIGKILL\n\
client A stderr:\n{}\nclient B stderr:\n{}\nleader log:\n{}",
client_a.stderr_text(),
client_b.stderr_text(),
leader_log(home.path()),
)
});
eprintln!("new leader elected: pid {new_pid}");
let a_reconnected =
wait_for_replay_notifications(&client_a, base_a, Duration::from_secs(60)).await;
let b_reconnected =
wait_for_replay_notifications(&client_b, base_b, Duration::from_secs(60)).await;
eprintln!("replay evidence: A={a_reconnected} B={b_reconnected}");
// ── Phase 4: prompts on the ORIGINAL session IDs must work ────
let res_a = client_a.prompt(&session_a, "after crash A").await;
let res_b = client_b.prompt(&session_b, "after crash B").await;
assert!(
res_a.is_ok(),
"client A prompt after leader crash failed: {:?}\n\
stderr:\n{}\nleader log:\n{}",
res_a.err(),
client_a.stderr_text(),
leader_log(home.path()),
);
assert!(
res_b.is_ok(),
"client B prompt after leader crash failed: {:?}\n\
stderr:\n{}\nleader log:\n{}",
res_b.err(),
client_b.stderr_text(),
leader_log(home.path()),
);
})
.await;
}
/// Single-client variant: kill -9 the leader, the lone client must re-elect
/// and restore. Narrower failure surface than the two-client test.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_leader_sigkill_single_client_recovers() {
tokio::task::LocalSet::new()
.run_until(async {
let server = MockInferenceServer::start().await.unwrap();
let workdir = git_workdir();
let home = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".kigi")).unwrap();
let client = LeaderStdioClient::spawn(&server, workdir.path(), home.path()).await;
client.initialize().await;
let session = client.create_session(workdir.path()).await;
client
.prompt(&session, "hello")
.await
.expect("pre-crash prompt failed");
let leader_pid = wait_for_live_leader(home.path(), Duration::from_secs(5))
.await
.expect("no live leader PID in lock file");
let base = client.notification_count();
eprintln!("killing leader pid {leader_pid}");
unsafe {
libc::kill(leader_pid as i32, libc::SIGKILL);
}
let new_pid = wait_for_new_leader(home.path(), leader_pid, Duration::from_secs(60))
.await
.unwrap_or_else(|| {
panic!(
"no new leader was elected after SIGKILL\nstderr:\n{}\nleader log:\n{}",
client.stderr_text(),
leader_log(home.path()),
)
});
eprintln!("new leader elected: pid {new_pid}");
let reconnected =
wait_for_replay_notifications(&client, base, Duration::from_secs(60)).await;
eprintln!("replay evidence: {reconnected}");
let res = client.prompt(&session, "after crash").await;
assert!(
res.is_ok(),
"prompt after leader crash failed: {:?}\nstderr:\n{}\nleader log:\n{}",
res.err(),
client.stderr_text(),
leader_log(home.path()),
);
})
.await;
}
/// One client driving TWO sessions over a single stdio bridge (the IDE
/// shape). After a leader SIGKILL, BOTH sessions must be replayed onto the
/// re-elected leader — restoring only the most recent one left the other
/// failing with "unknown session id".
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_leader_sigkill_multi_session_client_recovers_all_sessions() {
tokio::task::LocalSet::new()
.run_until(async {
let server = MockInferenceServer::start().await.unwrap();
let workdir = git_workdir();
let home = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".kigi")).unwrap();
let client = LeaderStdioClient::spawn(&server, workdir.path(), home.path()).await;
client.initialize().await;
let session_one = client.create_session(workdir.path()).await;
client
.prompt(&session_one, "hello one")
.await
.expect("pre-crash prompt on session one failed");
let session_two = client.create_session(workdir.path()).await;
client
.prompt(&session_two, "hello two")
.await
.expect("pre-crash prompt on session two failed");
assert_ne!(session_one.0, session_two.0);
let leader_pid = wait_for_live_leader(home.path(), Duration::from_secs(5))
.await
.expect("no live leader PID in lock file");
let base = client.notification_count();
eprintln!("killing leader pid {leader_pid}");
unsafe {
libc::kill(leader_pid as i32, libc::SIGKILL);
}
wait_for_new_leader(home.path(), leader_pid, Duration::from_secs(60))
.await
.unwrap_or_else(|| {
panic!(
"no new leader was elected after SIGKILL\nstderr:\n{}\nleader log:\n{}",
client.stderr_text(),
leader_log(home.path()),
)
});
wait_for_replay_notifications(&client, base, Duration::from_secs(60)).await;
// BOTH sessions must work on the new leader.
let res_one = client.prompt(&session_one, "after crash one").await;
let res_two = client.prompt(&session_two, "after crash two").await;
assert!(
res_one.is_ok(),
"session one prompt after crash failed: {:?}\nstderr:\n{}\nleader log:\n{}",
res_one.err(),
client.stderr_text(),
leader_log(home.path()),
);
assert!(
res_two.is_ok(),
"session two prompt after crash failed: {:?}\nstderr:\n{}\nleader log:\n{}",
res_two.err(),
client.stderr_text(),
leader_log(home.path()),
);
})
.await;
}
/// Prompt sent DURING the outage (after the bridge noticed the dead leader
/// but before the new one is ready). The stdio bridge must hold and deliver
/// it once the session is restored — not silently drop it (which left the
/// client's request hanging forever).
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_prompt_sent_during_outage_is_delivered_after_recovery() {
tokio::task::LocalSet::new()
.run_until(async {
let server = MockInferenceServer::start().await.unwrap();
let workdir = git_workdir();
let home = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".kigi")).unwrap();
let client = LeaderStdioClient::spawn(&server, workdir.path(), home.path()).await;
client.initialize().await;
let session = client.create_session(workdir.path()).await;
client
.prompt(&session, "hello")
.await
.expect("pre-crash prompt failed");
let leader_pid = wait_for_live_leader(home.path(), Duration::from_secs(5))
.await
.expect("no live leader PID in lock file");
eprintln!("killing leader pid {leader_pid}");
unsafe {
libc::kill(leader_pid as i32, libc::SIGKILL);
}
// Give the bridge a moment to observe the dead socket (its send
// channel closes), then prompt mid-outage: re-election + session
// restore are still seconds away.
tokio::time::sleep(Duration::from_millis(300)).await;
let res = tokio::time::timeout(
Duration::from_secs(90),
client.conn.prompt(acp::PromptRequest::new(session.clone(), vec![acp::ContentBlock::Text(acp::TextContent::new("sent during outage".to_string()))])),
)
.await
.unwrap_or_else(|_| {
panic!(
"prompt sent during outage never completed (dropped by bridge?)\n\
stderr:\n{}\nleader log:\n{}",
client.stderr_text(),
leader_log(home.path()),
)
});
assert!(
res.is_ok(),
"prompt sent during outage failed: {:?}\nstderr:\n{}\nleader log:\n{}",
res.err(),
client.stderr_text(),
leader_log(home.path()),
);
// A session-scoped request other than prompt (model switch) must
// also survive — same "unknown session id" class.
let set_model = tokio::time::timeout(
Duration::from_secs(30),
client.conn.set_session_model(acp::SetSessionModelRequest::new(session.clone(), acp::ModelId::new("test-model"))),
)
.await
.unwrap_or_else(|_| {
panic!(
"set_session_model after recovery never completed\nstderr:\n{}\nleader log:\n{}",
client.stderr_text(),
leader_log(home.path()),
)
});
assert!(
set_model.is_ok(),
"set_session_model after recovery failed: {:?}\nstderr:\n{}\nleader log:\n{}",
set_model.err(),
client.stderr_text(),
leader_log(home.path()),
);
})
.await;
}