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,479 @@
//! System-reminder injection concern for `SessionActor`: reminder policy,
//! the TodoGate, date/interrupt reminders, and between-turn completion
//! reminders.
use super::*;
/// Owned snapshot returned by [`SessionActor::collect_todo_gate_input`].
///
/// The borrowed `TodoGateInput<'_>` consumed by [`evaluate_todo_gate`]
/// is built from this owned data via [`Self::as_input`], which performs
/// the "first N in_progress are backed" insertion-order partition.
///
/// Exposed as `pub` solely so the replay-trace integration test in
/// `tests/trace_replay.rs` can drive the gate against synthetic JSON
/// fixtures. Not part of the public API.
#[doc(hidden)]
pub struct CollectedTodoGateInput {
/// Pairs of `(id, content, status)` in `TodoState.todo_items_with_ids()`
/// (insertion) order — `IndexMap` preserves this so the partition
/// between backed and unbacked in-progress items is deterministic.
pub todos: Vec<(String, String, crate::tools::todo::TodoStatus)>,
/// `|outstanding subagents| + |incomplete bash/monitor tasks|` at
/// the moment of gate evaluation.
pub backing_task_count: usize,
}
impl CollectedTodoGateInput {
/// Borrowed view used by [`evaluate_todo_gate`]. Pure transformation —
/// no I/O, no clones (besides the borrow into `&str`).
///
/// Allocates exactly two `Vec<&str>`s: one for pending, one for
/// in-progress (which is then split in place via `split_off` —
/// the leading slice is reused as `in_progress_backed`, no extra
/// allocation).
pub fn as_input(&self) -> TodoGateInput<'_> {
use crate::tools::todo::TodoStatus;
let mut pending = Vec::new();
let mut in_progress: Vec<&str> = Vec::new();
for (_, content, status) in &self.todos {
match status {
TodoStatus::Pending => pending.push(content.as_str()),
TodoStatus::InProgress => in_progress.push(content.as_str()),
TodoStatus::Completed | TodoStatus::Cancelled => {}
}
}
let backed_count = in_progress.len().min(self.backing_task_count);
let in_progress_unbacked = in_progress.split_off(backed_count);
let in_progress_backed = in_progress;
TodoGateInput {
pending,
in_progress_unbacked,
in_progress_backed,
backing_task_count: self.backing_task_count,
}
}
}
/// Inputs to `evaluate_todo_gate`. All fields are deliberately owned
/// borrows from the gate's call-site so the helper is a pure function.
///
/// The struct itself is `pub` (with `#[doc(hidden)]`) only so the
/// replay-trace integration test in `tests/trace_replay.rs` can name
/// the type as `&TodoGateInput<'_>` when calling `evaluate_todo_gate`.
/// Fields stay crate-private — the test never constructs the struct
/// directly; it obtains an instance via `CollectedTodoGateInput::as_input()`.
#[doc(hidden)]
pub struct TodoGateInput<'a> {
pub(super) pending: Vec<&'a str>,
pub(super) in_progress_unbacked: Vec<&'a str>,
pub(super) in_progress_backed: Vec<&'a str>,
pub(super) backing_task_count: usize,
}
impl TodoGateReason {
/// Wire-string form, byte-identical to a `TODO_GATE_*` const in
/// `crate::session::events`.
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::InFlight => crate::session::events::TODO_GATE_IN_FLIGHT,
}
}
}
/// Pure decision function: does the gate fire, and with what reminder?
///
/// The function does NOT consult the cap — the caller folds the cap check
/// in around this function. Keeping cap logic out makes `evaluate_todo_gate`
/// trivially testable.
///
/// Exposed as `pub` solely so the replay-trace integration test in
/// `tests/trace_replay.rs` can call the gate directly. Not part of the
/// public API.
#[doc(hidden)]
pub fn evaluate_todo_gate(input: &TodoGateInput<'_>) -> TodoGateDecision {
if input.pending.is_empty() && input.in_progress_unbacked.is_empty() {
return TodoGateDecision::Continue;
}
TodoGateDecision::Nudge {
reminder: build_todo_gate_reminder(&input.pending, &input.in_progress_unbacked),
reason: TodoGateReason::InFlight,
}
}
/// Build the in-flight TodoGate reminder text.
///
/// Uses the doubled-`${{{{ tools.by_kind.* }}}}` convention so the
/// caller's `format!` pass leaves a single `${{ tools.by_kind.* }}`
/// for `TemplateRenderer` / `render_prompt` to resolve into the
/// model-facing tool name.
pub(super) fn build_todo_gate_reminder(pending: &[&str], unbacked_in_progress: &[&str]) -> String {
use std::fmt::Write as _;
let mut buf =
String::from("You have outstanding todos but ended your turn without a tool call.\n\n");
if !unbacked_in_progress.is_empty() {
buf.push_str("In-progress (no backing background task):\n");
for c in unbacked_in_progress {
let _ = writeln!(buf, "- {c}");
}
buf.push('\n');
}
if !pending.is_empty() {
buf.push_str("Pending:\n");
for c in pending {
let _ = writeln!(buf, "- {c}");
}
buf.push('\n');
}
let _ = write!(
buf,
"Per <task_completion_discipline>, advance the next pending todo \
with the appropriate tool call NOW. If you have a genuine external \
blocker (missing credential, denied permission, network unreachable), \
state it explicitly AND mark the affected todos `cancelled` via \
${{{{ tools.by_kind.plan }}}} with a reason in the same turn."
);
buf
}
/// Resolve the runtime `ReminderPolicy` from the resolved inputs.
///
/// Precedence: CLI `--todo-gate` > remote `/settings` > built-in default
/// (which is disabled). Extracted from `spawn_session_actor` so the
/// precedence rules are unit-testable. Named `resolve_*` to match the
/// sibling precedence helpers in `crate::util::config`
/// (`resolve_zdr_access_enabled`, `resolve_restore_code`, …).
pub(crate) fn resolve_reminder_policy(
remote: Option<&crate::util::config::RemoteSettings>,
todo_gate: bool,
) -> kigi_agent::ReminderPolicy {
let mut policy = kigi_agent::ReminderPolicy::default();
if let Some(remote) = remote {
if let Some(enabled) = remote.todo_gate_enabled {
policy.todo_gate.enabled = enabled;
}
if let Some(cap) = remote.todo_gate_max_fires_per_prompt {
policy.todo_gate.max_fires_per_prompt = cap;
}
}
if todo_gate {
policy.todo_gate.enabled = true;
}
policy
}
/// Build the date-rollover reminder when the local calendar
/// date has advanced past the date last surfaced to the model.
///
/// Returns `None` when the date is unchanged (already announced) or has moved
/// backwards (e.g. a manual clock adjustment), so the caller injects nothing
/// in the common case. Pure (no `self`, no clock access) so the rollover
/// boundary logic is unit-testable; see `reminder_policy_tests`.
pub(crate) fn date_rollover_reminder(
today: chrono::NaiveDate,
last_announced: chrono::NaiveDate,
) -> Option<String> {
if today <= last_announced {
return None;
}
Some(format!(
"The local date has changed since this session started. Today's date is now \
{today}. The \"Today's date\" value shown in the <user_info> block above was set \
earlier in the session and is now stale; use {today} as the current date."
))
}
/// Body of the one-shot interrupt `<system-reminder>` injected on the next real
/// user turn after a mid-stream abort that left the model with no other signal.
/// Wrapped in grok's `<system-reminder>` shape by [`SessionActor::push_system_reminder`].
/// See [`SessionActor::maybe_inject_interrupt_reminder`].
pub(crate) const INTERRUPT_REMINDER: &str = "[Request interrupted by user]";
/// TodoGate when enabled and the prompt carries `<task_completion_discipline>`
/// (`{DISCIPLINE_BLOCK}`), but NOT while the goal loop is active — the
/// continuation directive drives the loop there (see the body).
pub(super) fn todo_gate_active(
policy: &kigi_agent::system_reminder::ReminderPolicy,
audience: kigi_agent::prompt::context::PromptAudience,
definition: &AgentDefinition,
goal_harness_enabled: bool,
goal_status: Option<crate::session::goal_tracker::GoalStatus>,
) -> bool {
if !policy.todo_gate.enabled {
return false;
}
if laziness_injection_active(goal_harness_enabled, goal_status) {
return false;
}
definition.carries_task_completion_discipline(audience)
}
impl SessionActor {
/// Date rollover for long-running sessions. When a session crosses a
/// local-midnight boundary the `Today's date` value stamped into the cached
/// `<user_info>` prefix goes stale (the prefix is only re-stamped on
/// compaction / resume, to preserve the prompt cache). Detect the change
/// and inject a one-shot `<system-reminder>` announcing the new date.
///
/// Self-dedupes via `last_announced_local_date`, so it fires at most once
/// per calendar day regardless of how many turns occur. Skipped when the
/// active template manages this surface elsewhere.
pub(super) async fn maybe_inject_date_rollover_reminder(&self) {
let today = chrono::Local::now().date_naive();
let last = self.last_announced_local_date.get();
let Some(reminder) = date_rollover_reminder(today, last) else {
return;
};
self.last_announced_local_date.set(today);
self.push_system_reminder(&reminder);
tracing::debug!(
previous = % last, today = % today, "Injected date rollover reminder"
);
}
/// Inject a one-shot `<system-reminder>` telling the model its previous turn
/// was interrupted mid-stream, when nothing else will (no in-flight tool to
/// repair into a "cancelled" tool-result, no permission tool-result). The
/// flag is armed by [`Self::cancel_running_task`] only on the no-active-tool
/// abort path, and is consumed exactly once (caller gates to real user
/// prompts). Skipped when the active template manages this surface
/// elsewhere, matching [`Self::maybe_inject_date_rollover_reminder`].
pub(super) async fn maybe_inject_interrupt_reminder(&self) {
if !self.events.take_pending_interrupt_reminder() {
return;
}
self.push_system_reminder(INTERRUPT_REMINDER);
tracing::debug!("Injected prior-turn interrupt reminder");
}
/// Push a `<system-reminder>`-wrapped user message into the conversation.
pub(super) fn push_system_reminder(&self, content: &str) {
self.push_system_reminder_with_tag(content, "system-reminder");
}
/// The active reminder wrapper tag, backed by the canonical tag constants
/// in `kigi_tools::reminders`.
pub(super) fn reminder_wrapper_tag(&self) -> &'static str {
kigi_tools::reminders::DEFAULT_REMINDER_TAG
}
/// Push a `<{tag}>`-wrapped user message.
pub(super) fn push_system_reminder_with_tag(&self, content: &str, tag: &str) {
let message = ConversationItem::system_reminder(format!("<{tag}>\n{content}\n</{tag}>"));
self.chat_state_handle.push_user_message(message);
}
/// Mark completion IDs as reported in the shared
/// `ReportedTaskCompletions` state so the per-tool-call
/// `TaskCompletionReminder` won't (re-)surface them. Used both to dedupe
/// completions the model already saw (notification-drain / auto-wake
/// prompts) and to drop them during the goal loop (between-turn drain).
/// No-op on an empty list.
pub(super) async fn mark_completions_reported(&self, ids: &[&str]) {
if ids.is_empty() {
return;
}
use kigi_tools::reminders::task_completion::ReportedTaskCompletions;
use kigi_tools::types::resources::State;
let bridge = self.agent.borrow().tool_bridge().clone();
let resources = bridge.shared_resources().await;
let mut res = resources.lock().await;
let reported = res.get_or_default::<State<ReportedTaskCompletions>>();
for id in ids {
reported.mark_reported(id);
}
}
/// Drain background completions (bash tasks + subagents) that arrived
/// while the model was idle and inject them as system-reminders so the
/// model sees them at turn start.
///
/// While the goal loop is active the completions are DROPPED instead:
/// the continuation directive is the sole driver, and an async "task /
/// subagent completed" reminder can derail a weak model (e.g.
/// relaunching a killed server). They are still drained (consumed) and
/// marked reported so nothing accumulates to surface on a later turn.
///
/// Surfacing (the `push_system_reminder` calls) is suppressed when the
/// active template handles this elsewhere, but the goal-loop drain is not:
/// with the goal loop active, completions are still consumed and marked
/// reported so nothing surfaces later.
pub(super) async fn drain_between_turn_completions(&self) {
let goal_loop_active = self.goal_loop_active();
let bridge = self.agent.borrow().tool_bridge().clone();
let bash_completions = bridge.drain_between_turn_bash_completions().await;
if !bash_completions.is_empty() {
let ids: Vec<&str> = bash_completions
.iter()
.map(|t| t.task_id.as_str())
.collect();
if goal_loop_active {
tracing::info!(
count = bash_completions.len(), task_ids = ? ids,
"dropping between-turn bash task completions (goal loop active)"
);
self.mark_completions_reported(&ids).await;
} else {
tracing::info!(
count = bash_completions.len(), task_ids = ? ids,
"draining between-turn bash task completions"
);
let task_output_name =
kigi_tools::reminders::task_completion::resolve_task_output_tool_name(&bridge)
.await;
let read_tool_name =
kigi_tools::reminders::task_completion::resolve_read_tool_name(&bridge).await;
let reminder =
kigi_tools::reminders::task_completion::format_between_turn_bash_completions(
&bash_completions,
task_output_name.as_deref(),
read_tool_name.as_deref(),
);
self.push_system_reminder(&reminder);
}
}
let Some(tx) = &self.tool_context.subagent_event_tx else {
return;
};
use kigi_tools::implementations::grok_build::task::types::{
SubagentCompletionsRequest, SubagentEvent,
};
let suppress_ids = self
.tool_context
.auto_wake_delivered
.as_ref()
.map(|a| a.snapshot())
.unwrap_or_default();
let (respond_to, rx) = tokio::sync::oneshot::channel();
if tx
.send(SubagentEvent::Completions(SubagentCompletionsRequest {
suppress_ids,
respond_to,
}))
.is_err()
{
return;
}
let Ok(completions) = rx.await else {
return;
};
if completions.is_empty() {
return;
}
let ids: Vec<&str> = completions.iter().map(|c| c.subagent_id.as_str()).collect();
if goal_loop_active {
tracing::info!(
count = completions.len(), subagent_ids = ? ids,
"dropping between-turn subagent completions (goal loop active)"
);
self.mark_completions_reported(&ids).await;
return;
}
tracing::info!(
count = completions.len(), subagent_ids = ? ids,
"draining between-turn subagent completions"
);
let reminder =
kigi_tools::reminders::task_completion::format_between_turn_completion_reminder(
&completions,
&bridge,
)
.await;
self.push_system_reminder(&reminder);
}
/// Persist a manifest of running background tasks to the session directory.
///
/// Called during session shutdown (both explicit and channel-closed paths)
/// so a resumed session can inform the model about processes that were
/// still alive when the session ended.
pub(super) async fn persist_background_task_manifest(&self) {
let tasks = self
.agent
.borrow()
.tool_bridge()
.list_background_tasks()
.await;
let entries: Vec<crate::terminal::BackgroundTaskManifestEntry> = tasks
.into_iter()
.filter(|t| !t.completed)
.map(|t| crate::terminal::BackgroundTaskManifestEntry {
task_id: t.task_id,
command: t.command,
display_command: t.display_command,
output_file: t.output_file,
start_time: t.start_time,
cwd: t.cwd,
kind: t.kind,
})
.collect();
if !entries.is_empty() {
tracing::info!(
count = entries.len(),
"persisting background task manifest for session resume"
);
}
let session_dir = crate::session::persistence::session_dir(&self.session_info);
crate::terminal::persist_manifest(&session_dir, entries);
}
/// Load the background task manifest from a prior session and inject a
/// system-reminder so the model knows about orphaned tasks.
///
/// The manifest file is deleted after loading so it is only shown once.
pub(super) fn inject_resumed_tasks_reminder(&self) {
let session_dir = crate::session::persistence::session_dir(&self.session_info);
let entries = crate::terminal::load_and_clear_manifest(&session_dir);
if entries.is_empty() {
return;
}
tracing::info!(
count = entries.len(),
"injecting resumed background tasks reminder"
);
let reminder = crate::terminal::format_resumed_tasks_reminder(&entries);
self.push_system_reminder(&reminder);
}
/// Turn-end TodoGate config, or `None` when [`todo_gate_active`] is false.
pub(super) fn todo_gate_policy(&self) -> Option<kigi_agent::system_reminder::TodoGateConfig> {
let goal_status = self.goal_tracker.lock().status();
let agent = self.agent.borrow();
let policy = agent.reminder_policy();
let active = todo_gate_active(
policy,
agent.prompt_audience(),
agent.definition(),
self.goal_harness_enabled(),
goal_status,
);
tracing::debug!(
enabled = policy.todo_gate.enabled,
goal_harness_enabled = self.goal_harness_enabled(),
?goal_status,
active,
"todo_gate_policy"
);
if !active {
return None;
}
Some(policy.todo_gate)
}
/// Gather the inputs needed by `evaluate_todo_gate` from live session
/// state.
///
/// Each `.await` is preceded by an owned `Arc<ToolBridge>` clone
/// from `tool_bridge_handle()` — no `RefCell::Ref<Agent>` guard is
/// held across a suspension point.
pub(super) async fn collect_todo_gate_input(&self, prompt_id: &str) -> CollectedTodoGateInput {
use crate::tools::todo::{TodoState, TodoStatus};
use kigi_tools::types::resources::State;
let bridge = self.tool_bridge_handle();
let todos: Vec<(String, String, TodoStatus)> = bridge
.read_resource::<State<TodoState>>()
.await
.map(|state| {
state
.0
.todo_items_with_ids()
.map(|(id, item)| (id.clone(), item.content.clone(), item.status))
.collect()
})
.unwrap_or_default();
let outstanding_live = self
.outstanding_reply_for_prompt(prompt_id)
.await
.map(|r| r.live_ids.len())
.unwrap_or(0);
let incomplete_terminal_tasks = bridge
.list_background_tasks()
.await
.into_iter()
.filter(kigi_tools::computer::types::TaskSnapshot::is_outstanding)
.count();
let backing_task_count = outstanding_live + incomplete_terminal_tasks;
CollectedTodoGateInput {
todos,
backing_task_count,
}
}
}