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
+14
View File
@@ -0,0 +1,14 @@
/// Terminal in-process error from [`crate::FsEventSource::start`]. Not
/// `Serialize`: never crosses the workspace transport boundary.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FsNotifyError {
#[error("failed to start watcher")]
WatcherStart(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("watcher initialization timed out")]
Timeout,
#[error("FsEventSource::start called outside a tokio runtime")]
NoRuntime,
}
+76
View File
@@ -0,0 +1,76 @@
//! Public event types — the wire contract for `kigi-fsnotify`.
//!
//! Pure data: no I/O, no tokio, no intra-crate deps. Safe to lift into a
//! sibling `-types` crate for WASM/no-tokio consumers.
//!
//! All variants are `#[non_exhaustive]`; add additively. The workspace
//! translator (in `kigi-workspace`) maps these to
//! `WorkspaceEvent`s and enriches `GitOperationCompleted { head_changed:
//! true }` with `commit + branch + vcs` via a git shell-out — that I/O
//! belongs at the workspace layer, not on the OS-watcher hot path.
use std::path::PathBuf;
/// One semantic event from the local workspace. Causal order on the
/// source's broadcast channel. `FilesChanged` paths share a single `kind`
/// (per-debounce-window grouping); per-event causality would need
/// `Vec<{path, kind}>`.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
#[non_exhaustive]
pub enum FsEvent {
/// Workspace file changes; all paths share `kind`. Paths under
/// `git_dir` are excluded (metadata surfaces as `GitMetaChanged`,
/// `.lock` files are dropped).
FilesChanged {
paths: Vec<PathBuf>,
kind: FsEventKind,
},
/// A git metadata file changed (HEAD, index, refs/, FETCH_HEAD).
GitMetaChanged { kind: GitMetaKind },
/// VCS lock activity observed: `index.lock`/`gc.pid`/`.sl` `wlock` is
/// present, or an event for one arrived with the file already gone (fast
/// ops complete inside one debounce batch). State is in flux until the
/// matching `GitOperationCompleted` arrives.
GitOperationStarted,
/// The lock has been gone for [`crate::SETTLE_MS`]: rapid lock cycles
/// (rebase/squash picks) merge into one operation, so one pair is emitted
/// per burst, not per cycle. `head_changed` reports whether `.git/HEAD`
/// differs from its value when the operation's *first* lock appeared.
GitOperationCompleted { head_changed: bool },
}
/// Aligned with `kigi_workspace_types::FsEventKind` (identity map at
/// the workspace boundary). `notify::EventKind::{Access, Any, Other}` are
/// filtered upstream and never surface here.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum FsEventKind {
Created,
#[default]
Modified,
Removed,
Renamed,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum GitMetaKind {
/// `.git/HEAD` (branch switch, commit, rebase step).
HeadChanged,
/// `.git/index` (`git add`, `git reset`, `git commit`).
IndexChanged,
/// `.git/refs/*` or `.git/packed-refs` (ref updates).
RefsChanged,
/// `.git/FETCH_HEAD` (fetch / pull).
FetchHeadChanged,
}
+20
View File
@@ -0,0 +1,20 @@
//! Local-filesystem event source. Single causal stream of wire-ready
//! [`FsEvent`]s on one broadcast channel. The `kigi-workspace` layer
//! translates these into `WorkspaceEvent`s with git-enrichment I/O.
//!
//! Single workspace root only; multi-root composition (parent + worktrees)
//! lives in the workspace layer.
mod error;
mod event;
mod paths;
mod source;
mod state;
mod watcher;
pub use error::FsNotifyError;
pub use event::{FsEvent, FsEventKind, GitMetaKind};
pub use source::{
FsConfig, FsEventSource, FsWatcherStats, STATS_TARGET, set_runtime_handle, shared, stats,
};
pub use state::SETTLE_MS;
+85
View File
@@ -0,0 +1,85 @@
//! `.git/` path classification. Component-based against the discovered
//! `git_dir` (not substring matching), so `/tmp/.git-backup/HEAD` is safe
//! and Windows separators work.
//!
//! Watched: `HEAD`, `index`, `refs/*`, `packed-refs`, `FETCH_HEAD`.
//! Skipped: `COMMIT_EDITMSG`, `MERGE_HEAD`, `REBASE_HEAD`, `objects/*`
//! (too noisy or no meaningful state change). `index.lock` is handled by
//! the lock state machine, not here.
use std::path::Path;
use crate::event::GitMetaKind;
/// `git_dir` is from `git2::Repository::discover().path()` (handles worktrees).
pub(crate) fn classify_git_path(path: &Path, git_dir: &Path) -> Option<GitMetaKind> {
let rel = path.strip_prefix(git_dir).ok()?.to_str()?;
match rel {
"HEAD" => Some(GitMetaKind::HeadChanged),
"FETCH_HEAD" => Some(GitMetaKind::FetchHeadChanged),
"index" => Some(GitMetaKind::IndexChanged),
"packed-refs" => Some(GitMetaKind::RefsChanged),
s if s.starts_with("refs/") || s.starts_with("refs\\") => Some(GitMetaKind::RefsChanged),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn classify(p: &str, git_dir: &str) -> Option<GitMetaKind> {
classify_git_path(&PathBuf::from(p), &PathBuf::from(git_dir))
}
#[test]
fn classify_positive_cases() {
let g = "/r/.git";
assert_eq!(classify("/r/.git/HEAD", g), Some(GitMetaKind::HeadChanged));
assert_eq!(
classify("/r/.git/index", g),
Some(GitMetaKind::IndexChanged)
);
assert_eq!(
classify("/r/.git/FETCH_HEAD", g),
Some(GitMetaKind::FetchHeadChanged)
);
assert_eq!(
classify("/r/.git/packed-refs", g),
Some(GitMetaKind::RefsChanged)
);
assert_eq!(
classify("/r/.git/refs/heads/feature-branch-with-slashes", g),
Some(GitMetaKind::RefsChanged)
);
assert_eq!(
classify("/r/.git/refs/remotes/origin/main", g),
Some(GitMetaKind::RefsChanged)
);
}
#[test]
fn classify_returns_none() {
let g = "/r/.git";
// Excluded git internals.
assert_eq!(classify("/r/.git/COMMIT_EDITMSG", g), None);
assert_eq!(classify("/r/.git/MERGE_HEAD", g), None);
assert_eq!(classify("/r/.git/objects/ab/1234", g), None);
assert_eq!(classify("/r/.git/index.lock", g), None);
// Workspace files.
assert_eq!(classify("/r/src/main.rs", g), None);
// Substring false-positive prevented by strip_prefix.
assert_eq!(classify("/r/.git-backup/HEAD", g), None);
// Path under a different git_dir.
assert_eq!(classify("/other/.git/HEAD", g), None);
}
#[test]
fn classify_handles_worktree_gitdir() {
assert_eq!(
classify("/r/.git/worktrees/wt/HEAD", "/r/.git/worktrees/wt"),
Some(GitMetaKind::HeadChanged)
);
}
}
File diff suppressed because it is too large Load Diff
+373
View File
@@ -0,0 +1,373 @@
//! Lock-state machine. Pure data + pure transition function. No I/O.
use std::time::{Duration, Instant};
/// Drop transient OS events for this window after a head-changing op;
/// consumers refresh from scratch anyway.
pub(crate) const COOLDOWN_MS: u64 = 500;
/// After a lock release, wait this long before declaring the operation
/// complete: a lock reappearing within the window (a rebase/squash cycles
/// `index.lock` per pick) is the *same* operation, so rapid cycles merge into
/// one `Started`/`Completed` pair instead of storming consumers.
pub const SETTLE_MS: u64 = 500;
/// Diagnostic threshold — fires a one-time warning when a lock is held
/// longer than this. `git gc` on huge repos can exceed this legitimately;
/// the state machine stays locked until the lock file disappears regardless.
const STALE_LOCK_SECS: u64 = 60;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum LockState {
Idle,
Locked {
head_at_start: Option<String>,
since: Instant,
},
/// Lock released, operation not yet declared complete. `head_at_start`
/// and `since` are carried from the first `Locked` entry of the merged
/// operation so re-locks preserve the op-wide HEAD comparison and the
/// stale-lock clock.
Settling {
head_at_start: Option<String>,
since: Instant,
until: Instant,
},
Cooldown {
until: Instant,
},
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum LockTransition {
None,
Started,
/// Emitted on any `Locked → !Locked` transition. `head_changed` is the
/// HEAD comparison; cooldown begins iff true.
Completed {
head_changed: bool,
},
/// Cooldown timer expired; consumer never sees this — internal only.
CooldownEnded,
}
/// One step. Pure; mutates `state` from freshly-observed FS facts.
pub(crate) fn drive(
state: &mut LockState,
lock_present: bool,
head_now: Option<String>,
now: Instant,
cooldown: Duration,
) -> LockTransition {
match (state.clone(), lock_present) {
(LockState::Idle, true) | (LockState::Cooldown { .. }, true) => {
*state = LockState::Locked {
head_at_start: head_now,
since: now,
};
LockTransition::Started
}
// Same operation resumes: keep the op-start HEAD and `since` so the
// eventual Completed spans the whole merged op. No duplicate Started —
// consumers never saw a Completed, so their in-op flag never flipped.
(
LockState::Settling {
head_at_start,
since,
..
},
true,
) => {
*state = LockState::Locked {
head_at_start,
since,
};
LockTransition::None
}
// Don't complete yet: give a rapid re-lock the settle window to merge.
(
LockState::Locked {
head_at_start,
since,
},
false,
) => {
*state = LockState::Settling {
head_at_start,
since,
until: now + Duration::from_millis(SETTLE_MS),
};
LockTransition::None
}
(
LockState::Settling {
head_at_start,
until,
..
},
false,
) if now >= until => {
let head_changed = head_at_start.as_ref() != head_now.as_ref();
*state = if head_changed {
LockState::Cooldown {
until: now + cooldown,
}
} else {
LockState::Idle
};
LockTransition::Completed { head_changed }
}
(LockState::Cooldown { until }, false) if now >= until => {
*state = LockState::Idle;
LockTransition::CooldownEnded
}
_ => LockTransition::None,
}
}
/// `check` fires once per stale period; resets when the lock releases.
#[derive(Debug, Default)]
pub(crate) struct StaleWarn {
warned: bool,
}
impl StaleWarn {
pub(crate) fn check(&mut self, state: &LockState, now: Instant) -> Option<Duration> {
match state {
// Settling counts as held: `since` spans the merged operation, so
// a long rebase of short lock cycles still warns (once), and the
// latch doesn't reset in the sub-second gaps between cycles.
LockState::Locked { since, .. } | LockState::Settling { since, .. } => {
let elapsed = now.duration_since(*since);
if !self.warned && elapsed > Duration::from_secs(STALE_LOCK_SECS) {
self.warned = true;
return Some(elapsed);
}
None
}
_ => {
self.warned = false;
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cooldown() -> Duration {
Duration::from_millis(500)
}
#[test]
fn idle_to_locked_on_lock_appearance() {
let mut s = LockState::Idle;
let now = Instant::now();
assert_eq!(
drive(&mut s, true, Some("ref: main".into()), now, cooldown()),
LockTransition::Started
);
assert!(matches!(s, LockState::Locked { .. }));
}
/// A lock release no longer completes the operation; it opens the settle
/// window (rapid re-locks merge) and emits nothing.
#[test]
fn locked_to_settling_emits_nothing() {
let now = Instant::now();
let mut s = LockState::Locked {
head_at_start: Some("ref: main".into()),
since: now,
};
assert_eq!(
drive(&mut s, false, Some("ref: feature".into()), now, cooldown()),
LockTransition::None
);
match &s {
LockState::Settling {
head_at_start,
since,
until,
} => {
assert_eq!(head_at_start.as_deref(), Some("ref: main"));
assert_eq!(*since, now);
assert_eq!(*until, now + Duration::from_millis(SETTLE_MS));
}
other => panic!("expected Settling, got {other:?}"),
}
}
/// Re-lock inside the settle window: the same operation continues, so the
/// op-start HEAD and `since` are preserved and nothing is emitted (no
/// duplicate Started — the consumer's in-op flag never flipped).
#[test]
fn settling_relock_preserves_op_start_and_emits_nothing() {
let op_start = Instant::now();
let later = op_start + Duration::from_millis(100);
let mut s = LockState::Settling {
head_at_start: Some("ref: main".into()),
since: op_start,
until: later + Duration::from_millis(400),
};
assert_eq!(
drive(&mut s, true, Some("pick-1".into()), later, cooldown()),
LockTransition::None
);
assert_eq!(
s,
LockState::Locked {
head_at_start: Some("ref: main".into()),
since: op_start,
},
"op-start HEAD and since must survive the re-lock"
);
}
/// Settle expiry emits exactly one Completed comparing the first pick's
/// pre-op HEAD against the final HEAD (head_changed spans the merged op).
#[test]
fn settling_expiry_emits_completed_spanning_merged_op() {
let now = Instant::now();
let mut s = LockState::Settling {
head_at_start: Some("ref: main".into()),
since: now - Duration::from_secs(1),
until: now,
};
assert_eq!(
drive(&mut s, false, Some("pick-4".into()), now, cooldown()),
LockTransition::Completed { head_changed: true }
);
assert!(matches!(s, LockState::Cooldown { .. }));
}
#[test]
fn settling_expiry_head_unchanged_goes_idle() {
let now = Instant::now();
let mut s = LockState::Settling {
head_at_start: Some("ref: main".into()),
since: now - Duration::from_secs(1),
until: now,
};
assert_eq!(
drive(&mut s, false, Some("ref: main".into()), now, cooldown()),
LockTransition::Completed {
head_changed: false
}
);
assert_eq!(s, LockState::Idle);
}
#[test]
fn settling_before_expiry_emits_nothing() {
let now = Instant::now();
let mut s = LockState::Settling {
head_at_start: Some("ref: main".into()),
since: now,
until: now + Duration::from_millis(1),
};
assert_eq!(
drive(&mut s, false, Some("pick-1".into()), now, cooldown()),
LockTransition::None
);
assert!(matches!(s, LockState::Settling { .. }));
}
#[test]
fn cooldown_to_idle_after_timer() {
let start = Instant::now();
let mut s = LockState::Cooldown { until: start };
let later = start + Duration::from_millis(1);
assert_eq!(
drive(&mut s, false, None, later, cooldown()),
LockTransition::CooldownEnded
);
assert_eq!(s, LockState::Idle);
}
#[test]
fn cooldown_to_locked_on_re_acquire() {
let now = Instant::now();
let mut s = LockState::Cooldown {
until: now + Duration::from_millis(500),
};
assert_eq!(
drive(&mut s, true, Some("ref: main".into()), now, cooldown()),
LockTransition::Started
);
assert!(matches!(s, LockState::Locked { .. }));
}
/// Regression: timer-arm `drive()` must report Started so the consumer's
/// `in_op` flag flips; otherwise FilesChanged events skip buffering.
#[test]
fn cooldown_to_locked_when_lock_reappears_at_timer_fire() {
let now = Instant::now();
let mut s = LockState::Cooldown { until: now };
assert_eq!(
drive(&mut s, true, Some("ref: main".into()), now, cooldown()),
LockTransition::Started,
);
assert!(matches!(s, LockState::Locked { .. }));
}
#[test]
fn no_transition_when_idle_and_no_lock() {
let mut s = LockState::Idle;
assert_eq!(
drive(&mut s, false, None, Instant::now(), cooldown()),
LockTransition::None
);
assert_eq!(s, LockState::Idle);
}
#[test]
fn stale_warn_fires_once_per_stale_period() {
let now = Instant::now();
let s = LockState::Locked {
head_at_start: None,
since: now - Duration::from_secs(STALE_LOCK_SECS + 1),
};
let mut w = StaleWarn::default();
assert!(w.check(&s, now).is_some());
// Second check while still Locked: latched, no re-fire.
assert!(w.check(&s, now).is_none());
}
#[test]
fn stale_warn_resets_when_lock_releases() {
let now = Instant::now();
let locked = LockState::Locked {
head_at_start: None,
since: now - Duration::from_secs(STALE_LOCK_SECS + 1),
};
let mut w = StaleWarn::default();
assert!(w.check(&locked, now).is_some());
assert!(w.check(&LockState::Idle, now).is_none());
// Re-acquire: should fire again.
assert!(w.check(&locked, now).is_some());
}
/// A long rebase made of short lock cycles: `since` spans the merged op,
/// so the warning fires once past the threshold and the settle gaps
/// between cycles neither reset the latch nor re-fire it.
#[test]
fn stale_warn_spans_merged_op_and_stays_latched_through_settling() {
let now = Instant::now();
let op_start = now - Duration::from_secs(STALE_LOCK_SECS + 1);
let settling = LockState::Settling {
head_at_start: None,
since: op_start,
until: now + Duration::from_millis(SETTLE_MS),
};
let locked = LockState::Locked {
head_at_start: None,
since: op_start,
};
let mut w = StaleWarn::default();
assert!(w.check(&settling, now).is_some(), "settling counts as held");
assert!(w.check(&locked, now).is_none(), "latched across re-lock");
assert!(w.check(&settling, now).is_none(), "latched across release");
}
}
File diff suppressed because it is too large Load Diff