From 94184fe13b6eaf305e17fe66b82c1eee7bdfe7ca Mon Sep 17 00:00:00 2001 From: X-iZhang Date: Sun, 15 Feb 2026 01:10:48 +0000 Subject: [PATCH] feat(workspace): defer directory creation to CLI and improve workspace handling --- EvoScientist/EvoScientist.py | 3 +-- EvoScientist/cli/commands.py | 19 +++++++++---------- EvoScientist/cli/skills_cmd.py | 27 ++++++++++++++++++++------- EvoScientist/config/onboard.py | 29 ++++++++++------------------- EvoScientist/paths.py | 12 ++++++++---- tests/test_onboard.py | 15 +++++++++------ 6 files changed, 57 insertions(+), 48 deletions(-) diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index f8d2fdb..767b8e1 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -47,8 +47,7 @@ apply_config_to_env(_config) MAX_CONCURRENT = _config.max_concurrent MAX_ITERATIONS = _config.max_iterations -# Workspace settings -ensure_dirs() +# Workspace settings (defer dir creation to CLI; here we just resolve paths) WORKSPACE_DIR = str(default_workspace_dir()) set_active_workspace(WORKSPACE_DIR) MEMORY_DIR = str(_MEMORY_DIR_PATH) # Shared across sessions (not per-session) diff --git a/EvoScientist/cli/commands.py b/EvoScientist/cli/commands.py index bc387e0..9bca010 100644 --- a/EvoScientist/cli/commands.py +++ b/EvoScientist/cli/commands.py @@ -11,7 +11,7 @@ import typer # type: ignore[import-untyped] from rich.table import Table from ..stream.display import console -from ..paths import ensure_dirs, default_workspace_dir +from ..paths import ensure_dirs from ._app import app, config_app, mcp_app from .agent import _deduplicate_run_name, _create_session_workspace, _load_agent from .mcp_ui import ( @@ -353,13 +353,12 @@ def _main_callback( if not re.fullmatch(r"[A-Za-z0-9_-]+", name): raise typer.BadParameter("--name may only contain letters, digits, hyphens, and underscores") - ensure_dirs() - # Resolve effective mode from config (CLI mode already applied via overrides) effective_mode: str | None = None # None means explicit --workdir/--use-cwd was used # Resolve workspace directory for this session - # Priority: --use-cwd > --workdir > --mode (explicit) > default_workdir > default_mode + # Priority: --workdir > --mode (explicit) > default_workdir > default_mode > cwd + # --use-cwd is kept for backward compat but is now the default behavior if use_cwd: workspace_dir = os.getcwd() workspace_fixed = True @@ -370,7 +369,7 @@ def _main_callback( elif mode: # Explicit --mode overrides default_workdir effective_mode = mode - workspace_root = config.default_workdir or str(default_workspace_dir()) + workspace_root = config.default_workdir or os.getcwd() workspace_root = os.path.abspath(os.path.expanduser(workspace_root)) if effective_mode == "run": runs_dir = Path(workspace_root, "runs") @@ -380,7 +379,6 @@ def _main_callback( workspace_fixed = False else: # daemon workspace_dir = workspace_root - os.makedirs(workspace_dir, exist_ok=True) workspace_fixed = True elif config.default_workdir: # Use configured default workdir with configured mode @@ -394,18 +392,19 @@ def _main_callback( workspace_fixed = False else: # daemon workspace_dir = workspace_root - os.makedirs(workspace_dir, exist_ok=True) workspace_fixed = True else: effective_mode = config.default_mode if effective_mode == "run": workspace_dir = _create_session_workspace(name) workspace_fixed = False - else: # daemon mode (default) - workspace_dir = str(default_workspace_dir()) - os.makedirs(workspace_dir, exist_ok=True) + else: # daemon mode (default) — use current directory + workspace_dir = os.getcwd() workspace_fixed = True + # Ensure memory and skills subdirs exist in workspace + ensure_dirs() + if prompt: # Single-shot mode: wrap in persistent checkpointer import asyncio diff --git a/EvoScientist/cli/skills_cmd.py b/EvoScientist/cli/skills_cmd.py index 448610a..d74667d 100644 --- a/EvoScientist/cli/skills_cmd.py +++ b/EvoScientist/cli/skills_cmd.py @@ -5,23 +5,36 @@ from .agent import _shorten_path def _cmd_list_skills() -> None: - """List installed user skills.""" + """List all available skills (user and system).""" from ..tools.skills_manager import list_skills from ..paths import USER_SKILLS_DIR - skills = list_skills(include_system=False) + skills = list_skills(include_system=True) if not skills: - console.print("[dim]No user-installed skills.[/dim]") + console.print("[dim]No skills available.[/dim]") console.print("[dim]Install with:[/dim] /install-skill ") console.print(f"[dim]Skills directory:[/dim] [cyan]{_shorten_path(str(USER_SKILLS_DIR))}[/cyan]") console.print() return - console.print(f"[bold]User-Installed Skills[/bold] ({len(skills)}):") - for skill in skills: - console.print(f" [green]{skill.name}[/green] - {skill.description}") - console.print(f"\n[dim]Location:[/dim] [cyan]{_shorten_path(str(USER_SKILLS_DIR))}[/cyan]") + user_skills = [s for s in skills if s.source == "user"] + system_skills = [s for s in skills if s.source == "system"] + + if user_skills: + console.print(f"[bold]User Skills[/bold] ({len(user_skills)}):") + for skill in user_skills: + console.print(f" [green]{skill.name}[/green] - {skill.description}") + + if user_skills and system_skills: + console.print() + + if system_skills: + console.print(f"[bold]Built-in Skills[/bold] ({len(system_skills)}):") + for skill in system_skills: + console.print(f" [cyan]{skill.name}[/cyan] - {skill.description}") + + console.print(f"\n[dim]User skills folder:[/dim] [green]{_shorten_path(str(USER_SKILLS_DIR))}[/green]") console.print() diff --git a/EvoScientist/config/onboard.py b/EvoScientist/config/onboard.py index 87bcaab..b6398b0 100644 --- a/EvoScientist/config/onboard.py +++ b/EvoScientist/config/onboard.py @@ -644,13 +644,15 @@ def _step_workspace(config: EvoScientistConfig) -> tuple[str, str]: Tuple of (mode, workdir). """ # Mode selection + cwd = os.getcwd() + cwd_short = os.path.basename(cwd) or cwd mode_choices = [ Choice( - title="Daemon (persistent workspace ./workspace/)", + title="Daemon (persistent workspace)", value="daemon", ), Choice( - title="Run (isolated per-session ./workspace/runs//)", + title="Run (isolated per-session)", value="run", ), ] @@ -668,27 +670,16 @@ def _step_workspace(config: EvoScientistConfig) -> tuple[str, str]: raise KeyboardInterrupt() # Custom workdir (optional) - use_custom = questionary.confirm( - "Use custom workspace directory? (default: ./workspace/)", - default=bool(config.default_workdir), + current_default = config.default_workdir or "" + workdir = questionary.text( + f"Workspace directory (Enter to use ./{cwd_short}/):", + default=current_default, style=WIZARD_STYLE, qmark=QMARK, ).ask() - - if use_custom is None: + if workdir is None: raise KeyboardInterrupt() - - workdir = "" - if use_custom: - workdir = questionary.text( - "Workspace directory path:", - default=config.default_workdir or "", - style=WIZARD_STYLE, - qmark=QMARK, - ).ask() - if workdir is None: - raise KeyboardInterrupt() - workdir = workdir.strip() + workdir = workdir.strip() return mode, workdir diff --git a/EvoScientist/paths.py b/EvoScientist/paths.py index df288ae..b5e3c4b 100644 --- a/EvoScientist/paths.py +++ b/EvoScientist/paths.py @@ -18,8 +18,8 @@ def _env_path(key: str) -> Path | None: return _expand(value) -# Workspace root: directly under cwd (no hidden .evoscientist layer) -WORKSPACE_ROOT = _env_path("EVOSCIENTIST_WORKSPACE_DIR") or (Path.cwd() / "workspace") +# Workspace root: current working directory by default (user's project dir) +WORKSPACE_ROOT = _env_path("EVOSCIENTIST_WORKSPACE_DIR") or Path.cwd() RUNS_DIR = _env_path("EVOSCIENTIST_RUNS_DIR") or (WORKSPACE_ROOT / "runs") MEMORY_DIR = _env_path("EVOSCIENTIST_MEMORY_DIR") or (WORKSPACE_ROOT / "memory") @@ -27,8 +27,12 @@ USER_SKILLS_DIR = _env_path("EVOSCIENTIST_SKILLS_DIR") or (WORKSPACE_ROOT / "ski def ensure_dirs() -> None: - """Create runtime directories if they do not exist.""" - for path in (WORKSPACE_ROOT, RUNS_DIR, MEMORY_DIR, USER_SKILLS_DIR): + """Create runtime subdirectories (memory, skills) if they do not exist. + + Does NOT create the workspace root itself — it should already exist + (either the user's cwd or a directory they specified). + """ + for path in (MEMORY_DIR, USER_SKILLS_DIR): path.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_onboard.py b/tests/test_onboard.py index c981a18..fdd3d51 100644 --- a/tests/test_onboard.py +++ b/tests/test_onboard.py @@ -1,5 +1,6 @@ """Tests for EvoScientist onboarding wizard.""" +import os import subprocess from unittest.mock import patch, Mock, MagicMock @@ -257,14 +258,14 @@ class TestStepModel: class TestStepWorkspace: def test_returns_mode_and_empty_workdir(self): - """Test workspace step with no custom directory.""" + """Test workspace step with no custom directory (user keeps cwd default).""" from EvoScientist.config.onboard import _step_workspace config = EvoScientistConfig() with patch("EvoScientist.config.onboard.questionary") as mock_q: mock_q.select.return_value.ask.return_value = "daemon" - mock_q.confirm.return_value.ask.return_value = False # No custom dir + mock_q.text.return_value.ask.return_value = "" # Keep default (empty = use cwd) result = _step_workspace(config) assert result == ("daemon", "") @@ -277,7 +278,6 @@ class TestStepWorkspace: with patch("EvoScientist.config.onboard.questionary") as mock_q: mock_q.select.return_value.ask.return_value = "run" - mock_q.confirm.return_value.ask.return_value = True # Use custom dir mock_q.text.return_value.ask.return_value = "/custom/path" result = _step_workspace(config) @@ -686,10 +686,10 @@ class TestRunOnboard: "", # Tavily key (keep current) ] mock_q.confirm.return_value.ask.side_effect = [ - False, # Custom workdir True, # Save config ] mock_q.text.return_value.ask.side_effect = [ + "", # Workspace directory (empty = use cwd) "3", # Max concurrent "3", # Max iterations ] @@ -735,10 +735,13 @@ class TestRunOnboard: ] mock_q.password.return_value.ask.side_effect = ["", ""] mock_q.confirm.return_value.ask.side_effect = [ - False, # Custom workdir False, # Save config - NO ] - mock_q.text.return_value.ask.side_effect = ["3", "3"] + mock_q.text.return_value.ask.side_effect = [ + "", # Workspace directory (empty = use cwd) + "3", # Max concurrent + "3", # Max iterations + ] mock_q.checkbox.return_value.ask.return_value = [] # Skills: skip result = run_onboard(skip_validation=True)