From 897b00fb36467d536178933cd0ef0d0a1a6f4a53 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 10 Jun 2026 11:23:56 +0000 Subject: [PATCH] Fix evolution CLI workspace DB scoping Co-authored-by: EXboy --- .../integrations/evolution_ui/backlog.rs | 2 + crates/skilllite-commands/src/evolution.rs | 9 +- .../src/evolution_desktop.rs | 153 ++++++++++++++++-- .../src/evolution_status.rs | 83 +++++++++- docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md | 10 +- docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md | 12 +- skilllite/src/cli.rs | 3 + skilllite/src/dispatch/mod.rs | 15 +- skilllite/tests/cli_evolution_workspace.rs | 90 +++++++++++ .../CONTEXT.md | 42 +++++ .../PRD.md | 39 +++++ .../REVIEW.md | 58 +++++++ .../STATUS.md | 29 ++++ .../TASK.md | 74 +++++++++ tasks/board.md | 3 +- 15 files changed, 591 insertions(+), 31 deletions(-) create mode 100644 skilllite/tests/cli_evolution_workspace.rs create mode 100644 tasks/TASK-2026-068-evolution-workspace-db-scope/CONTEXT.md create mode 100644 tasks/TASK-2026-068-evolution-workspace-db-scope/PRD.md create mode 100644 tasks/TASK-2026-068-evolution-workspace-db-scope/REVIEW.md create mode 100644 tasks/TASK-2026-068-evolution-workspace-db-scope/STATUS.md create mode 100644 tasks/TASK-2026-068-evolution-workspace-db-scope/TASK.md diff --git a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/backlog.rs b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/backlog.rs index 9407e519..b412026c 100644 --- a/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/backlog.rs +++ b/crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/backlog.rs @@ -28,6 +28,8 @@ pub fn get_evolution_proposal_status( "evolution", "proposal-status", "--json", + "--workspace", + workspace, proposal_id, ], ) diff --git a/crates/skilllite-commands/src/evolution.rs b/crates/skilllite-commands/src/evolution.rs index d68dd95d..ee12525c 100644 --- a/crates/skilllite-commands/src/evolution.rs +++ b/crates/skilllite-commands/src/evolution.rs @@ -156,17 +156,18 @@ fn query_backlog_rows( pub fn cmd_backlog( json: bool, hide_closed: bool, + workspace: &str, status: Option<&str>, risk: Option<&str>, limit: usize, ) -> Result<()> { if json && hide_closed { - let rows = query_backlog_desktop(limit)?; + let rows = query_backlog_desktop(workspace, limit)?; println!("{}", serde_json::to_string_pretty(&rows)?); return Ok(()); } - let root = paths::chat_root(); + let root = crate::evolution_status::chat_root_for_workspace(workspace); let status_filter = normalize_status_filter(status)?; let risk_filter = normalize_risk_filter(risk)?; let rows = query_backlog_rows( @@ -537,8 +538,8 @@ pub fn cmd_pending(json: bool, workspace: &str) -> Result<()> { } /// `skilllite evolution proposal-status ` — single backlog row for desktop. -pub fn cmd_proposal_status(json: bool, proposal_id: &str) -> Result<()> { - let row = desktop_query_proposal_status(proposal_id)?; +pub fn cmd_proposal_status(json: bool, workspace: &str, proposal_id: &str) -> Result<()> { + let row = desktop_query_proposal_status(workspace, proposal_id)?; if json { println!("{}", serde_json::to_string_pretty(&row)?); } else { diff --git a/crates/skilllite-commands/src/evolution_desktop.rs b/crates/skilllite-commands/src/evolution_desktop.rs index 48a93d69..809c68b3 100644 --- a/crates/skilllite-commands/src/evolution_desktop.rs +++ b/crates/skilllite-commands/src/evolution_desktop.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use skilllite_core::skill::discovery::resolve_skills_dir_with_legacy_fallback; -use crate::evolution_status::resolve_workspace_root; +use crate::evolution_status::{chat_root_for_workspace, resolve_workspace_root}; use crate::Result; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -68,8 +68,11 @@ fn truncate_utf8(s: &str, max: usize) -> String { format!("{}…", &s[..end]) } -pub fn query_backlog_desktop(limit: usize) -> Result> { - let chat_root = skilllite_core::paths::chat_root(); +pub fn query_backlog_desktop( + workspace: &str, + limit: usize, +) -> Result> { + let chat_root = chat_root_for_workspace(workspace); let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root)?; let limit = limit.clamp(1, 200); let mut stmt = conn @@ -102,8 +105,11 @@ pub fn query_backlog_desktop(limit: usize) -> Result>>() } -pub fn query_proposal_status(proposal_id: &str) -> Result { - let chat_root = skilllite_core::paths::chat_root(); +pub fn query_proposal_status( + workspace: &str, + proposal_id: &str, +) -> Result { + let chat_root = chat_root_for_workspace(workspace); let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root)?; conn.query_row( "SELECT proposal_id, status, acceptance_status, updated_at, note @@ -167,7 +173,7 @@ pub fn read_pending_skill_md(workspace: &str, skill_name: &str) -> Result Result<()> { let skills_root = resolve_skills_root(workspace)?; skilllite_evolution::skill_synth::confirm_pending_skill(&skills_root, skill_name)?; - let chat_root = skilllite_core::paths::chat_root(); + let chat_root = chat_root_for_workspace(workspace); if let Ok(conn) = skilllite_evolution::feedback::open_evolution_db(&chat_root) { let _ = skilllite_evolution::log_evolution_event( &conn, @@ -198,8 +204,8 @@ pub fn authorize_capability_evolution( outcome: &str, summary: &str, ) -> Result { - let _ = resolve_workspace_root(workspace); - let chat_root = skilllite_core::paths::chat_root(); + let workspace_root = resolve_workspace_root(workspace); + let chat_root = workspace_root.join("chat"); let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root)?; let proposal_id = skilllite_evolution::enqueue_user_capability_evolution(&conn, tool_name, outcome, summary)?; @@ -223,7 +229,7 @@ pub fn log_manual_evolution_trigger( proposal_id: Option<&str>, summary: &str, ) -> Result<()> { - let chat_root = skilllite_core::paths::chat_root(); + let chat_root = chat_root_for_workspace(workspace); let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root)?; let clipped = clip_manual_trigger_summary(summary); let _ = skilllite_evolution::log_evolution_event( @@ -240,6 +246,65 @@ pub fn log_manual_evolution_trigger( #[cfg(test)] mod tests { use super::*; + use skilllite_core::config::env_keys::paths as env_paths; + use std::sync::Mutex; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + struct EnvRestore { + key: &'static str, + previous: Option, + } + + impl EnvRestore { + fn set(key: &'static str, value: &str) -> Self { + let previous = std::env::var(key).ok(); + skilllite_core::config::set_env_var(key, value); + Self { key, previous } + } + } + + impl Drop for EnvRestore { + fn drop(&mut self) { + if let Some(value) = &self.previous { + skilllite_core::config::set_env_var(self.key, value); + } else { + skilllite_core::config::remove_env_var(self.key); + } + } + } + + fn temp_workspace(label: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "skilllite-evo-desktop-{label}-{}", + uuid::Uuid::new_v4() + )) + } + + fn seed_backlog_row(workspace: &std::path::Path, proposal_id: &str, note: &str) { + let chat_root = workspace.join("chat"); + let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root).expect("open db"); + let dedupe_key = format!("dedupe_{proposal_id}"); + conn.execute( + "INSERT INTO evolution_backlog + (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, expected_gain, effort, acceptance_criteria, status, acceptance_status, note) + VALUES (?1, 'active', ?2, '{}', 'low', 0.5, 0.5, 1.0, '[]', 'queued', 'pending_validation', ?3)", + [proposal_id, dedupe_key.as_str(), note], + ) + .expect("insert backlog row"); + } + + fn capability_rows(workspace: &std::path::Path, tool_name: &str) -> i64 { + let chat_root = workspace.join("chat"); + let conn = skilllite_evolution::feedback::open_evolution_db(&chat_root).expect("open db"); + let dedupe_key = format!("user_capability:{tool_name}:failure"); + conn.query_row( + "SELECT COUNT(*) FROM evolution_backlog WHERE dedupe_key = ?1", + [dedupe_key.as_str()], + |row| row.get(0), + ) + .expect("count rows") + } #[test] fn manual_trigger_summary_clip_is_utf8_boundary_safe() { @@ -260,4 +325,74 @@ mod tests { assert_eq!(clip_manual_trigger_summary(summary), summary); } + + #[test] + fn query_backlog_desktop_uses_workspace_argument_over_env() { + let _lock = ENV_LOCK.lock().expect("env lock"); + let env_workspace = temp_workspace("env"); + let target_workspace = temp_workspace("target"); + let _env_restore = EnvRestore::set( + env_paths::SKILLLITE_WORKSPACE, + env_workspace.to_string_lossy().as_ref(), + ); + seed_backlog_row(&env_workspace, "env_only", "ENV_DB_ROW"); + seed_backlog_row(&target_workspace, "target_only", "TARGET_DB_ROW"); + + let rows = + query_backlog_desktop(target_workspace.to_string_lossy().as_ref(), 10).expect("query"); + + let ids: Vec<_> = rows.into_iter().map(|row| row.proposal_id).collect(); + assert_eq!(ids, vec!["target_only"]); + let _ = std::fs::remove_dir_all(env_workspace); + let _ = std::fs::remove_dir_all(target_workspace); + } + + #[test] + fn query_proposal_status_uses_workspace_argument_over_env() { + let _lock = ENV_LOCK.lock().expect("env lock"); + let env_workspace = temp_workspace("env"); + let target_workspace = temp_workspace("target"); + let _env_restore = EnvRestore::set( + env_paths::SKILLLITE_WORKSPACE, + env_workspace.to_string_lossy().as_ref(), + ); + seed_backlog_row(&env_workspace, "shared_id", "ENV_DB_ROW"); + seed_backlog_row(&target_workspace, "shared_id", "TARGET_DB_ROW"); + + let row = query_proposal_status(target_workspace.to_string_lossy().as_ref(), "shared_id") + .expect("query"); + + assert_eq!(row.note.as_deref(), Some("TARGET_DB_ROW")); + let _ = std::fs::remove_dir_all(env_workspace); + let _ = std::fs::remove_dir_all(target_workspace); + } + + #[test] + fn authorize_capability_uses_workspace_argument_over_env() { + let _lock = ENV_LOCK.lock().expect("env lock"); + let env_workspace = temp_workspace("env"); + let target_workspace = temp_workspace("target"); + let tool_name = "workspace_scope_regression"; + let _env_restore = EnvRestore::set( + env_paths::SKILLLITE_WORKSPACE, + env_workspace.to_string_lossy().as_ref(), + ); + let _ = skilllite_evolution::feedback::open_evolution_db(&env_workspace.join("chat")) + .expect("open env db"); + let _ = skilllite_evolution::feedback::open_evolution_db(&target_workspace.join("chat")) + .expect("open target db"); + + authorize_capability_evolution( + target_workspace.to_string_lossy().as_ref(), + tool_name, + "failure", + "summary", + ) + .expect("authorize"); + + assert_eq!(capability_rows(&env_workspace, tool_name), 0); + assert_eq!(capability_rows(&target_workspace, tool_name), 1); + let _ = std::fs::remove_dir_all(env_workspace); + let _ = std::fs::remove_dir_all(target_workspace); + } } diff --git a/crates/skilllite-commands/src/evolution_status.rs b/crates/skilllite-commands/src/evolution_status.rs index 41a2e25c..e5b31aa2 100644 --- a/crates/skilllite-commands/src/evolution_status.rs +++ b/crates/skilllite-commands/src/evolution_status.rs @@ -75,6 +75,10 @@ pub(crate) fn resolve_workspace_root(workspace: &str) -> PathBuf { } } +pub(crate) fn chat_root_for_workspace(workspace: &str) -> PathBuf { + resolve_workspace_root(workspace).join("chat") +} + fn workspace_env_lookup(workspace_root: &Path, key: &str) -> Option { skilllite_core::config::parse_dotenv_from_dir(workspace_root) .into_iter() @@ -156,7 +160,7 @@ pub fn build_evolution_status_snapshot(params: &EvolutionStatusParams) -> Evolut skilllite_evolution::skill_synth::list_pending_skills_with_review(&skills_root).len(); } - let chat_root = skilllite_core::paths::chat_root(); + let chat_root = workspace_root.join("chat"); let mut db_error = None; let mut unprocessed_decisions = 0i64; let mut weighted_signal_sum = 0i64; @@ -298,7 +302,7 @@ pub fn cmd_status(json: bool, workspace: &str, periodic_anchor_unix: Option fn cmd_status_human(workspace: &str) -> Result<()> { let workspace_root = resolve_workspace_root(workspace); skilllite_core::config::load_dotenv_from_dir(&workspace_root); - let root = skilllite_core::paths::chat_root(); + let root = workspace_root.join("chat"); let conn = skilllite_evolution::feedback::open_evolution_db(&root)?; let mode = evolution_mode_from_workspace(&workspace_root); @@ -420,6 +424,81 @@ fn cmd_status_human(workspace: &str) -> Result<()> { Ok(()) } +#[cfg(test)] +mod workspace_scope_tests { + use super::*; + use skilllite_core::config::env_keys::paths as env_paths; + use std::sync::Mutex; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + struct EnvRestore { + key: &'static str, + previous: Option, + } + + impl EnvRestore { + fn set(key: &'static str, value: &str) -> Self { + let previous = std::env::var(key).ok(); + skilllite_core::config::set_env_var(key, value); + Self { key, previous } + } + } + + impl Drop for EnvRestore { + fn drop(&mut self) { + if let Some(value) = &self.previous { + skilllite_core::config::set_env_var(self.key, value); + } else { + skilllite_core::config::remove_env_var(self.key); + } + } + } + + fn temp_workspace(label: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "skilllite-evo-status-{label}-{}", + uuid::Uuid::new_v4() + )) + } + + fn seed_decision(workspace: &Path) { + let conn = skilllite_evolution::feedback::open_evolution_db(&workspace.join("chat")) + .expect("open db"); + conn.execute( + "INSERT INTO decisions + (evolved, total_tools, failed_tools, replans, task_completed, task_description, ts) + VALUES (0, 1, 0, 0, 1, 'workspace scoped decision', datetime('now'))", + [], + ) + .expect("insert decision"); + } + + #[test] + fn status_snapshot_uses_workspace_argument_for_db_over_env() { + let _lock = ENV_LOCK.lock().expect("env lock"); + let env_workspace = temp_workspace("env"); + let target_workspace = temp_workspace("target"); + let _env_restore = EnvRestore::set( + env_paths::SKILLLITE_WORKSPACE, + env_workspace.to_string_lossy().as_ref(), + ); + let _ = skilllite_evolution::feedback::open_evolution_db(&env_workspace.join("chat")) + .expect("open env db"); + seed_decision(&target_workspace); + + let snapshot = build_evolution_status_snapshot(&EvolutionStatusParams { + workspace: target_workspace.to_string_lossy().to_string(), + periodic_anchor_unix: None, + }); + + assert_eq!(snapshot.unprocessed_decisions, 1); + assert!(snapshot.db_error.is_none()); + let _ = std::fs::remove_dir_all(env_workspace); + let _ = std::fs::remove_dir_all(target_workspace); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md b/docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md index 97a33797..693f6c12 100644 --- a/docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md +++ b/docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md @@ -121,16 +121,16 @@ Priority commands for parity with today’s Desktop bridge: | Command | JSON output | Notes | |---------|-------------|-------| | `skilllite evolution status --json` | `EvolutionStatusSnapshot` | **Shipped**; `--workspace`, `--periodic-anchor-unix` | -| `skilllite evolution backlog --json --hide-closed` | `EvolutionBacklogRowSnapshot[]` | **Shipped** (desktop filter) | -| `skilllite evolution pending --json` | `PendingSkillSnapshot[]` | **Shipped** | -| `skilllite evolution proposal-status --json ` | `EvolutionProposalStatusSnapshot` | **Shipped** | -| `skilllite evolution confirm/reject --json` | `EvolutionOpSnapshot` | **Shipped** | +| `skilllite evolution backlog --json --hide-closed` | `EvolutionBacklogRowSnapshot[]` | **Shipped**; `--workspace` (desktop filter) | +| `skilllite evolution pending --json` | `PendingSkillSnapshot[]` | **Shipped**; `--workspace` | +| `skilllite evolution proposal-status --json ` | `EvolutionProposalStatusSnapshot` | **Shipped**; `--workspace` | +| `skilllite evolution confirm/reject --json` | `EvolutionOpSnapshot` | **Shipped**; `--workspace` | | `skilllite evolution run --json` | `NodeResult` | **Shipped**; `--workspace`, `--proposal-id`, `--log-manual-trigger` | | `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** | | `skilllite suggest-followup --json` | `{ "suggestions": string[] }` | **Shipped** | -| `skilllite evolution authorize-capability --json` | `{ "proposal_id": string }` | **Shipped** | +| `skilllite evolution authorize-capability --json` | `{ "proposal_id": string }` | **Shipped**; `--workspace` | **Convention:** `--json` always prints a single JSON document on stdout; human text on stderr only. diff --git a/docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md b/docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md index db4a5127..3227f2bd 100644 --- a/docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md +++ b/docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md @@ -123,16 +123,16 @@ flowchart TB | 命令 | JSON 输出 | 说明 | |------|-----------|------| | `skilllite evolution status --json` | `EvolutionStatusSnapshot` | **已落地**;`--workspace`、`--periodic-anchor-unix` | -| `skilllite evolution backlog --json --hide-closed` | `EvolutionBacklogRowSnapshot[]` | **已落地**(桌面默认过滤) | -| `skilllite evolution pending --json` | 待审核技能列表 | **已落地** | -| `skilllite evolution proposal-status --json` | 单条 backlog | **已落地** | -| `skilllite evolution confirm/reject --json` | 操作结果 | **已落地** | -| `skilllite evolution run --json` | `NodeResult` | **已落地**;`--proposal-id`、`--log-manual-trigger` | +| `skilllite evolution backlog --json --hide-closed` | `EvolutionBacklogRowSnapshot[]` | **已落地**;`--workspace`(桌面默认过滤) | +| `skilllite evolution pending --json` | 待审核技能列表 | **已落地**;`--workspace` | +| `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 runtime probe --json` | `RuntimeUiSnapshot` | **已落地** | | `skilllite runtime provision --json` | stderr 进度 JSON 行 + stdout `ProvisionRuntimesResult` | **已落地**;`--python` / `--node` / `--force` | | `skilllite skills list --json --workspace` | `DesktopSkillSnapshot[]`(对齐 `DesktopSkillInfo`) | **已落地** | | `skilllite suggest-followup --json` | `{ "suggestions": [...] }` | **已落地** | -| `skilllite evolution authorize-capability --json` | `{ "proposal_id": "..." }` | **已落地** | +| `skilllite evolution authorize-capability --json` | `{ "proposal_id": "..." }` | **已落地**;`--workspace` | **约定:** `--json` 仅在 stdout 输出**一个** JSON 文档;人类可读信息走 stderr。 diff --git a/skilllite/src/cli.rs b/skilllite/src/cli.rs index 362e90c5..865d6c91 100644 --- a/skilllite/src/cli.rs +++ b/skilllite/src/cli.rs @@ -1148,6 +1148,9 @@ pub enum EvolutionAction { ProposalStatus { #[arg(long)] json: bool, + /// Project workspace root + #[arg(long, short = 'w', default_value = ".")] + workspace: String, #[arg(value_name = "PROPOSAL_ID")] proposal_id: String, }, diff --git a/skilllite/src/dispatch/mod.rs b/skilllite/src/dispatch/mod.rs index 7e953901..40059028 100644 --- a/skilllite/src/dispatch/mod.rs +++ b/skilllite/src/dispatch/mod.rs @@ -502,13 +502,14 @@ fn register_agent(reg: &mut CommandRegistry) { EvolutionAction::Backlog { json, hide_closed, - workspace: _, + workspace, status, risk, limit, } => skilllite_commands::evolution::cmd_backlog( *json, *hide_closed, + workspace, status.as_deref(), risk.as_deref(), *limit, @@ -516,9 +517,15 @@ fn register_agent(reg: &mut CommandRegistry) { EvolutionAction::Pending { json, workspace } => { skilllite_commands::evolution::cmd_pending(*json, workspace) } - EvolutionAction::ProposalStatus { json, proposal_id } => { - skilllite_commands::evolution::cmd_proposal_status(*json, proposal_id) - } + EvolutionAction::ProposalStatus { + json, + workspace, + proposal_id, + } => skilllite_commands::evolution::cmd_proposal_status( + *json, + workspace, + proposal_id, + ), EvolutionAction::Reset { force } => { skilllite_commands::evolution::cmd_reset(*force) } diff --git a/skilllite/tests/cli_evolution_workspace.rs b/skilllite/tests/cli_evolution_workspace.rs new file mode 100644 index 00000000..63699a37 --- /dev/null +++ b/skilllite/tests/cli_evolution_workspace.rs @@ -0,0 +1,90 @@ +//! Integration tests for evolution CLI workspace scoping. + +mod common; + +use common::{skilllite_bin, stdout_str}; +use std::path::Path; +use std::process::{Command, Output}; + +fn run_with_workspace_env(args: &[&str], env_workspace: &Path) -> Output { + Command::new(skilllite_bin()) + .args(args) + .env("NO_COLOR", "1") + .env("SKILLLITE_WORKSPACE", env_workspace) + .output() + .expect("failed to spawn skilllite") +} + +fn authorize_capability(workspace: &Path, tool_name: &str) { + let workspace_arg = workspace.to_string_lossy(); + let out = run_with_workspace_env( + &[ + "evolution", + "authorize-capability", + "--json", + "--workspace", + workspace_arg.as_ref(), + "--tool-name", + tool_name, + "--outcome", + "failure", + "--summary", + "workspace scope integration seed", + ], + workspace, + ); + assert!( + out.status.success(), + "authorize-capability failed: {}", + String::from_utf8_lossy(&out.stderr) + ); +} + +#[test] +fn evolution_backlog_workspace_flag_overrides_env_workspace() { + let env_workspace = tempfile::tempdir().expect("env workspace"); + let target_workspace = tempfile::tempdir().expect("target workspace"); + authorize_capability(env_workspace.path(), "env_workspace_tool"); + authorize_capability(target_workspace.path(), "target_workspace_tool"); + + let target_arg = target_workspace.path().to_string_lossy(); + let out = run_with_workspace_env( + &[ + "evolution", + "backlog", + "--json", + "--hide-closed", + "--workspace", + target_arg.as_ref(), + "--limit", + "20", + ], + env_workspace.path(), + ); + assert!( + out.status.success(), + "backlog failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + + let rows: Vec = + serde_json::from_str(stdout_str(&out).trim()).expect("valid backlog JSON"); + let notes: Vec = rows + .iter() + .filter_map(|row| row.get("note").and_then(|note| note.as_str())) + .map(str::to_string) + .collect(); + + assert!( + notes + .iter() + .any(|note| note.contains("target_workspace_tool")), + "target workspace backlog row should be returned: {notes:?}" + ); + assert!( + notes + .iter() + .all(|note| !note.contains("env_workspace_tool")), + "env workspace backlog row should not leak into target query: {notes:?}" + ); +} diff --git a/tasks/TASK-2026-068-evolution-workspace-db-scope/CONTEXT.md b/tasks/TASK-2026-068-evolution-workspace-db-scope/CONTEXT.md new file mode 100644 index 00000000..af5a4c51 --- /dev/null +++ b/tasks/TASK-2026-068-evolution-workspace-db-scope/CONTEXT.md @@ -0,0 +1,42 @@ +# Technical Context + +## Current State + +- Relevant crates/files: + - `skilllite/src/dispatch/mod.rs` + - `crates/skilllite-commands/src/evolution.rs` + - `crates/skilllite-commands/src/evolution_desktop.rs` + - `crates/skilllite-commands/src/evolution_status.rs` + - `crates/skilllite-core/src/paths.rs` +- Current behavior: + - `EvolutionAction::Backlog.workspace` is discarded in dispatch. + - `cmd_backlog` calls `query_backlog_desktop(limit)` for `--json --hide-closed` and `paths::chat_root()` otherwise. + - `query_backlog_desktop`, `query_proposal_status`, `authorize_capability_evolution`, and `log_manual_evolution_trigger` use `paths::chat_root()` directly. + - `build_evolution_status_snapshot` resolves the workspace for config but opens the DB via `paths::chat_root()`. + +## Architecture Fit + +- Layer boundaries involved: + - Entry crate dispatches CLI options into `skilllite-commands`. + - `skilllite-commands` may depend on `skilllite-core` and `skilllite-evolution`. +- Interfaces to preserve: + - Existing desktop JSON DTO structs and command output shapes. + - `skilllite_core::paths::chat_root()` global semantics for non-workspace-specific callers. + +## Dependency and Compatibility + +- New dependencies: none planned. +- Backward compatibility notes: + - Omitted workspace uses the CLI default `.` and resolves to the current working directory workspace. + - CLI callers that pass `--workspace` get the documented workspace-scoped behavior. + +## Design Decisions + +- Decision: use an explicit `chat_root_for_workspace` helper in command code and pass `workspace` through affected APIs. + - Rationale: avoids relying on process-global `SKILLLITE_WORKSPACE` mutation and keeps the fix localized. + - Alternatives considered: set `SKILLLITE_WORKSPACE` before each DB open. + - Why rejected: process-wide env mutation is more fragile and can create test/order side effects. + +## Open Questions + +- [ ] Whether `reset`, `disable`, and `explain` should grow `--workspace` in a later command cleanup task. diff --git a/tasks/TASK-2026-068-evolution-workspace-db-scope/PRD.md b/tasks/TASK-2026-068-evolution-workspace-db-scope/PRD.md new file mode 100644 index 00000000..fbb79c92 --- /dev/null +++ b/tasks/TASK-2026-068-evolution-workspace-db-scope/PRD.md @@ -0,0 +1,39 @@ +# PRD + +## Background + +Desktop L2 evolution commands are intended to operate on the workspace selected by the caller. Several JSON/desktop paths resolve the workspace for UI or `.env` data but still open the evolution DB through `skilllite_core::paths::chat_root()`, which only follows `SKILLLITE_WORKSPACE` or the global default. + +## Objective + +Ensure evolution L2 CLI/desktop JSON DB reads and writes use the workspace passed by `--workspace`, without broad refactors or schema changes. + +## Functional Requirements + +- FR-1: `evolution backlog --workspace ` must query `/chat/feedback.sqlite`. +- FR-2: `evolution status --json --workspace ` must read metrics/events from `/chat/feedback.sqlite`. +- FR-3: `evolution proposal-status --workspace ` must read the selected workspace DB. +- FR-4: `evolution authorize-capability --workspace ` must enqueue and audit into the selected workspace DB. +- FR-5: Existing JSON shapes and human output formats must remain unchanged. + +## Non-Functional Requirements + +- Security: do not relax evolution policy/runtime gating; authorization only enqueues governed proposals. +- Performance: no additional long-running DB scans beyond existing queries. +- Compatibility: callers that omit `--workspace` use the CLI default `.` and therefore resolve to the current working directory workspace. + +## Constraints + +- Technical: preserve crate dependency direction and avoid new dependencies. +- Timeline: high-severity correctness fix; keep changes minimal and focused. + +## Success Metrics + +- Metric: seeded CLI repro reads/enqueues the row in the `--workspace` DB. +- Baseline: pre-fix command uses the env/default DB instead of the CLI workspace. +- Target: post-fix command uses the CLI workspace DB and regression tests pass. + +## Rollout + +- Rollout plan: ship as a narrow command-layer bug fix with regression tests. +- Rollback plan: revert the command-layer DB path plumbing if regressions appear. diff --git a/tasks/TASK-2026-068-evolution-workspace-db-scope/REVIEW.md b/tasks/TASK-2026-068-evolution-workspace-db-scope/REVIEW.md new file mode 100644 index 00000000..6b9b8def --- /dev/null +++ b/tasks/TASK-2026-068-evolution-workspace-db-scope/REVIEW.md @@ -0,0 +1,58 @@ +# Review Report + +## Scope Reviewed + +- Files/modules: + - `skilllite/src/cli.rs` + - `skilllite/src/dispatch/mod.rs` + - `skilllite/tests/cli_evolution_workspace.rs` + - `crates/skilllite-commands/src/evolution.rs` + - `crates/skilllite-commands/src/evolution_desktop.rs` + - `crates/skilllite-commands/src/evolution_status.rs` + - `crates/skilllite-assistant/src-tauri/src/skilllite_bridge/integrations/evolution_ui/backlog.rs` + - `docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md` + - `docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md` +- Commits/changes: + - Added explicit workspace chat-root derivation for affected evolution DB reads/writes. + - Passed `workspace` through backlog/proposal-status CLI dispatch and assistant proposal-status bridge. + - Added command crate and CLI integration regressions for env-vs-workspace DB scoping. + +## Findings + +- Critical: none remaining. +- Major: pre-fix confirmed workspace mismatch is fixed by explicit workspace chat-root plumbing. +- Minor: `reset`, `disable`, and `explain` still have legacy no-`--workspace` semantics; captured as out of scope/follow-up. + +## Quality Gates + +- Architecture boundary checks: `pass` +- Security invariants: `pass` +- Required tests executed: `pass` +- Docs sync (EN/ZH): `pass` + +## Test Evidence + +- Commands run: + - `cargo run -p skilllite -- --help` after `rustup update stable && rustup default stable` + - Pre-fix seeded repro commands for backlog/status/authorize with `SKILLLITE_WORKSPACE=env` and `--workspace target` + - Post-fix seeded repro commands for backlog/status/authorize/proposal-status with `SKILLLITE_WORKSPACE=env` and `--workspace target` + - `cargo fmt --check` + - `cargo test -p skilllite-commands --features agent` + - `cargo test -p skilllite --test cli_evolution_workspace` + - `cargo clippy --all-targets -- -D warnings` + - `cargo test -p skilllite` + - `cargo test` +- Key outputs: + - Pre-fix: backlog outputs `['env_only']`; authorization row present in env DB and absent in target DB. + - Pre-fix logs: dispatch received target workspace, but DB roots selected `/tmp/skilllite-workspace-db-repro/env/chat`. + - Post-fix: backlog outputs `['target_only']` for desktop filtered mode and `['target_only', 'target_closed']` for unfiltered JSON; proposal-status returned `target_only TARGET_DB_ROW`; authorization row present in target DB and absent in env DB. + - `cargo test -p skilllite-commands --features agent`: `39 passed; 0 failed`. + - `cargo test -p skilllite --test cli_evolution_workspace`: `1 passed; 0 failed`. + - `cargo test -p skilllite`: all unit/integration tests passed. + - `cargo test`: full workspace passed. + +## Decision + +- Merge readiness: `ready` +- Follow-up actions: + - Consider a later task for `reset`, `disable`, and `explain` workspace flag consistency. diff --git a/tasks/TASK-2026-068-evolution-workspace-db-scope/STATUS.md b/tasks/TASK-2026-068-evolution-workspace-db-scope/STATUS.md new file mode 100644 index 00000000..c9978983 --- /dev/null +++ b/tasks/TASK-2026-068-evolution-workspace-db-scope/STATUS.md @@ -0,0 +1,29 @@ +# Status Journal + +## Timeline + +- 2026-06-10: + - Progress: Created task artifacts, injected required specs, and drafted PRD/CONTEXT before implementation. Code inspection found `Backlog.workspace` discarded in dispatch and multiple L2 evolution DB reads/writes using `paths::chat_root()` without CLI workspace scoping. + - Blockers: None. + - Next step: Add temporary instrumentation and run seeded CLI repro to collect runtime evidence. +- 2026-06-10: + - Progress: Reproduced the mismatch with temporary instrumentation. Pre-fix CLI output returned `env_only` for both backlog modes while `--workspace` pointed at `target`; authorization inserted into the env DB and left target empty. Debug logs showed dispatch received `workspace=/tmp/skilllite-workspace-db-repro/target`, while `query_backlog_desktop`, non-desktop backlog, status, and authorization selected `/tmp/skilllite-workspace-db-repro/env/chat`. + - Blockers: None. + - Next step: Apply focused workspace chat-root plumbing and verify with the same repro. +- 2026-06-10: + - Progress: Implemented explicit `chat_root_for_workspace` usage for affected command/desktop paths, passed workspace through backlog/proposal-status, updated the assistant proposal-status bridge, added regression tests, removed temporary instrumentation, and synced EN/ZH L2 docs. + - Blockers: None. + - Next step: Finalize review, validate task artifacts, commit, push, and open PR. +- 2026-06-10: + - Progress: Verification passed: post-fix repro returned `target_only`/`target_closed`, proposal-status returned `TARGET_DB_ROW`, authorization inserted into target and not env; `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test -p skilllite-commands --features agent`, `cargo test -p skilllite`, and `cargo test` all passed. + - Blockers: None. + - Next step: Done. + +## Checkpoints + +- [x] PRD drafted before implementation (or `N/A` recorded) +- [x] Context drafted before implementation (or `N/A` recorded) +- [x] Implementation complete +- [x] Tests passed +- [x] Review complete +- [x] Board updated diff --git a/tasks/TASK-2026-068-evolution-workspace-db-scope/TASK.md b/tasks/TASK-2026-068-evolution-workspace-db-scope/TASK.md new file mode 100644 index 00000000..672542e4 --- /dev/null +++ b/tasks/TASK-2026-068-evolution-workspace-db-scope/TASK.md @@ -0,0 +1,74 @@ +# TASK Card + +## Metadata + +- Task ID: `TASK-2026-068` +- Title: Evolution workspace DB scope +- Status: `done` +- Priority: `P0` +- Owner: `agent` +- Contributors: +- Created: `2026-06-10` +- Target milestone: + +## Problem + +Recent L2 evolution CLI and desktop JSON paths may ignore the `--workspace` flag when opening the evolution SQLite database. This can show backlog/status data from the wrong workspace and can enqueue user-authorized capability evolution into a different DB than the later workspace-scoped evolution run. + +## Scope + +- In scope: + - Investigate backlog/status/proposal-status/authorize-capability DB path scoping with runtime evidence. + - Apply the smallest Rust fix needed for workspace-scoped evolution DB reads/writes. + - Add focused regression tests for the affected command/desktop paths. +- Out of scope: + - Broad evolution architecture refactors. + - Changing SQLite schema or evolution policy/runtime thresholds. + +## Acceptance Criteria + +- [x] Runtime evidence proves the pre-fix workspace mismatch. +- [x] `--workspace`-scoped evolution backlog/status/proposal-status/authorize-capability paths use the selected workspace DB. +- [x] Regression tests cover the fixed workspace scoping behavior. +- [x] Required Rust verification commands pass. + +## Risks + +- Risk: mutating process environment globally to honor CLI workspace. + - Impact: unrelated commands/tests could become order-dependent. + - Mitigation: prefer explicit workspace-derived chat root helpers for command paths. +- Risk: over-widening the fix surface. + - Impact: unintended command behavior drift. + - Mitigation: limit changes to L2 evolution DB open call sites and dispatch parameter plumbing. + +## Validation Plan + +- Required tests: + - Focused regression tests for workspace-scoped evolution query/enqueue paths. + - Workspace command tests required by repo policy. +- Commands to run: + - `cargo fmt --check` + - `cargo clippy --all-targets -- -D warnings` + - `cargo test -p skilllite-commands` + - `cargo test -p skilllite` + - `cargo test` + - `python3 scripts/validate_tasks.py` +- Manual checks: + - Seed two temporary workspace DBs and run the CLI before/after the fix to verify output and inserted rows come from the `--workspace` target. + +## Regression Scope + +- Areas likely affected: + - `skilllite evolution backlog` + - `skilllite evolution status` + - `skilllite evolution proposal-status` + - `skilllite evolution authorize-capability` + - manual evolution trigger logging when `cmd_run --log-manual-trigger` completes +- Explicit non-goals: + - `reset`, `disable`, `explain`, and `repair-skills` workspace semantics unless directly required by the reported L2 bug. + +## Links + +- Source TODO section: user report in automation prompt. +- Related PRs/issues: +- Related docs: `docs/en/ASSISTANT-SPLIT-ARCHITECTURE.md`, `docs/zh/ASSISTANT-SPLIT-ARCHITECTURE.md` diff --git a/tasks/board.md b/tasks/board.md index 4b63f2b2..df732725 100644 --- a/tasks/board.md +++ b/tasks/board.md @@ -1,6 +1,6 @@ # Task Board -Last updated: 2026-06-06 (TASK-2026-067 utf8 llm error truncate done) +Last updated: 2026-06-10 (TASK-2026-068 evolution workspace db scope done) ## In Progress @@ -17,6 +17,7 @@ Last updated: 2026-06-06 (TASK-2026-067 utf8 llm error truncate done) ## Done +- `TASK-2026-068-evolution-workspace-db-scope` - Status: `done` - Owner: `agent` - `TASK-2026-067-utf8-llm-error-truncate` - Status: `done` - Owner: `agent` - `TASK-2026-066-utf8-evolution-log-truncate` - Status: `done` - Owner: `agent` - `TASK-2026-064-env-keys-single-source` - Status: `done` - Owner: `agent`