feat(workspace): defer directory creation to CLI and improve workspace handling

This commit is contained in:
X-iZhang
2026-02-15 01:10:48 +00:00
parent f80246c48f
commit 94184fe13b
6 changed files with 57 additions and 48 deletions
+1 -2
View File
@@ -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)
+9 -10
View File
@@ -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
+20 -7
View File
@@ -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 <path-or-url>")
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()
+10 -19
View File
@@ -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/<timestamp>/)",
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
+8 -4
View File
@@ -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)
+9 -6
View File
@@ -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)