This guide covers development setup, project architecture, and contribution guidelines.
- Python 3.13
- Git
- uv for Python package management
# Clone the repository
git clone https://github.com/ckrough/agentspaces.git
cd agentspaces
# Install all dependencies including dev tools
uv sync --all-extras
# Verify installation
uv run agentspaces --version
# Run tests to confirm setup
uv run pytestAlways use uv run to execute commands:
uv run agentspaces --help
uv run agentspaces workspace listsrc/agentspaces/
├── __init__.py # Package version
├── main.py # CLI entry point (app)
├── cli/ # CLI layer - Typer commands
│ ├── app.py # Main app, global options
│ ├── workspace.py # Workspace subcommands
│ ├── agent.py # Agent subcommands
│ ├── context.py # CLI context (verbosity state)
│ └── formatters.py # Rich output formatting
├── modules/ # Business logic layer
│ ├── workspace/
│ │ ├── service.py # WorkspaceService - core operations
│ │ ├── worktree.py # Git worktree operations
│ │ ├── environment.py # Python venv setup
│ │ └── models.py # Data models (WorkspaceInfo, etc.)
│ └── agent/
│ └── launcher.py # Agent launching logic
└── infrastructure/ # Shared utilities
├── git.py # Git subprocess wrapper
├── naming.py # Name generation (adjective-scientist)
├── paths.py # Path resolution
├── similarity.py # String similarity for suggestions
└── logging.py # structlog configuration
CLI Layer (cli/): Handles user interaction, argument parsing, and output formatting. Uses Typer for command definitions and Rich for styled output.
Module Layer (modules/): Contains business logic. Each module (workspace, agent) has its own service class that orchestrates operations. Services use constructor dependency injection for testability.
Infrastructure Layer (infrastructure/): Shared utilities used across modules. Git operations are performed via subprocess (not a Git library). Logging uses structlog for structured output.
User runs: agentspaces workspace create main
1. cli/workspace.py: create() command handler
2. modules/workspace/service.py: WorkspaceService.create()
3. modules/workspace/worktree.py: create_worktree()
4. infrastructure/git.py: Git subprocess calls
5. modules/workspace/environment.py: setup_venv()
6. Return WorkspaceInfo to CLI
7. cli/formatters.py: print_workspace_created()
# Run all tests
uv run pytest
# Run with coverage report
uv run pytest --cov=src --cov-report=term-missing
# Run specific test file
uv run pytest tests/unit/test_naming.py
# Run tests matching a pattern
uv run pytest -k "test_create"
# Run with verbose output
uv run pytest -vtests/
├── conftest.py # Shared fixtures
├── unit/ # Unit tests (fast, isolated)
│ ├── test_naming.py
│ ├── test_paths.py
│ └── ...
└── integration/ # Integration tests (may use filesystem)
└── ...
Use pytest with descriptive names following the pattern test_<function>_<scenario>_<expected>:
def test_generate_name_returns_adjective_scientist_format():
"""Names should be formatted as adjective-scientist."""
name = generate_name()
parts = name.split("-")
assert len(parts) == 2
def test_create_workspace_with_invalid_branch_raises_error():
"""Creating a workspace from non-existent branch should fail."""
with pytest.raises(WorkspaceError, match="branch not found"):
service.create(base_branch="nonexistent-branch")- Target: 80% coverage on business logic
- Excluded from coverage: CLI layer, logging config, main entry point
- Run
uv run pytest --cov=srcto check coverage
# Check for lint issues
uv run ruff check src/ tests/
# Auto-fix lint issues
uv run ruff check src/ tests/ --fix
# Format code
uv run ruff format src/ tests/# Run mypy with strict mode
uv run mypy src/Run all checks before committing:
uv run ruff check src/ tests/ --fix && \
uv run ruff format src/ tests/ && \
uv run mypy src/ && \
uv run pytestTarget Python 3.13. Use modern Python features:
- Type hints on all function signatures (including
-> None) collections.abctypes for abstract containers- Union with
|syntax:int | str - Pattern matching where appropriate
Order: stdlib, third-party, local. Ruff handles sorting automatically.
from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING
import typer
from rich.console import Console
from agentspaces.infrastructure import git
from agentspaces.modules.workspace.models import WorkspaceInfoCreate meaningful exceptions in each module:
class WorkspaceError(Exception):
"""Base exception for workspace operations."""
class WorkspaceNotFoundError(WorkspaceError):
"""Raised when a workspace doesn't exist."""Chain exceptions with from:
try:
git.checkout(branch)
except git.GitError as e:
raise WorkspaceError(f"Failed to checkout: {e}") from eGoogle-style docstrings for public APIs:
def create(
self,
base_branch: str,
purpose: str | None = None,
) -> WorkspaceInfo:
"""Create a new isolated workspace.
Args:
base_branch: Branch to create workspace from.
purpose: Optional description of workspace purpose.
Returns:
Information about the created workspace.
Raises:
WorkspaceError: If workspace creation fails.
"""- Add command function to appropriate CLI module (
cli/workspace.pyorcli/agent.py):
@app.command("new-command")
def new_command(
arg: Annotated[str, typer.Argument(help="Argument description")],
) -> None:
"""Brief description of command.
Detailed description here.
\b
Examples:
agentspaces workspace new-command foo
agentspaces workspace new-command bar --option
"""
try:
result = _service.new_operation(arg)
print_success(f"Done: {result}")
except WorkspaceError as e:
print_error(str(e))
raise typer.Exit(1) from e- Add business logic to service class (
modules/workspace/service.py) - Add tests for both the service method and CLI command
- Update command reference in README.md
- Create module in
infrastructure/ - Keep it focused on a single concern
- Add comprehensive unit tests
- Use from service layer, not directly from CLI
This walkthrough adds a "workspace info" command that shows metadata.
Step 1: Add the service method
# modules/workspace/service.py
def get_info(self, name: str) -> dict[str, str]:
"""Get workspace metadata.
Args:
name: Workspace name.
Returns:
Dictionary of metadata key-value pairs.
Raises:
WorkspaceNotFoundError: If workspace doesn't exist.
"""
workspace = self.get(name)
metadata_file = workspace.path / ".agentspace" / "metadata.json"
if not metadata_file.exists():
return {}
return json.loads(metadata_file.read_text())Step 2: Add the CLI command
# cli/workspace.py
@app.command("info")
def info(
name: Annotated[str, typer.Argument(help="Workspace name")],
) -> None:
"""Show workspace metadata.
\b
Examples:
agentspaces workspace info eager-turing
"""
try:
metadata = _service.get_info(name)
except WorkspaceNotFoundError:
print_error(f"Workspace not found: {name}")
raise typer.Exit(1) from None
if not metadata:
print_info("No metadata found")
return
for key, value in metadata.items():
print_info(f"{key}: {value}")Step 3: Add tests
# tests/unit/test_workspace_service.py
def test_get_info_returns_metadata(tmp_path, service):
"""get_info should return metadata from .agentspace/metadata.json."""
# Setup
workspace = service.create("main")
metadata_file = workspace.path / ".agentspace" / "metadata.json"
metadata_file.write_text('{"purpose": "Test"}')
# Test
result = service.get_info(workspace.name)
# Verify
assert result == {"purpose": "Test"}
def test_get_info_nonexistent_workspace_raises(service):
"""get_info should raise for non-existent workspace."""
with pytest.raises(WorkspaceNotFoundError):
service.get_info("nonexistent")Step 4: Run checks
uv run ruff check src/ tests/ --fix
uv run mypy src/
uv run pytest -k "test_get_info"- Create a feature branch:
git checkout -b feature/description - Make focused, incremental changes
- Run all checks before committing
- Write clear commit messages
- Update documentation if adding features
- Ensure tests pass and coverage is maintained
<type>: <description>
<optional body>
Types: feat, fix, refactor, docs, test, chore
Examples:
feat: add workspace sync command
fix: handle missing .python-version file
refactor: extract path resolution to infrastructure
docs: update README with new commands
test: add coverage for edge cases in naming
For version management and release procedures, see RELEASING.md.
Open an issue on GitHub for questions about contributing.