feat(workspace): defer directory creation to CLI and improve workspace handling
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user