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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
//! Repository/worktree discovery helpers.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
/// Find the git directory for a path using gix (handles both repos and worktrees).
///
/// For a regular repo, returns the `.git` directory. For a linked worktree,
/// returns the worktree's git dir under `.git/worktrees/<name>`.
///
/// Note: currently unused in production code — retained for future use and
/// tested below. See `find_worktree_git_dir` for the version used by
/// `copy_git_index`.
#[allow(dead_code)]
pub(crate) fn find_git_dir(path: &Path) -> Result<PathBuf> {
let repo = gix::discover(path)
.with_context(|| format!("failed to discover git repo at {}", path.display()))?;
Ok(repo.git_dir().to_path_buf())
}
/// Find the worktree's git directory from its `.git` file.
///
/// Worktrees have a `.git` file (not directory) that points to the actual git dir.
/// For regular repos, returns the `.git` directory.
pub(crate) fn find_worktree_git_dir(worktree_path: &Path) -> Result<PathBuf> {
let git_path = worktree_path.join(".git");
if git_path.is_file() {
// Worktree: .git is a file containing "gitdir: <path>"
let content = std::fs::read_to_string(&git_path)
.with_context(|| format!("failed to read .git file at {}", git_path.display()))?;
let raw = content
.strip_prefix("gitdir: ")
.ok_or_else(|| anyhow::anyhow!("invalid .git file format: {}", content.trim()))?
.trim();
// git may write a RELATIVE pointer (worktrees added with a relative
// path). Resolve it against the worktree dir — otherwise downstream
// index lookups join it against the CWD and break (mirrors
// `read_worktree_gitdir` in api.rs).
let raw_path = Path::new(raw);
let resolved = if raw_path.is_relative() {
worktree_path.join(raw_path)
} else {
raw_path.to_path_buf()
};
Ok(dunce::canonicalize(&resolved).unwrap_or(resolved))
} else if git_path.is_dir() {
// Regular repository
Ok(git_path)
} else {
anyhow::bail!(
"no .git file or directory found at {}",
worktree_path.display()
)
}
}
/// Find the worktree root (working directory root) for a path.
///
/// This handles both regular repositories and worktrees correctly.
/// For a regular repo at `/repo`, returns `/repo`.
/// For a worktree at `/worktrees/wt1`, returns `/worktrees/wt1`.
/// For a subdirectory `/repo/subdir`, returns `/repo`.
pub(crate) fn find_worktree_root(path: &Path) -> Result<PathBuf> {
let repo = gix::discover(path)
.with_context(|| format!("failed to discover git repo at {}", path.display()))?;
// workdir() returns the working directory root for both repos and worktrees
let work_dir = repo
.workdir()
.ok_or_else(|| anyhow::anyhow!("bare repository has no working directory"))?;
Ok(work_dir.to_path_buf())
}
/// Get the HEAD commit hash using gix.
pub(crate) fn get_head_commit(path: &Path) -> Result<String> {
let repo = gix::discover(path)
.with_context(|| format!("failed to discover git repo at {}", path.display()))?;
let head = repo
.head()
.context("failed to get HEAD")?
.peel_to_commit()
.context("failed to peel HEAD to commit")?;
Ok(head.id().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use kigi_test_utils::git::{git_commit_all, init_git_repo};
use tempfile::TempDir;
#[test]
fn test_find_git_dir() {
kigi_test_utils::require_git!();
let temp = TempDir::new().unwrap();
init_git_repo(temp.path());
let git_dir = find_git_dir(temp.path()).unwrap();
assert!(git_dir.ends_with(".git"));
assert!(git_dir.is_dir());
}
#[test]
fn test_find_worktree_git_dir_resolves_relative_gitdir() {
// A worktree `.git` file can hold a RELATIVE `gitdir:` pointer; it must
// resolve against the worktree dir, not be returned as-is (which would
// break index copy for relative-gitdir worktrees).
let temp = TempDir::new().unwrap();
let worktree = temp.path().join("wt");
std::fs::create_dir_all(&worktree).unwrap();
let real_git = temp.path().join("repo/.git/worktrees/wt");
std::fs::create_dir_all(&real_git).unwrap();
std::fs::write(worktree.join(".git"), "gitdir: ../repo/.git/worktrees/wt\n").unwrap();
let resolved = find_worktree_git_dir(&worktree).unwrap();
assert_eq!(resolved, dunce::canonicalize(&real_git).unwrap());
}
#[test]
fn test_find_worktree_git_dir_regular_repo() {
kigi_test_utils::require_git!();
let temp = TempDir::new().unwrap();
init_git_repo(temp.path());
let git_dir = find_worktree_git_dir(temp.path()).unwrap();
assert!(git_dir.ends_with(".git"));
}
#[test]
fn test_get_head_commit() {
kigi_test_utils::require_git!();
let temp = TempDir::new().unwrap();
init_git_repo(temp.path());
// Create a commit
std::fs::write(temp.path().join("file.txt"), "content").unwrap();
git_commit_all(temp.path(), "initial");
let commit = get_head_commit(temp.path()).unwrap();
assert_eq!(commit.len(), 40); // SHA-1 hex string
assert!(commit.chars().all(|c| c.is_ascii_hexdigit()));
}
}
@@ -0,0 +1,307 @@
//! Git index operations used during worktree creation.
use std::fs::Metadata;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::copy::cow::clone_file;
use crate::git::discovery::find_worktree_git_dir;
/// Copy the git index from source to destination worktree.
///
/// Resolves the actual git directory for both sides, handling linked worktrees
/// where `.git` is a file pointing to the real git dir. Both sides use
/// `find_worktree_git_dir` for consistency: for a regular repo it returns
/// `.git/`, for a linked worktree it follows the `gitdir:` pointer.
///
/// Uses CoW (reflink) copy for efficiency on APFS/Btrfs.
///
/// Returns `true` if the index was actually copied, `false` if the source
/// has no index file.
pub(crate) fn copy_git_index(source: &Path, dest_worktree: &Path) -> Result<bool> {
let source_git_dir = find_worktree_git_dir(source)?;
let dest_git_dir = find_worktree_git_dir(dest_worktree)?;
let source_index = source_git_dir.join("index");
let dest_index = dest_git_dir.join("index");
if source_index.exists() {
// reflink_or_copy cannot overwrite — remove destination first
if dest_index.exists() {
let _ = std::fs::remove_file(&dest_index);
}
clone_file(&source_index, &dest_index).with_context(|| {
format!(
"failed to copy index from {} to {}",
source_index.display(),
dest_index.display()
)
})?;
// Handle split index: when core.splitIndex is enabled, the index
// file references a `sharedindex.<hash>` file that must be
// reachable from the same directory as the index. For linked
// worktrees the shared index lives in the common git dir (the
// main repo's `.git/`), not in `.git/worktrees/<name>/`.
// Symlink any sharedindex.* files from the source's common dir
// into the dest git dir so gix can resolve them.
link_shared_indexes(&source_git_dir, &dest_git_dir)?;
tracing::debug!(
source = %source_index.display(),
dest = %dest_index.display(),
"copied git index (reflink)"
);
Ok(true)
} else {
Ok(false)
}
}
/// Symlink `sharedindex.*` files from the source into the destination
/// git directory.
///
/// When `core.splitIndex` is enabled, the main index file contains a
/// `link` extension referencing a content-addressed `sharedindex.<hash>`
/// file. `gix::index::File::at()` looks for this file in the **same
/// directory** as the index file. For linked worktrees the index lives
/// in `.git/worktrees/<name>/` but the shared index lives in the common
/// `.git/` directory. We bridge this by symlinking.
///
/// We scan **two** directories for shared index files:
/// 1. The source's **common dir** (main repo `.git/`) — where git
/// typically stores shared index files.
/// 2. The source's **own git dir** (`.git/worktrees/<name>/`) — git may
/// create new shared index files directly here when running inside a
/// linked worktree with `core.splitIndex` enabled.
///
/// No-op if there are no `sharedindex.*` files (i.e. split index is not
/// in use).
fn link_shared_indexes(source_git_dir: &Path, dest_git_dir: &Path) -> Result<()> {
// Resolve the common dir: for a linked worktree the `commondir` file
// points to the shared `.git/`. For a regular repo the git dir IS the
// common dir.
let source_common_dir = resolve_common_dir(source_git_dir);
// Collect directories to scan. Always include the common dir. If the
// source git dir is different (i.e. source is a linked worktree), also
// scan the source git dir itself — git may have created shared index
// files directly there.
let mut dirs_to_scan: Vec<&Path> = vec![&source_common_dir];
if source_git_dir != source_common_dir {
dirs_to_scan.push(source_git_dir);
}
let mut linked = 0u32;
for scan_dir in &dirs_to_scan {
let entries = match std::fs::read_dir(scan_dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.starts_with("sharedindex.") {
continue;
}
let src = entry.path();
let dst = dest_git_dir.join(&name);
// Skip if already present (e.g. dest IS the common dir, or
// already linked from a previous scan_dir iteration).
if dst.exists() {
continue;
}
// Symlink is ideal: instant, zero-copy, shared index is
// read-only content-addressed data.
#[cfg(unix)]
{
std::os::unix::fs::symlink(&src, &dst).with_context(|| {
format!(
"failed to symlink sharedindex {} -> {}",
dst.display(),
src.display()
)
})?;
}
#[cfg(not(unix))]
{
// Fallback: reflink/copy on Windows.
clone_file(&src, &dst).with_context(|| {
format!(
"failed to copy sharedindex {} -> {}",
src.display(),
dst.display()
)
})?;
}
linked += 1;
}
}
if linked > 0 {
tracing::debug!(
source_common_dir = %source_common_dir.display(),
dest_git_dir = %dest_git_dir.display(),
linked,
"linked sharedindex files for split-index support"
);
}
Ok(())
}
/// Resolve the common git directory from a worktree git dir.
///
/// For a linked worktree, `.git/worktrees/<name>/commondir` contains a
/// relative path (typically `../..`) pointing to the shared `.git/`.
/// For a regular repo, the git dir itself is the common dir.
fn resolve_common_dir(git_dir: &Path) -> PathBuf {
let commondir_file = git_dir.join("commondir");
if let Ok(content) = std::fs::read_to_string(&commondir_file) {
let relative = content.trim();
let resolved = git_dir.join(relative);
// Canonicalize to clean up `../..` etc.
dunce::canonicalize(&resolved).unwrap_or(resolved)
} else {
git_dir.to_path_buf()
}
}
/// Update index entries with new stat information from file metadata.
///
/// This updates the stat cache (mtime, size, etc.) for files that were copied,
/// avoiding the need for a full `git update-index --refresh`.
pub(crate) fn update_index_stats(
worktree_path: &Path,
file_metadata: &[(PathBuf, Metadata)],
) -> Result<()> {
if file_metadata.is_empty() {
return Ok(());
}
let start = std::time::Instant::now();
let git_dir = find_worktree_git_dir(worktree_path)?;
let index_path = git_dir.join("index");
// If index doesn't exist yet, there's nothing to update
if !index_path.exists() {
tracing::debug!(
path = %worktree_path.display(),
"index file doesn't exist yet, skipping update"
);
return Ok(());
}
// Guard against empty index files — gix-index panics when the file
// is 0 bytes because it tries to slice the trailing hash from an
// empty mmap (integer underflow in the slice range).
if index_path.metadata().map_or(true, |m| m.len() == 0) {
tracing::debug!(
path = %worktree_path.display(),
"index file is empty, skipping update"
);
return Ok(());
}
// Open the index file directly for modification
let mut index = gix::index::File::at(
&index_path,
gix::hash::Kind::Sha1,
false,
Default::default(),
)
.context("failed to open git index")?;
// Update stat info for each file that was copied
let mut updated_count = 0;
for entry in file_metadata.iter() {
let (path, metadata) = (&entry.0, &entry.1);
// Convert path to BStr for gix
let path_str = path.to_string_lossy();
let path_bytes: &gix::bstr::BStr = path_str.as_bytes().into();
// Find the entry in the index
if let Ok(entry_index) = index.entry_index_by_path(path_bytes) {
let entry = &mut index.entries_mut()[entry_index];
updated_count += 1;
// Update stat fields from metadata
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
// mtime (modification time)
entry.stat.mtime.secs = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as u32)
.unwrap_or(0);
entry.stat.mtime.nsecs = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.subsec_nanos())
.unwrap_or(0);
// ctime (change time) - MUST be the actual ctime, not mtime
entry.stat.ctime.secs = metadata.ctime() as u32;
entry.stat.ctime.nsecs = metadata.ctime_nsec() as u32;
// Other stat fields
entry.stat.size = metadata.len() as u32;
entry.stat.dev = metadata.dev() as u32;
entry.stat.ino = metadata.ino() as u32;
entry.stat.uid = metadata.uid();
entry.stat.gid = metadata.gid();
// Note: mode is on the entry itself, not stat
}
#[cfg(not(unix))]
{
// On non-Unix systems, use mtime for both
entry.stat.mtime.secs = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as u32)
.unwrap_or(0);
entry.stat.mtime.nsecs = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.subsec_nanos())
.unwrap_or(0);
entry.stat.ctime.secs = entry.stat.mtime.secs;
entry.stat.ctime.nsecs = entry.stat.mtime.nsecs;
entry.stat.size = metadata.len() as u32;
}
}
}
// Count how many entries were actually updated
let num_updated = updated_count;
// Write the updated index
index.write(Default::default())?;
tracing::debug!(
path = %worktree_path.display(),
files_updated = num_updated,
elapsed = ?start.elapsed(),
"updated index stat cache"
);
Ok(())
}
@@ -0,0 +1,20 @@
//! Git operations used by fast worktree creation.
//!
//! This module isolates git-specific functionality (worktree creation, status, index refresh)
//! from filesystem copy logic and orchestration.
pub(crate) mod checkout;
pub(crate) mod discovery;
pub(crate) mod index;
pub(crate) mod status;
pub(crate) mod worktree;
pub(crate) use checkout::checkout_ref;
pub(crate) use checkout::{git_clean_fd, git_reset_hard_command};
// Only consumed by the Linux-only snapshot finalize path.
#[cfg(target_os = "linux")]
pub(crate) use checkout::{has_staged_changes, worktree_at_ref, worktree_has_tracked_changes};
pub(crate) use discovery::{find_worktree_root, get_head_commit};
pub(crate) use index::{copy_git_index, update_index_stats};
pub(crate) use status::get_modified_files;
pub(crate) use worktree::worktree_add_no_checkout;
@@ -0,0 +1,106 @@
//! Git status helpers (compute dirty paths).
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use dashmap::DashSet;
use gix::bstr::BString;
use gix::status::index_worktree::Item;
use gix_status::index_as_worktree::{Change, EntryStatus};
use crate::copy::DirtyFilesReport;
/// Result of scanning for modified files, including both paths and counts by category.
pub(crate) struct ModifiedFilesResult {
/// Set of all relative paths that are modified/untracked/deleted.
pub paths: DashSet<PathBuf>,
/// Categorized counts of dirty files.
pub report: DirtyFilesReport,
}
/// Get modified files from the source repository.
///
/// This uses `gix`'s `index_worktree_iter` which compares the **index** to the
/// **worktree**. It reports which files have been modified/added/deleted relative
/// to what's staged, but does **not** expose the two-column staged-vs-worktree
/// status (`XY` in porcelain output). For full `XY` semantics (needed by
/// `sync::WorktreeSync`), see the CLI-based parser in `sync.rs`.
///
/// This is a blocking operation.
pub(crate) fn get_modified_files(source: &Path) -> Result<ModifiedFilesResult> {
let repo = gix::discover(source).context("failed to discover git repository")?;
let modified: DashSet<PathBuf> = DashSet::new();
// Guard against empty index files — gix-index panics when the file
// is 0 bytes because it tries to slice the trailing hash from an
// empty mmap (integer underflow in the slice range).
let index_path = repo.git_dir().join("index");
if index_path.metadata().map_or(true, |m| m.len() == 0) {
tracing::debug!(
path = %source.display(),
"index file is empty or missing, returning empty modified set"
);
return Ok(ModifiedFilesResult {
paths: modified,
report: DirtyFilesReport {
modified_files: 0,
untracked_files: 0,
deleted_files: 0,
},
});
}
let mut modified_count = 0u64;
let mut untracked_count = 0u64;
let mut deleted_count = 0u64;
// Cap produce workers: gix-features spawn-EAGAIN aborts under panic=abort.
let status = kigi_gix_status::with_budgeted_thread_limit(repo.status(gix::progress::Discard)?);
let iter = status.into_index_worktree_iter(Vec::<BString>::new())?;
for item_result in iter {
let item = item_result?;
let path = match &item {
Item::Modification {
rela_path, status, ..
} => {
// Check if it's a deletion (file exists in index but not in worktree)
match status {
EntryStatus::Change(Change::Removed) => deleted_count += 1,
_ => modified_count += 1,
}
rela_path.to_string()
}
Item::DirectoryContents { entry, .. } => {
// DirectoryContents = untracked files from directory walk
untracked_count += 1;
entry.rela_path.to_string()
}
Item::Rewrite { dirwalk_entry, .. } => {
// Rewrite = file was renamed (tracked as modified)
modified_count += 1;
dirwalk_entry.rela_path.to_string()
}
};
modified.insert(PathBuf::from(path));
}
tracing::info!(
count = modified.len(),
modified = modified_count,
untracked = untracked_count,
deleted = deleted_count,
"found modified files"
);
Ok(ModifiedFilesResult {
paths: modified,
report: DirtyFilesReport {
modified_files: modified_count,
untracked_files: untracked_count,
deleted_files: deleted_count,
},
})
}
@@ -0,0 +1,30 @@
//! Git worktree operations.
use std::path::Path;
use anyhow::{Context, Result};
use crate::git::checkout::git_command;
/// Create a git worktree with `--no-checkout`. Blocking.
pub(crate) fn worktree_add_no_checkout(source: &Path, dest: &str, git_ref: &str) -> Result<()> {
let output = git_command()
.current_dir(source)
.args([
"worktree",
"add",
"--detach",
"--no-checkout",
dest,
git_ref,
])
.output()
.context("failed to run git worktree add")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree add failed: {}", stderr);
}
Ok(())
}