Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ pub fn repair_skills(

let mut cmd = std::process::Command::new(skilllite_path);
crate::windows_spawn::hide_child_console(&mut cmd);
cmd.arg("evolution").arg("repair-skills");
cmd.arg("evolution")
.arg("repair-skills")
.arg("--workspace")
.arg(root.to_string_lossy().as_ref());
for name in skill_names {
cmd.arg(name);
}
Expand Down
41 changes: 12 additions & 29 deletions crates/skilllite-commands/src/evolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,11 @@ use skilllite_agent::types::AgentConfig;

use crate::error::bail;
use crate::Result;
use skilllite_core::config::env_keys::paths as env_paths;
use skilllite_core::paths;
use skilllite_core::protocol::{NewSkill, NodeResult};
use skilllite_core::skill::discovery::resolve_skills_dir_with_legacy_fallback;
use skilllite_core::skill::manifest;

/// Resolve workspace for legacy project-level skill commands.
/// Uses SKILLLITE_WORKSPACE env or current_dir. Returns workspace/.skills.
fn resolve_skills_root(workspace: Option<&str>) -> Option<PathBuf> {
let ws: PathBuf = workspace
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| {
std::env::var(env_paths::SKILLLITE_WORKSPACE)
.ok()
.map(PathBuf::from)
})
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let ws = if ws.is_absolute() {
ws
} else {
std::env::current_dir().ok()?.join(ws)
};
Some(ws.join(".skills"))
}

fn resolve_run_skills_root(workspace: &str) -> PathBuf {
let ws = crate::evolution_status::resolve_workspace_root(workspace);
resolve_skills_dir_with_legacy_fallback(&ws, "skills").effective_path
Expand Down Expand Up @@ -254,7 +233,7 @@ pub fn cmd_backlog(
}

/// `skilllite evolution reset` — delete all evolved data, return to seed state.
pub fn cmd_reset(force: bool) -> Result<()> {
pub fn cmd_reset(workspace: &str, force: bool) -> Result<()> {
if !force {
println!("⚠️ 这将删除所有进化产物(规则、示例、Skill),回到种子状态。");
println!(" 已有进化经验将永久丢失。种子规则不受影响。");
Expand All @@ -263,15 +242,15 @@ pub fn cmd_reset(force: bool) -> Result<()> {
return Ok(());
}

let root = paths::chat_root();
let root = crate::evolution_status::chat_root_for_workspace(workspace);

// Re-seed prompts (overwrite evolved rules/examples with seed data)
skilllite_evolution::seed::ensure_seed_data_force(&root);
println!("✅ Prompts 已重置为种子状态");

// Remove evolved skills (project-level, includes _pending)
let evolved_dir = resolve_skills_root(None).map(|sr| sr.join("_evolved"));
if let Some(evolved_dir) = evolved_dir.filter(|p| p.exists()) {
let evolved_dir = resolve_run_skills_root(workspace).join("_evolved");
if evolved_dir.exists() {
let count = std::fs::read_dir(&evolved_dir)
.ok()
.into_iter()
Expand Down Expand Up @@ -776,10 +755,14 @@ fn is_fetchable_source(source: &str) -> bool {
/// `skilllite evolution repair-skills [SKILL_NAME...]` — 验证技能并修复失败的。
/// 不传技能名时验证并修复所有失败技能;传一个或多个技能名时仅验证并修复这些技能,缩短执行时间。
/// `from_source`: 对下载的技能失败时自动从源头更新,不交互询问(桌面/CI 等非 TTY 时传 true)。
pub fn cmd_repair_skills(skills_filter: Option<Vec<String>>, from_source: bool) -> Result<()> {
let skills_root = resolve_skills_root(None).ok_or_else(|| {
crate::Error::validation("无法解析工作区。请设置 SKILLLITE_WORKSPACE 或在项目目录运行。")
})?;
pub fn cmd_repair_skills(
workspace: &str,
skills_filter: Option<Vec<String>>,
from_source: bool,
) -> Result<()> {
let ws_root = crate::evolution_status::resolve_workspace_root(workspace);
skilllite_core::config::load_dotenv_from_dir(&ws_root);
let skills_root = resolve_run_skills_root(workspace);

let config = AgentConfig::from_env();
if config.api_key.is_empty() {
Expand Down
2 changes: 2 additions & 0 deletions docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ Priority commands for parity with today’s Desktop bridge:
| `skilllite evolution proposal-status --json <id>` | `EvolutionProposalStatusSnapshot` | **Shipped**; `--workspace` |
| `skilllite evolution confirm/reject --json` | `EvolutionOpSnapshot` | **Shipped**; `--workspace` |
| `skilllite evolution run --json` | `NodeResult` | **Shipped**; `--workspace`, `--proposal-id`, `--log-manual-trigger` |
| `skilllite evolution reset --workspace` | human output | **Shipped**; destructive reset is scoped to the selected workspace `chat/` and effective skills root |
| `skilllite evolution repair-skills --workspace` | human output | **Shipped**; desktop repair validates the selected workspace skills root (`skills/` with `.skills` fallback) |
| `skilllite runtime probe --json` | `RuntimeUiSnapshot` | **Shipped** |
| `skilllite runtime provision --json` | stderr progress JSON lines + `ProvisionRuntimesResult` on stdout | **Shipped**; `--python` / `--node` / `--force` |
| `skilllite skills list --json --workspace` | `DesktopSkillSnapshot[]` (desktop `DesktopSkillInfo`) | **Shipped** |
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ flowchart TB
| `skilllite evolution proposal-status --json` | 单条 backlog | **已落地**;`--workspace` |
| `skilllite evolution confirm/reject --json` | 操作结果 | **已落地**;`--workspace` |
| `skilllite evolution run --json` | `NodeResult` | **已落地**;`--workspace`、`--proposal-id`、`--log-manual-trigger` |
| `skilllite evolution reset --workspace` | 人类可读输出 | **已落地**;破坏性重置限定在所选工作区的 `chat/` 与有效技能根 |
| `skilllite evolution repair-skills --workspace` | 人类可读输出 | **已落地**;桌面修复验证所选工作区技能根(`skills/`,缺失时回退 `.skills`) |
| `skilllite runtime probe --json` | `RuntimeUiSnapshot` | **已落地** |
| `skilllite runtime provision --json` | stderr 进度 JSON 行 + stdout `ProvisionRuntimesResult` | **已落地**;`--python` / `--node` / `--force` |
| `skilllite skills list --json --workspace` | `DesktopSkillSnapshot[]`(对齐 `DesktopSkillInfo`) | **已落地** |
Expand Down
7 changes: 7 additions & 0 deletions skilllite/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,9 @@ pub enum EvolutionAction {

/// Reset to seed state — delete all evolved rules, examples, and skills
Reset {
/// Project workspace root
#[arg(long, short = 'w', default_value = ".")]
workspace: String,
/// Skip confirmation prompt
#[arg(long, short)]
force: bool,
Expand Down Expand Up @@ -1226,6 +1229,10 @@ pub enum EvolutionAction {

/// Repair skills: validate then LLM-fix failures. Without names, repair all failed; with names, only validate/repair those (faster when many skills).
RepairSkills {
/// Project workspace root
#[arg(long, short = 'w', default_value = ".")]
workspace: String,

/// 仅验证并修复这些技能(目录名);不传则处理全部失败技能
#[arg(value_name = "SKILL_NAME")]
skills: Vec<String>,
Expand Down
6 changes: 4 additions & 2 deletions skilllite/src/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,8 @@ fn register_agent(reg: &mut CommandRegistry) {
workspace,
proposal_id,
),
EvolutionAction::Reset { force } => {
skilllite_commands::evolution::cmd_reset(*force)
EvolutionAction::Reset { workspace, force } => {
skilllite_commands::evolution::cmd_reset(workspace, *force)
}
EvolutionAction::Disable { rule_id } => {
skilllite_commands::evolution::cmd_disable(rule_id)
Expand Down Expand Up @@ -566,9 +566,11 @@ fn register_agent(reg: &mut CommandRegistry) {
*log_manual_trigger,
),
EvolutionAction::RepairSkills {
workspace,
skills,
from_source,
} => skilllite_commands::evolution::cmd_repair_skills(
workspace,
if skills.is_empty() {
None
} else {
Expand Down
135 changes: 134 additions & 1 deletion skilllite/tests/cli_evolution_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

mod common;

use common::{skilllite_bin, stdout_str};
use common::{create_calculator_skill, skilllite_bin, stdout_str};
use std::path::Path;
use std::process::{Command, Output};

Expand Down Expand Up @@ -40,6 +40,21 @@ fn authorize_capability(workspace: &Path, tool_name: &str) {
);
}

fn write_file(path: &Path, content: &str) {
std::fs::create_dir_all(path.parent().expect("file parent")).expect("create parent");
std::fs::write(path, content).expect("write file");
}

fn run_reset(args: &[&str], current_dir: &Path, env_workspace: &Path) -> Output {
Command::new(skilllite_bin())
.args(args)
.current_dir(current_dir)
.env("NO_COLOR", "1")
.env("SKILLLITE_WORKSPACE", env_workspace)
.output()
.expect("failed to spawn skilllite")
}

#[test]
fn evolution_backlog_workspace_flag_overrides_env_workspace() {
let env_workspace = tempfile::tempdir().expect("env workspace");
Expand Down Expand Up @@ -88,3 +103,121 @@ fn evolution_backlog_workspace_flag_overrides_env_workspace() {
"env workspace backlog row should not leak into target query: {notes:?}"
);
}

#[test]
fn evolution_reset_defaults_to_current_workspace_roots() {
let env_workspace = tempfile::tempdir().expect("env workspace");
let target_workspace = tempfile::tempdir().expect("target workspace");

let env_rules = env_workspace.path().join("chat/prompts/rules.json");
let target_rules = target_workspace.path().join("chat/prompts/rules.json");
write_file(&env_rules, "env rules must remain");
write_file(&target_rules, "target evolved rules");
std::fs::create_dir_all(target_workspace.path().join("skills/_evolved/generated"))
.expect("create evolved skills");

let out = run_reset(
&["evolution", "reset", "--force"],
target_workspace.path(),
env_workspace.path(),
);

assert!(
out.status.success(),
"reset failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
std::fs::read_to_string(&env_rules).expect("env rules still exist"),
"env rules must remain"
);
assert_ne!(
std::fs::read_to_string(&target_rules).expect("target rules reseeded"),
"target evolved rules"
);
assert!(
!target_workspace.path().join("skills/_evolved").exists(),
"reset should remove evolved skills under the current workspace skills root"
);
}

#[test]
fn evolution_reset_workspace_flag_overrides_env_workspace_roots() {
let env_workspace = tempfile::tempdir().expect("env workspace");
let target_workspace = tempfile::tempdir().expect("target workspace");
let cwd = tempfile::tempdir().expect("cwd");

let env_rules = env_workspace.path().join("chat/prompts/rules.json");
let target_rules = target_workspace.path().join("chat/prompts/rules.json");
write_file(&env_rules, "env rules must remain");
write_file(&target_rules, "target evolved rules");
std::fs::create_dir_all(target_workspace.path().join("skills/_evolved/generated"))
.expect("create evolved skills");

let target_arg = target_workspace.path().to_string_lossy();
let out = run_reset(
&[
"evolution",
"reset",
"--force",
"--workspace",
target_arg.as_ref(),
],
cwd.path(),
env_workspace.path(),
);

assert!(
out.status.success(),
"reset failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
std::fs::read_to_string(&env_rules).expect("env rules still exist"),
"env rules must remain"
);
assert_ne!(
std::fs::read_to_string(&target_rules).expect("target rules reseeded"),
"target evolved rules"
);
assert!(
!target_workspace.path().join("skills/_evolved").exists(),
"reset should remove evolved skills under the explicit workspace skills root"
);
}

#[test]
fn evolution_repair_skills_prefers_workspace_skills_over_legacy() {
let workspace = tempfile::tempdir().expect("workspace");
let cwd = tempfile::tempdir().expect("cwd");
std::fs::create_dir_all(workspace.path().join("skills")).expect("create modern skills root");
create_calculator_skill(workspace.path());

let workspace_arg = workspace.path().to_string_lossy();
let out = Command::new(skilllite_bin())
.args([
"evolution",
"repair-skills",
"--workspace",
workspace_arg.as_ref(),
"--from-source",
])
.current_dir(cwd.path())
.env("NO_COLOR", "1")
.env("API_KEY", "sk-test")
.env("OPENAI_API_KEY", "sk-test")
.output()
.expect("failed to spawn skilllite");

assert!(
out.status.success(),
"repair-skills should validate the empty modern skills root instead of legacy .skills: stdout={}, stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
stdout_str(&out).contains("所有技能验证通过"),
"repair-skills stdout should report no repairs needed: {}",
stdout_str(&out)
);
}
49 changes: 49 additions & 0 deletions tasks/TASK-2026-070-evolution-reset-repair-workspace/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Technical Context

## Current State

- Relevant crates/files:
- `skilllite/src/cli.rs`: `EvolutionAction` argument definitions.
- `skilllite/src/dispatch/mod.rs`: CLI dispatch to command crate.
- `crates/skilllite-commands/src/evolution.rs`: reset, run, repair command implementations.
- `crates/skilllite-commands/src/evolution_status.rs`: workspace and chat-root helpers.
- `crates/skilllite-core/src/skill/discovery.rs`: effective skills-root resolver.
- `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/skill_rpc.rs`: desktop repair subprocess.
- Current behavior:
- `cmd_run` resolves the requested workspace, sets `SKILLLITE_WORKSPACE` to the absolute workspace, uses `paths::chat_root()`, and uses `resolve_skills_dir_with_legacy_fallback`.
- `cmd_reset` does not accept a workspace and directly uses `paths::chat_root()`, which ignores relative `SKILLLITE_WORKSPACE` values.
- `cmd_reset` deletes only `<workspace>/.skills/_evolved` through a legacy helper.
- `cmd_repair_skills` validates only the legacy helper result, so `skills/_evolved` can be skipped.

## Architecture Fit

- Layer boundaries involved:
- Entry CLI (`skilllite`) remains responsible for argument parsing and dispatch.
- `skilllite-commands` remains responsible for command behavior.
- `skilllite-core` continues to provide shared skills-root discovery.
- Interfaces to preserve:
- Existing command names and `--from-source` repair behavior.
- Legacy `.skills` fallback when `skills/` does not exist.

## Dependency and Compatibility

- New dependencies: None.
- Backward compatibility notes:
- New `--workspace/-w` flags align reset/repair with adjacent evolution commands.
- Default workspace `"."` changes reset away from legacy global fallback, but fixes the destructive split-brain behavior and remains explicit for callers.

## Design Decisions

- Decision: Reuse `resolve_run_skills_root` for reset and repair.
- Rationale: It already implements the modern `skills/` then `.skills` fallback used by `evolution run`.
- Alternatives considered: Keep the old helper and special-case `_evolved`.
- Why rejected: The old helper encodes the broken `.skills`-only behavior.
- Decision: Use `chat_root_for_workspace` for reset.
- Rationale: It gives deterministic `<workspace>/chat` behavior for relative and absolute workspaces without mutating process-global environment.
- Alternatives considered: Set `SKILLLITE_WORKSPACE` and keep `paths::chat_root()`.
- Why rejected: Environment mutation is unnecessary and easier to get wrong in tests or embedded callers.

## Open Questions

- [x] Should unrelated legacy commands be changed in this task? No; keep the fix scoped to reset/repair because they are the concrete high-impact paths found.
- [x] Are docs required? No user docs currently document reset/repair workspace flags in detail; PR/task artifacts and CLI help cover the new flags.
42 changes: 42 additions & 0 deletions tasks/TASK-2026-070-evolution-reset-repair-workspace/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PRD

## Background

SkillLite evolution data is now project-scoped for desktop and CLI L2 flows. `status`, `backlog`, `pending`, `confirm`, `reject`, and `run` accept or derive a workspace and use project-local `chat/` plus the effective skills root (`skills/` with `.skills` fallback).

The remaining maintenance commands `reset` and `repair-skills` did not move with that contract. A destructive reset can therefore modify the wrong chat store while leaving current evolved skills in place, and repair can silently validate an empty legacy tree.

## Objective

Make evolution reset and repair target the same workspace roots as the rest of the evolution command surface.

Prevent split-brain behavior where chat state and evolved skills are reset or validated in different workspaces.

## Functional Requirements

- FR-1: `evolution reset` MUST accept `--workspace/-w` with default `"."` and use `<workspace>/chat` for prompt/log/snapshot reset.
- FR-2: `evolution reset --force` MUST remove `_evolved` under the effective skills root for the selected workspace.
- FR-3: `evolution repair-skills` MUST accept `--workspace/-w` with default `"."` and validate the effective skills root for that workspace.
- FR-4: Desktop repair calls MUST pass the resolved workspace explicitly to the CLI subprocess.

## Non-Functional Requirements

- Security: No sandbox, permission, or LLM credential handling changes.
- Performance: Root resolution remains filesystem-only and should not add meaningful overhead.
- Compatibility: Legacy `.skills` workspaces continue to work through existing fallback behavior; explicit workspace selection is available for non-current workspaces.

## Constraints

- Technical: Keep dependency direction unchanged (`skilllite` entry dispatches into `skilllite-commands`).
- Timeline: N/A for autonomous execution; this is a focused CLI bugfix.

## Success Metrics

- Metric: Regression tests cover reset root selection and repair root selection.
- Baseline: Reset/repair use legacy `.skills` or global chat roots in at least one common workspace scenario.
- Target: Tests fail on the old behavior and pass with workspace-aware resolution.

## Rollout

- Rollout plan: Ship as a CLI/desktop bugfix with task evidence and focused tests.
- Rollback plan: Revert the CLI argument and resolver changes if unexpected compatibility issues are found.
Loading
Loading