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,311 @@
//! Roster types for the multi-client FleetView dashboard.
//!
//! The roster is a list
//! of dashboard-sized summaries of every session the leader hosts (resident
//! 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` →
//! `{ "upserted": [RosterEntry, …], "removed": ["sess-abc", …] }`
//!
//! The wire shape is intentionally small and current-state only — no event
//! fold or materialized snapshot is required (the snapshot is deferred).
use kigi_sampling_types::ReasoningEffort;
use serde::{Deserialize, Serialize};
use crate::session::persistence::Summary;
/// Coarse activity of a session as rendered in the dashboard's status column.
///
/// Mirrors the design's `SessionActivity` at dashboard granularity. A full
/// background-work breakdown (bg tasks / monitors / scheduler / subagents)
/// lands with a richer `SessionActivity`; the dashboard only needs this
/// coarse signal to pick a status glyph.
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RosterActivity {
/// A turn (user-originated or autonomous) is running.
Working,
/// Resident, no turn in flight.
Idle,
/// A permission / question / plan-approval is pending.
NeedsInput,
/// On disk, not resident.
Dormant,
/// Finished and resumable.
Completed,
/// Actor panicked / load failed.
Dead,
}
/// Where the session lives. Only `Local` is produced today; `Remote` is
/// reserved for cross-machine roster aggregation.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum RosterOrigin {
Local,
Remote { host: String },
}
/// One dashboard row.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RosterEntry {
pub session_id: String,
/// Generated/display title, if known. Clients fall back to cwd / id.
#[serde(default)]
pub title: Option<String>,
pub cwd: String,
pub is_worktree: bool,
#[serde(default)]
pub model_id: Option<String>,
/// Per-session reasoning effort for `model_id`. Carried alongside the model
/// so clients can render the session's effort in the roster without a
/// separate `model_state` fetch. `None` means "use the model/global
/// default" (or the session predates per-session effort persistence).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
pub yolo: bool,
pub activity: RosterActivity,
/// `true` while a resident actor hosts the session (vs. read from disk).
pub resident: bool,
/// Best-effort last-change timestamp (unix millis). Used for sort order.
pub last_change_unix_ms: i64,
pub origin: RosterOrigin,
}
/// Response payload for `x.ai/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.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct RosterChanged {
#[serde(default)]
pub upserted: Vec<RosterEntry>,
#[serde(default)]
pub removed: Vec<String>,
}
/// 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";
/// Merge live `resident` rows with on-disk `summaries` into the sorted roster.
/// Pure, so it is unit-testable without disk or a live actor.
///
/// Resident rows own the live state but carry no title or last-active time, so
/// each adopts those from its summary — except a `Working` row keeps its "now"
/// timestamp. Summaries with no resident row become `Dormant`; keying by id
/// dedups them. Hidden summaries are excluded.
pub(crate) fn merge_roster(
mut entries: Vec<RosterEntry>,
summaries: Vec<Summary>,
) -> Vec<RosterEntry> {
let mut by_id: std::collections::HashMap<String, Summary> = summaries
.into_iter()
.filter(|s| !s.is_hidden())
.map(|s| (s.info.id.0.to_string(), s))
.collect();
// Backfill resident rows; remove the summary so it isn't re-emitted below.
for entry in &mut entries {
let Some(summary) = by_id.remove(&entry.session_id) else {
continue;
};
if let Some(title) = summary.display_title_opt() {
entry.title = Some(title);
}
if entry.activity != RosterActivity::Working {
entry.last_change_unix_ms = summary.last_change_unix_ms();
}
}
// Remaining summaries have no resident row: emit them as dormant.
entries.extend(by_id.into_values().map(|summary| RosterEntry {
session_id: summary.info.id.0.to_string(),
title: summary.display_title_opt(),
cwd: summary.info.cwd.clone(),
is_worktree: summary.session_kind.as_deref() == Some("worktree")
|| summary.source_workspace_dir.is_some(),
model_id: Some(summary.current_model_id.0.to_string()),
reasoning_effort: summary.reasoning_effort,
yolo: false,
activity: RosterActivity::Dormant,
resident: false,
last_change_unix_ms: summary.last_change_unix_ms(),
origin: RosterOrigin::Local,
}));
// Most-recently-changed first.
entries.sort_by_key(|entry| std::cmp::Reverse(entry.last_change_unix_ms));
entries
}
#[cfg(test)]
mod merge_roster_tests {
use super::*;
use crate::session::info::Info;
use crate::session::persistence::default_model_id;
use agent_client_protocol as acp;
fn summary(id: &str, title: Option<&str>, last_active_ms: i64) -> Summary {
let mut s = Summary::new(
&Info {
id: acp::SessionId::new(id),
cwd: format!("/repo/{id}"),
},
default_model_id(),
)
.expect("summary");
s.generated_title = title.map(String::from);
s.last_active_at = chrono::DateTime::from_timestamp_millis(last_active_ms);
s
}
fn resident(id: &str, activity: RosterActivity, last_change_unix_ms: i64) -> RosterEntry {
RosterEntry {
session_id: id.to_string(),
title: None,
cwd: format!("/live/{id}"),
is_worktree: false,
model_id: Some("grok-4".into()),
reasoning_effort: None,
yolo: false,
activity,
resident: true,
last_change_unix_ms,
origin: RosterOrigin::Local,
}
}
#[test]
fn idle_resident_adopts_persisted_title_and_last_active() {
let now = 9_000;
let out = merge_roster(
vec![resident("a", RosterActivity::Idle, now)],
vec![summary("a", Some("Fix the roster"), 1_234)],
);
assert_eq!(out.len(), 1, "resident must not be duplicated as dormant");
assert_eq!(out[0].title.as_deref(), Some("Fix the roster"));
assert_eq!(out[0].last_change_unix_ms, 1_234, "idle adopts last-active");
assert!(out[0].resident);
assert_eq!(out[0].cwd, "/live/a", "live cwd is preserved");
assert_eq!(out[0].activity, RosterActivity::Idle);
}
#[test]
fn working_resident_keeps_now_but_adopts_title() {
let now = 9_000;
let out = merge_roster(
vec![resident("a", RosterActivity::Working, now)],
vec![summary("a", Some("Busy turn"), 1_234)],
);
assert_eq!(out[0].title.as_deref(), Some("Busy turn"));
assert_eq!(out[0].last_change_unix_ms, now, "Working stays 'now'");
}
#[test]
fn new_resident_without_summary_stays_titleless_now() {
let now = 9_000;
let out = merge_roster(vec![resident("a", RosterActivity::Idle, now)], vec![]);
assert_eq!(out[0].title, None);
assert_eq!(out[0].last_change_unix_ms, now);
}
#[test]
fn blank_persisted_title_leaves_row_untitled() {
let out = merge_roster(
vec![resident("a", RosterActivity::Idle, 9_000)],
vec![summary("a", Some(" "), 1_234)],
);
assert_eq!(out[0].title, None, "blank title normalizes to None");
assert_eq!(out[0].last_change_unix_ms, 1_234);
}
#[test]
fn dormant_sessions_are_emitted_and_sorted_after_residents() {
let out = merge_roster(
vec![resident("live", RosterActivity::Idle, 5_000)],
vec![
summary("live", Some("Live one"), 4_000),
summary("old", Some("Dormant one"), 1_000),
summary("new", Some("Newer dormant"), 8_000),
],
);
let ids: Vec<&str> = out.iter().map(|e| e.session_id.as_str()).collect();
assert_eq!(
ids,
vec!["new", "live", "old"],
"sorted by last-change desc"
);
let dormant = out.iter().find(|e| e.session_id == "new").unwrap();
assert_eq!(dormant.activity, RosterActivity::Dormant);
assert!(!dormant.resident);
}
#[test]
fn duplicate_summaries_are_deduped() {
let out = merge_roster(
vec![],
vec![
summary("dup", Some("First"), 1_000),
summary("dup", Some("Second"), 2_000),
],
);
assert_eq!(out.len(), 1, "duplicate ids collapse to one row");
}
#[test]
fn hidden_summaries_are_excluded() {
let mut hidden = summary("sub", Some("Subagent"), 5_000);
hidden.session_kind = Some("subagent".into());
let out = merge_roster(vec![], vec![hidden]);
assert!(out.is_empty(), "hidden/subagent summaries are dropped");
}
#[test]
fn dormant_row_carries_persisted_reasoning_effort() {
let mut s = summary("dorm", Some("Dormant"), 1_000);
s.reasoning_effort = Some(ReasoningEffort::Xhigh);
let out = merge_roster(vec![], vec![s]);
assert_eq!(out[0].reasoning_effort, Some(ReasoningEffort::Xhigh));
}
#[test]
fn resident_effort_is_taken_from_the_live_row_not_the_summary() {
// The live handle is authoritative for a resident session, so the
// resident row's effort must survive the summary backfill.
let mut live = resident("a", RosterActivity::Idle, 9_000);
live.reasoning_effort = Some(ReasoningEffort::High);
let mut s = summary("a", Some("Title"), 1_234);
s.reasoning_effort = Some(ReasoningEffort::Low);
let out = merge_roster(vec![live], vec![s]);
assert_eq!(out.len(), 1);
assert_eq!(out[0].reasoning_effort, Some(ReasoningEffort::High));
}
#[test]
fn reasoning_effort_serializes_as_camel_case_and_skips_when_none() {
let with_effort = RosterEntry {
reasoning_effort: Some(ReasoningEffort::Xhigh),
..resident("a", RosterActivity::Idle, 1)
};
let json = serde_json::to_string(&with_effort).unwrap();
assert!(
json.contains("\"reasoningEffort\":\"xhigh\""),
"effort must be camelCase and snake_case-valued: {json}"
);
let without = resident("b", RosterActivity::Idle, 1);
let json = serde_json::to_string(&without).unwrap();
assert!(
!json.contains("reasoningEffort"),
"a None effort must not be serialized: {json}"
);
}
}