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:
@@ -0,0 +1,48 @@
|
||||
//! Cross-cutting reminder: notifies LSP of file changes and drains diagnostics.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::implementations::lsp::LspBackend;
|
||||
use crate::types::output::{SearchReplaceOutput, ToolOutput};
|
||||
use crate::types::resources::SharedResources;
|
||||
use crate::types::tool::Reminder;
|
||||
|
||||
pub struct LspDiagnosticsReminder;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Reminder for LspDiagnosticsReminder {
|
||||
async fn collect_reminders(
|
||||
&self,
|
||||
resources: SharedResources,
|
||||
tool_output: &ToolOutput,
|
||||
) -> Vec<String> {
|
||||
let lsp = {
|
||||
let res = resources.lock().await;
|
||||
match res.get::<Arc<dyn LspBackend>>() {
|
||||
Some(h) => h.clone(),
|
||||
None => return vec![],
|
||||
}
|
||||
};
|
||||
|
||||
lsp.ensure_started_background();
|
||||
|
||||
// After SearchReplace edits, notify LSP so diagnostics refresh.
|
||||
// The adapter routes immediately when ready and buffers pre-ready edits otherwise.
|
||||
if let ToolOutput::SearchReplace(SearchReplaceOutput::EditsApplied(edits)) = tool_output
|
||||
&& let Ok(content) = std::fs::read_to_string(&edits.absolute_path)
|
||||
{
|
||||
lsp.notify_file_changed(&edits.absolute_path, &content)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Drain any pending diagnostics (from this or previous edits).
|
||||
if let Some(summary) = lsp
|
||||
.drain_diagnostics(std::time::Duration::from_millis(500))
|
||||
.await
|
||||
{
|
||||
return vec![summary.text];
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Cross-cutting reminders for tool outputs.
|
||||
//!
|
||||
//! Provides contextual hints wrapped in `<system-reminder>` tags that are
|
||||
//! appended to tool outputs before being sent to the model.
|
||||
//!
|
||||
//! Two categories of reminders:
|
||||
//! - **Per-tool reminders**: each tool implements the `Reminder` trait to
|
||||
//! emit reminders based on its output (e.g., empty file warning).
|
||||
//! - **Cross-cutting reminders**: standalone structs registered on the
|
||||
//! registry that fire after every tool call.
|
||||
//!
|
||||
//! This module contains the cross-cutting reminders:
|
||||
//! - [`LspDiagnosticsReminder`], [`SkillDiscoveryReminder`], [`TaskCompletionReminder`]
|
||||
//!
|
||||
//! All reminders are collected and appended in `call_new_tool()`.
|
||||
|
||||
pub mod lsp_diagnostics;
|
||||
pub mod skill_discovery;
|
||||
pub mod task_completion;
|
||||
|
||||
pub use lsp_diagnostics::LspDiagnosticsReminder;
|
||||
pub use skill_discovery::SkillDiscoveryReminder;
|
||||
pub use task_completion::TaskCompletionReminder;
|
||||
|
||||
/// The default system-reminder tag name (hyphen).
|
||||
pub const DEFAULT_REMINDER_TAG: &str = "system-reminder";
|
||||
|
||||
/// Wrap plain text in `<system-reminder>` tags (default hyphen variant).
|
||||
///
|
||||
/// Input: `"Some reminder text"`
|
||||
/// Output: `"<system-reminder>\nSome reminder text\n</system-reminder>"`
|
||||
pub fn wrap_reminder(text: &str) -> String {
|
||||
wrap_reminder_with_tag(text, DEFAULT_REMINDER_TAG)
|
||||
}
|
||||
|
||||
/// Wrap plain text in a configurable reminder wrapper.
|
||||
///
|
||||
/// Use [`DEFAULT_REMINDER_TAG`] unless the harness requires a different
|
||||
/// tag name (harness-specific tags live with the harness crate).
|
||||
pub fn wrap_reminder_with_tag(text: &str, tag: &str) -> String {
|
||||
format!("<{tag}>\n{text}\n</{tag}>")
|
||||
}
|
||||
|
||||
/// Frame a scheduled task prompt with `<system-reminder>` context for the model.
|
||||
///
|
||||
/// The raw `prompt` is what the user wrote in `/loop`; this wrapping tells
|
||||
/// the model the message is a recurring task execution so it executes
|
||||
/// rather than questioning the prompt. The UI shows the raw prompt text;
|
||||
/// only the model receives this framed version.
|
||||
pub fn format_scheduled_task_prompt(prompt: &str, task_id: &str, human_schedule: &str) -> String {
|
||||
format!(
|
||||
"<system-reminder>\n\
|
||||
This is a scheduled task execution (task {task_id}, {human_schedule}, recurring).\n\
|
||||
Execute the prompt below. Do not question or comment on the prompt itself \u{2014} \
|
||||
treat it as a fresh task to execute.\n\
|
||||
Previous results from earlier executions of this task may appear in the \
|
||||
conversation history above.\n\
|
||||
</system-reminder>\n\
|
||||
\n\
|
||||
{prompt}"
|
||||
)
|
||||
}
|
||||
|
||||
/// Append wrapped reminders to a tool output string.
|
||||
/// Returns output unchanged if reminders is empty.
|
||||
///
|
||||
/// Each reminder is individually wrapped via
|
||||
/// [`wrap_reminder_with_tag`] using the given `tag`, then all are joined
|
||||
/// with `"\n\n"` and appended to the output with a `"\n\n"` separator.
|
||||
///
|
||||
/// Use [`DEFAULT_REMINDER_TAG`] unless the harness requires a different
|
||||
/// tag name.
|
||||
pub fn format_with_reminders(output: String, reminders: Vec<String>, tag: &str) -> String {
|
||||
if reminders.is_empty() {
|
||||
return output;
|
||||
}
|
||||
let wrapped: Vec<String> = reminders
|
||||
.iter()
|
||||
.map(|r| wrap_reminder_with_tag(r, tag))
|
||||
.collect();
|
||||
let joined = wrapped.join("\n\n");
|
||||
if output.is_empty() {
|
||||
joined
|
||||
} else {
|
||||
format!("{}\n\n{}", output, joined)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wrap_reminder_adds_tags() {
|
||||
let result = wrap_reminder("Some reminder text");
|
||||
assert_eq!(
|
||||
result,
|
||||
"<system-reminder>\nSome reminder text\n</system-reminder>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_with_reminders_wraps_and_appends() {
|
||||
let output = "file content here".to_string();
|
||||
let reminders = vec![
|
||||
"File is empty.".to_string(),
|
||||
"File was created by you.".to_string(),
|
||||
];
|
||||
let result = format_with_reminders(output, reminders, DEFAULT_REMINDER_TAG);
|
||||
assert!(result.starts_with("file content here\n\n"));
|
||||
assert!(result.contains("<system-reminder>\nFile is empty.\n</system-reminder>"));
|
||||
assert!(result.contains("<system-reminder>\nFile was created by you.\n</system-reminder>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_with_reminders_custom_tag() {
|
||||
let output = "file content here".to_string();
|
||||
let reminders = vec!["File is empty.".to_string()];
|
||||
let result = format_with_reminders(output, reminders, "custom_reminder");
|
||||
assert!(result.contains("<custom_reminder>\nFile is empty.\n</custom_reminder>"));
|
||||
assert!(!result.contains("<system-reminder>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_scheduled_task_prompt_includes_framing() {
|
||||
let out = format_scheduled_task_prompt("do stuff", "task-1", "every 5m");
|
||||
assert!(out.starts_with("<system-reminder>"));
|
||||
assert!(out.contains("task task-1"));
|
||||
assert!(out.contains("every 5m"));
|
||||
assert!(out.contains("do stuff"));
|
||||
assert!(
|
||||
!out.contains("<user_query>"),
|
||||
"must not add <user_query> — shell does that"
|
||||
);
|
||||
assert!(out.ends_with("do stuff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_with_reminders_returns_unchanged_when_empty() {
|
||||
let output = "file content here".to_string();
|
||||
let result = format_with_reminders(output.clone(), vec![], DEFAULT_REMINDER_TAG);
|
||||
assert_eq!(result, output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
//! Skill discovery reminder — discovers new skills near accessed paths.
|
||||
//!
|
||||
//! Contains `SkillDiscoveryReminder`, a cross-cutting `Reminder` that fires
|
||||
//! after every tool call to check for SKILL.md files in `.kigi/skills/`,
|
||||
//! `.agents/skills/`, or `.claude/skills/` directories near the accessed path.
|
||||
//!
|
||||
//! The actual tracking logic lives in
|
||||
//! `types::skill_discovery_tracker::SkillDiscoveryTracker`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Directories that contain skill definitions (`.kigi/skills/`, `.agents/skills/`,
|
||||
/// `.claude/skills/`, `.cursor/skills/`). Shared between startup skill discovery
|
||||
/// and runtime `SkillDiscoveryReminder`.
|
||||
pub const SKILL_CONFIG_DIRS: &[&str] = &[".kigi", ".agents", ".claude", ".cursor"];
|
||||
|
||||
use crate::implementations::skills::discovery;
|
||||
use crate::implementations::skills::types::SkillScope;
|
||||
use crate::types::output::{
|
||||
ApplyPatchOutput, ListDirOutput, ReadFileOutput, SearchReplaceOutput, ToolOutput,
|
||||
};
|
||||
use crate::types::requirements::{Expr, ToolRequirement};
|
||||
use crate::types::resources::SharedResources;
|
||||
use crate::types::skill_discovery_tracker::SkillManager;
|
||||
use crate::types::tool::{Reminder, ToolKind};
|
||||
|
||||
/// Cross-cutting reminder that discovers skills in subdirectories
|
||||
/// near filesystem paths accessed by tools.
|
||||
///
|
||||
/// **Concise mode limitation (V1):** This reminder is globally disabled when
|
||||
/// `SystemRemindersEnabled(false)` is set (concise mode). This means dynamic
|
||||
/// skill discovery will NOT fire in concise mode. This is an intentional V1
|
||||
/// layering compromise — discovery is coupled to the Reminder delivery
|
||||
/// mechanism for expediency. If concise-mode support is later needed, migrate
|
||||
/// to a dedicated post-tool-call hook that is NOT gated by
|
||||
/// `SystemRemindersEnabled`.
|
||||
///
|
||||
/// Reacts to `ReadFile`, `ListDir`, and `SearchReplace` outputs by
|
||||
/// extracting the filesystem path the tool accessed, walking up toward
|
||||
/// cwd checking for skill directories, and emitting a reminder for any
|
||||
/// newly discovered skills.
|
||||
///
|
||||
/// This is a standalone struct — not attached to any specific tool.
|
||||
/// Register it alongside tools so it runs after every tool call.
|
||||
pub struct SkillDiscoveryReminder;
|
||||
|
||||
impl SkillDiscoveryReminder {
|
||||
/// Extract the filesystem path the tool accessed from the output.
|
||||
///
|
||||
/// Returns `None` for tools that don't operate on filesystem paths,
|
||||
/// or for error variants (no reliable path to extract).
|
||||
fn extract_target_path(tool_output: &ToolOutput) -> Option<&Path> {
|
||||
match tool_output {
|
||||
ToolOutput::ReadFile(ReadFileOutput::FileContent(fc)) => Some(&fc.absolute_path),
|
||||
ToolOutput::ListDir(ListDirOutput::Content(content)) => {
|
||||
Some(&content.absolute_root_path)
|
||||
}
|
||||
ToolOutput::SearchReplace(SearchReplaceOutput::EditsApplied(r)) => {
|
||||
Some(&r.absolute_path)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Files the tool touched, for activating `paths:`-gated skills — including
|
||||
/// every file of a multi-file `apply_patch`. Bash/grep paths are excluded
|
||||
/// (unparseable / incidental).
|
||||
fn extract_activation_paths(tool_output: &ToolOutput) -> Vec<PathBuf> {
|
||||
match tool_output {
|
||||
ToolOutput::ApplyPatch(ApplyPatchOutput::Success { files, .. }) => files
|
||||
.iter()
|
||||
.flat_map(|f| std::iter::once(f.path.clone()).chain(f.move_to.clone()))
|
||||
.collect(),
|
||||
other => Self::extract_target_path(other)
|
||||
.map(Path::to_path_buf)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a SKILL.md path is inside a supported skills directory
|
||||
/// (`.kigi/skills/`, `.agents/skills/`, or `.claude/skills/`).
|
||||
fn is_in_supported_skills_dir(path: &Path) -> bool {
|
||||
for ancestor in path.ancestors().skip(1) {
|
||||
if ancestor.file_name().is_some_and(|n| n == "skills") {
|
||||
return ancestor
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.is_some_and(|n| SKILL_CONFIG_DIRS.iter().any(|d| *d == n));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Reminder for SkillDiscoveryReminder {
|
||||
fn requires_expr(&self) -> Expr<ToolRequirement> {
|
||||
// Finalization-time check: "at least one path-producing tool exists."
|
||||
// At runtime, collect_reminders fires after every tool call regardless
|
||||
// — output pattern-matching does the actual filtering.
|
||||
Expr::Or(vec![
|
||||
Expr::Value(ToolRequirement::tool_kind(ToolKind::Read)),
|
||||
Expr::Value(ToolRequirement::tool_kind(ToolKind::Edit)),
|
||||
Expr::Value(ToolRequirement::tool_kind(ToolKind::List)),
|
||||
])
|
||||
}
|
||||
|
||||
async fn collect_reminders(
|
||||
&self,
|
||||
resources: SharedResources,
|
||||
tool_output: &ToolOutput,
|
||||
) -> Vec<String> {
|
||||
// 1. Activate `paths:`-gated skills matching any file the tool touched
|
||||
// (includes multi-file `apply_patch` edits).
|
||||
let activation_paths = Self::extract_activation_paths(tool_output);
|
||||
if !activation_paths.is_empty() {
|
||||
let path_refs: Vec<&Path> = activation_paths.iter().map(PathBuf::as_path).collect();
|
||||
let mut res = resources.lock().await;
|
||||
if let Some(tracker) = res.get_mut::<SkillManager>() {
|
||||
tracker.activate_conditional_skills_for_paths(&path_refs);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Discovery walks from a single representative path (read/list/edit).
|
||||
let Some(target_path) = Self::extract_target_path(tool_output) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// Direct SKILL.md detection: when a tool writes (or reads) a
|
||||
// SKILL.md file, register it immediately. The normal upward-walk
|
||||
// discovery cannot find these because it looks for `.kigi/skills/`
|
||||
// sub-directories in *ancestor* dirs, and user-scope skills
|
||||
// (~/.kigi/) are outside the git root so the walk breaks early.
|
||||
if target_path.file_name().is_some_and(|n| n == "SKILL.md")
|
||||
&& Self::is_in_supported_skills_dir(target_path)
|
||||
{
|
||||
let scope = {
|
||||
let res = resources.lock().await;
|
||||
let tracker = res.get::<SkillManager>();
|
||||
let cwd = tracker.and_then(|m| m.cwd.clone());
|
||||
let git_root = tracker.and_then(|m| m.git_root.clone());
|
||||
match (cwd, git_root) {
|
||||
(Some(cwd), _) if target_path.starts_with(&cwd) => SkillScope::Local,
|
||||
(_, Some(root)) if target_path.starts_with(&root) => SkillScope::Repo,
|
||||
_ => SkillScope::User,
|
||||
}
|
||||
};
|
||||
let skills = discovery::parse_skill_files(vec![(target_path.to_path_buf(), scope)]);
|
||||
if !skills.is_empty() {
|
||||
let mut res = resources.lock().await;
|
||||
if let Some(tracker) = res.get_mut::<SkillManager>() {
|
||||
tracker.add_discovered(skills);
|
||||
}
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// 2. Snapshot context under lock, then RELEASE the lock before I/O.
|
||||
let (cwd, git_root, mut checked_dirs_snapshot, compat) = {
|
||||
let res = resources.lock().await;
|
||||
let Some(tracker) = res.get::<SkillManager>() else {
|
||||
return vec![];
|
||||
};
|
||||
let cwd = match tracker.cwd.clone() {
|
||||
Some(c) => c,
|
||||
None => return vec![],
|
||||
};
|
||||
(
|
||||
cwd,
|
||||
tracker.git_root.clone(),
|
||||
tracker.checked_dirs.clone(),
|
||||
tracker.compat,
|
||||
)
|
||||
};
|
||||
// Lock is released here.
|
||||
|
||||
// 3. Run filesystem discovery OUTSIDE the lock.
|
||||
// Calls directly into the discovery module -- no callback indirection.
|
||||
let discovered = discovery::discover_skills_for_paths(
|
||||
&[target_path],
|
||||
&cwd,
|
||||
git_root.as_deref(),
|
||||
&mut checked_dirs_snapshot,
|
||||
compat,
|
||||
);
|
||||
|
||||
if discovered.is_empty() {
|
||||
// Even if no skills found, merge checked_dirs back so we don't
|
||||
// re-stat the same directories on future calls.
|
||||
let mut res = resources.lock().await;
|
||||
if let Some(tracker) = res.get_mut::<SkillManager>() {
|
||||
tracker.checked_dirs.extend(checked_dirs_snapshot);
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// 4. Re-acquire lock and merge results into tracker.
|
||||
// The reminder does NOT produce announcement text. It just updates
|
||||
// the tracker state. The session drains announcements from the
|
||||
// tracker via take_pending_reconciliation() after each tool call.
|
||||
{
|
||||
let mut res = resources.lock().await;
|
||||
let tracker = match res.get_mut::<SkillManager>() {
|
||||
Some(t) => t,
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
// Merge checked_dirs from the snapshot back into the tracker.
|
||||
tracker.checked_dirs.extend(checked_dirs_snapshot);
|
||||
|
||||
// Add discovered skills (dedup by canonical path, sets pending flag).
|
||||
tracker.add_discovered(discovered);
|
||||
}
|
||||
|
||||
// Return empty -- announcement delivery is handled by the session
|
||||
// via take_pending_reconciliation(), NOT by this reminder.
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::output::ApplyPatchFileResult;
|
||||
|
||||
fn edited(path: &str, move_to: Option<&str>) -> ApplyPatchFileResult {
|
||||
ApplyPatchFileResult {
|
||||
path: PathBuf::from(path),
|
||||
action: "modified".into(),
|
||||
old_text: None,
|
||||
new_text: String::new(),
|
||||
move_to: move_to.map(PathBuf::from),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_activation_paths_cover_every_edited_file() {
|
||||
let multi_file_patch = ToolOutput::ApplyPatch(ApplyPatchOutput::Success {
|
||||
files: vec![edited("/r/a.rs", None), edited("/r/b.rs", Some("/r/c.rs"))],
|
||||
tool_output_for_prompt: String::new(),
|
||||
});
|
||||
assert_eq!(
|
||||
SkillDiscoveryReminder::extract_activation_paths(&multi_file_patch),
|
||||
vec![
|
||||
PathBuf::from("/r/a.rs"),
|
||||
PathBuf::from("/r/b.rs"),
|
||||
PathBuf::from("/r/c.rs"),
|
||||
],
|
||||
);
|
||||
let failed_patch = ToolOutput::ApplyPatch(ApplyPatchOutput::ParseError("x".into()));
|
||||
assert!(SkillDiscoveryReminder::extract_activation_paths(&failed_patch).is_empty());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user