The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok' crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party license archives, README provenance, and the required 'Based on Grok Build Open Source' attribution, now sourced from version_attribution.txt). Wire-visible renames (both sides in this repo, changed in lockstep): - Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode). - Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* / _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps a read-side alias for the legacy '_x.ai/session/update' method so existing updates.jsonl histories load; writes emit only the new name (both directions test-pinned). - Agent types grok-build* → kigi* with a documented legacy-prefix alias at resolution time so persisted sessions keep resolving. - ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build implementation dirs renamed to kigi*. - x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes grokday/groknight → kigiday/kiginight (old persisted values fall back to the default theme), web_fetch allowlist xAI hosts → kimi.com + moonshot platforms, changelog CDN → this repo, grok-build changelog archives deleted. - BYOK default endpoint removed: [endpoints] api_base_url is now truly optional with NO default — consumers fail fast with the flag name when unset (no silent x.ai egress). Mock harnesses inject it explicitly. - System-prompt identity fixed: 'released by xAI' → 'an unofficial community CLI for Kimi' (template + regenerated encrypted form). Also repaired pre-existing grok-era test debt found by the sweep: the stale trace_classify default-model pin, the grok-pager UA label test, pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY test could previously reach the real api.moonshot.cn), and the outdated oauth fixture scope key. Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings); FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed; deny advisories ok.
200 lines
6.9 KiB
Rust
200 lines
6.9 KiB
Rust
use std::path::Path;
|
|
|
|
use kigi_workspace::session::git::VcsKind;
|
|
|
|
// Re-export from kigi-chat-state — canonical definition lives there.
|
|
pub use kigi_chat_state::compaction_utils::extract_user_query;
|
|
|
|
/// Wraps the user query properly
|
|
pub fn user_query(user_message: String) -> String {
|
|
format!(
|
|
r#"<user_query>
|
|
{user_message}
|
|
</user_query>"#
|
|
)
|
|
}
|
|
|
|
/// Environment info for constructing the `<user_info>` block.
|
|
///
|
|
/// When `None`, values are read from the local machine. When `Some`,
|
|
/// the provided values are used (e.g. from a remote workspace via
|
|
/// `workspace.info` RPC).
|
|
pub struct UserInfoOverride {
|
|
pub os: String,
|
|
pub shell: String,
|
|
pub cwd: String,
|
|
}
|
|
|
|
/// Minimal user message prefix for fast-start / headless contexts.
|
|
///
|
|
/// Intentionally excludes workspace snapshot and git status.
|
|
/// When `override_info` is provided, uses remote workspace info instead
|
|
/// of local machine introspection.
|
|
pub fn construct_user_message_minimal(
|
|
working_directory: &Path,
|
|
override_info: Option<&UserInfoOverride>,
|
|
) -> String {
|
|
let local_shell;
|
|
let (os, shell, cwd) = match override_info {
|
|
Some(info) => (info.os.as_str(), info.shell.as_str(), info.cwd.clone()),
|
|
None => {
|
|
local_shell = resolve_shell_display();
|
|
(
|
|
std::env::consts::OS,
|
|
local_shell.as_str(),
|
|
working_directory.to_string_lossy().to_string(),
|
|
)
|
|
}
|
|
};
|
|
// Local-timezone date, captured when the prefix is built. Re-stamped on
|
|
// compaction and on resume (build_user_message_prefix), so it stays current
|
|
// across long sessions.
|
|
let today = chrono::Local::now().format("%Y-%m-%d");
|
|
format!(
|
|
r#"<user_info>
|
|
OS Version: {os}
|
|
Shell: {shell}
|
|
Workspace Path: {cwd}
|
|
Today's date: {today}
|
|
Note: Prefer using relative paths over absolute paths as tool call args when possible.
|
|
</user_info>"#,
|
|
)
|
|
}
|
|
|
|
/// Resolve a display string for the user's shell.
|
|
///
|
|
/// Unix: full path from `$SHELL` (e.g. `/bin/zsh`).
|
|
/// Windows: detected via `detect_windows_shell` cascade
|
|
/// (pwsh > powershell.exe > Git Bash > cmd.exe).
|
|
fn resolve_shell_display() -> String {
|
|
#[cfg(unix)]
|
|
{
|
|
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
{
|
|
kigi_config::shell::detect_windows_shell()
|
|
.name()
|
|
.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn format_vcs_status_block(status: &str, vcs_kind: VcsKind) -> String {
|
|
let (tag, description) = if vcs_kind.is_jj() {
|
|
(
|
|
"jj_status",
|
|
"This is the Jujutsu (jj) status at the start of the conversation. This is a \
|
|
jj-managed repository \u{2014} use `jj` commands instead of `git`. There is no staging \
|
|
area; all changes are part of the working-copy commit (@). Use `jj describe` to \
|
|
set commit messages and `jj new` to finalize changes.",
|
|
)
|
|
} else {
|
|
(
|
|
"git_status",
|
|
"This is the git status at the start of the conversation. Note that this status \
|
|
is a snapshot in time, and will not update during the conversation.",
|
|
)
|
|
};
|
|
format!("\n\n<{tag}>\n{description}\n{status}\n</{tag}>\n")
|
|
}
|
|
|
|
/// Compute the VCS status block (without the `<user_info>` wrapper).
|
|
pub async fn compute_vcs_status_block(
|
|
working_directory: &Path,
|
|
vcs_kind: VcsKind,
|
|
) -> Option<String> {
|
|
use kigi_workspace::file_system::{git_status, jj_status};
|
|
|
|
if matches!(vcs_kind, VcsKind::None) {
|
|
return None;
|
|
}
|
|
let _timer = crate::instrumentation_timer!("session.user_prefix.vcs_status");
|
|
let timeout = std::time::Duration::from_secs(2);
|
|
let result = if vcs_kind.is_jj() {
|
|
tokio::time::timeout(timeout, jj_status(working_directory)).await
|
|
} else {
|
|
tokio::time::timeout(timeout, git_status(working_directory)).await
|
|
};
|
|
match result {
|
|
Ok(Ok(status)) => Some(format_vcs_status_block(&status, vcs_kind)),
|
|
Ok(Err(e)) => {
|
|
tracing::warn!("user prefix VCS status failed: {e}");
|
|
None
|
|
}
|
|
Err(_) => {
|
|
tracing::warn!(vcs = ?vcs_kind, "user prefix VCS status timed out after 2s");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Full user message prefix: `<user_info>` + VCS status.
|
|
///
|
|
/// When `override_info` is provided, uses remote workspace info and
|
|
/// `vcs_status_override` (pre-fetched from the remote workspace) instead
|
|
/// of local introspection.
|
|
pub async fn construct_user_message(
|
|
working_directory: &Path,
|
|
vcs_kind: VcsKind,
|
|
override_info: Option<&UserInfoOverride>,
|
|
vcs_status_override: Option<String>,
|
|
) -> String {
|
|
let cwd = working_directory.to_string_lossy().to_string();
|
|
let vcs_block = if let Some(status) = vcs_status_override {
|
|
Some(format_vcs_status_block(&status, vcs_kind))
|
|
} else {
|
|
let (block, elapsed_ms) = crate::timed!({
|
|
let mut timer = crate::instrumentation_timer!("session.user_prefix");
|
|
timer.with_field("cwd", cwd.as_str());
|
|
compute_vcs_status_block(working_directory, vcs_kind).await
|
|
});
|
|
tracing::debug!(elapsed_ms = elapsed_ms as u64, "startup: user_prefix");
|
|
block
|
|
};
|
|
let mut user_info = construct_user_message_minimal(working_directory, override_info);
|
|
if let Some(vcs) = vcs_block {
|
|
user_info.push_str(&vcs);
|
|
}
|
|
user_info
|
|
}
|
|
|
|
// Tests for extract_user_query now live in kigi_chat_state::compaction_utils.
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use kigi_workspace::file_system::FsError;
|
|
|
|
/// Verify that construct_user_message completes within the 2s git_status
|
|
/// timeout even when pointed at a non-existent directory (git commands
|
|
/// fail instantly → no timeout path exercised, but validates the happy
|
|
/// path doesn't regress).
|
|
#[tokio::test]
|
|
async fn construct_user_message_returns_without_git_status_on_bad_dir() {
|
|
let dir = std::path::Path::new("/tmp/nonexistent-kigi-test-dir");
|
|
let msg = construct_user_message(dir, VcsKind::Git, None, None).await;
|
|
// user_info block is always present
|
|
assert!(
|
|
msg.contains("<user_info>"),
|
|
"must contain user_info section"
|
|
);
|
|
// git_status block should be absent (git commands fail on non-repo)
|
|
assert!(
|
|
!msg.contains("<git_status>"),
|
|
"must not contain git_status for non-repo directory"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_status_error_omits_block() {
|
|
// Simulate what construct_user_message does when git_status returns Err
|
|
let git_status_res: Result<String, FsError> = Err(FsError::Other("timed out".into()));
|
|
let mut user_info = "<user_info>test</user_info>".to_string();
|
|
if let Ok(git_status) = git_status_res {
|
|
user_info = format!("{user_info}\n<git_status>{git_status}</git_status>");
|
|
}
|
|
assert!(!user_info.contains("<git_status>"));
|
|
}
|
|
}
|