- Add docs/modules/git-worktree-sandbox.md: full module guide for GitWorktreeSandbox (lifecycle, state machine, API, errors, apply summary, fallback) - Add docs/api/acms.md: context_tier_hydrator API reference (hydrate_tiers_for_plan/from_project, limits, metadata, structured logs) - Update docs/architecture.md: add Git Worktree Sandbox section and expand ACMS hydrator coverage - Update mkdocs.yml: surface new docs in Modules and API navigation groups - Update docs/api/index.md: add ACMS Services entry to API index - Update CHANGELOG.md: restructure Unreleased entries with Added/Changed/Fixed summary plus existing bug IDs - Correct Introduced versions and remove duplicate changelog entry for ACMS fix, clarify hydrator parameter types ISSUES CLOSED: #6837
8.8 KiB
Git Worktree Sandbox Module
Package: cleveragents.infrastructure.sandbox.git_worktree
Introduced: v3.5.0
The Git Worktree Sandbox provides isolated, git-native sandboxing for
git-checkout resources. Instead of copying files to a temporary directory,
it creates a real git worktree on a dedicated branch. Changes are committed
in the worktree and merged back to the original branch on commit, or
discarded entirely on rollback.
For the sandbox protocol interface, see
docs/development/custom_sandbox_strategy.md.
For the sandbox ADR, see
ADR-015 Sandbox & Checkpoint.
Purpose
The previous flat-copy sandbox (shutil.copy2) had no awareness of git
history, making it impossible to produce meaningful diffs or leverage git's
merge machinery. The git worktree sandbox:
- Preserves full git history for every plan's changes
- Enables
git mergefor conflict detection and resolution - Produces accurate insertions/deletions counts for the Apply Summary panel
- Falls back gracefully to flat-copy for non-git resources
Lifecycle
GitWorktreeSandbox(resource_id, original_path)
│
▼
sandbox.create(plan_id)
│ Creates branch cleveragents/plan-<plan_id>
│ Creates worktree at /tmp/ca-sandbox-<plan_id>-<random>/
▼
sandbox.get_path("src/foo.py")
│ Returns /tmp/ca-sandbox-.../src/foo.py
│ Actor writes changes here
▼
sandbox.commit("feat: ...")
│ git add -A (in worktree)
│ git commit (in worktree)
│ git merge cleveragents/plan-<id> (in original repo)
▼
sandbox.cleanup()
│ git worktree remove --force <path>
│ git branch -D cleveragents/plan-<id>
│ git worktree prune
▼
CLEANED_UP
Rollback path:
sandbox.rollback()
│ If COMMITTED: git reset --hard <pre_merge_commit> (original repo)
│ git reset --hard <base_commit> (worktree)
│ git clean -fd (worktree)
▼
ROLLED_BACK
Status State Machine
| Status | Description |
|---|---|
PENDING |
Sandbox created but create() not yet called |
CREATED |
Worktree and branch exist; no writes yet |
ACTIVE |
get_path() called; actor is writing |
COMMITTED |
Changes committed and merged to original branch |
ROLLED_BACK |
Changes discarded; worktree reset to base commit |
ERRORED |
A git command failed; sandbox is unusable |
CLEANED_UP |
Worktree and branch removed |
Key Class: GitWorktreeSandbox
from cleveragents.infrastructure.sandbox.git_worktree import GitWorktreeSandbox
sandbox = GitWorktreeSandbox(
resource_id="res-01JXYZ",
original_path="/path/to/project",
git_timeout=30, # seconds; default
)
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
resource_id |
str |
— | Identifier of the resource being sandboxed |
original_path |
str |
— | Absolute path to the git repository root |
git_timeout |
int |
30 |
Timeout in seconds for each git command |
Methods
create(plan_id: str) → SandboxContext
Creates the worktree and branch. Must be called before any other method.
- Branch name:
cleveragents/plan-<sanitised_plan_id> - Worktree path:
/tmp/ca-sandbox-<plan_id>-<random>/ SandboxContext.metadataincludesstrategy,branch,original_branch,base_commit, andworktree_path
Raises: SandboxCreationError if git commands fail; SandboxStateError
if not in PENDING status.
get_path(resource_path: str) → str
Translates a resource-relative path to its worktree equivalent.
worktree_file = sandbox.get_path("src/cleveragents/foo.py")
# → /tmp/ca-sandbox-.../src/cleveragents/foo.py
Raises: ValueError on path traversal (.. components); SandboxStateError
if sandbox is not usable.
commit(message: str | None = None) → CommitResult
Stages all changes, commits in the worktree, then merges back to the original
branch. Returns a CommitResult with changed_files, added_files,
deleted_files, and commit_ref.
If there are no staged changes, returns a no-op CommitResult with the base
commit ref and empty file lists.
Raises: SandboxCommitError on git failure; SandboxStateError if not
in CREATED or ACTIVE status.
rollback() → None
Discards all changes:
- From
ACTIVE: resets the worktree branch tobase_commit - From
COMMITTED: also resets the original branch topre_merge_commit(undoes the merge)
!!! warning "Multi-worktree safety"
Rolling back from COMMITTED executes git reset --hard on the original
branch. Other git worktrees or external processes tracking the same branch
will be affected.
Raises: SandboxRollbackError on git failure.
cleanup() → None
Removes the worktree directory and deletes the sandbox branch. Idempotent —
safe to call multiple times. Falls back to shutil.rmtree if
git worktree remove fails.
CommitResult
@dataclass
class CommitResult:
sandbox_id: str
success: bool
commit_ref: str | None # SHA of the commit in the worktree
changed_files: list[str] # Modified files (M status)
added_files: list[str] # New files (A status)
deleted_files: list[str] # Deleted files (D status)
error: str | None
timestamp: datetime
Error Types
| Exception | When raised |
|---|---|
SandboxCreationError |
git worktree add or git rev-parse fails |
SandboxCommitError |
git commit or git merge fails |
SandboxRollbackError |
git reset --hard or git clean fails |
SandboxStateError |
Method called in wrong lifecycle state |
All exceptions are subclasses of SandboxError from
cleveragents.infrastructure.sandbox.protocol.
Non-Git Fallback
For resources that are not git repositories, plan apply falls back to the
original flat-copy strategy (shutil.copy2). The GitWorktreeSandbox itself
raises SandboxCreationError if the original_path is not the root of a git
repository (verified via git rev-parse --show-toplevel).
Usage Example
from cleveragents.infrastructure.sandbox.git_worktree import GitWorktreeSandbox
sandbox = GitWorktreeSandbox(
resource_id="res-01JXYZ",
original_path="/home/user/my-project",
)
ctx = sandbox.create(plan_id="plan-01JABC")
print(f"Worktree at: {ctx.sandbox_path}")
print(f"Branch: {ctx.metadata['branch']}")
# Actor writes files into the worktree
import pathlib
target = pathlib.Path(sandbox.get_path("src/new_feature.py"))
target.write_text("# new feature\n")
# Commit and merge back
result = sandbox.commit("feat: add new_feature module")
print(f"Commit: {result.commit_ref}")
print(f"Added: {result.added_files}")
# Always clean up
sandbox.cleanup()
Apply Summary Output
When plan apply uses the git worktree sandbox, the CLI renders a
spec-aligned Apply Summary panel:
╭─ Apply Summary ──────────────────────────────────────────────╮
│ Plan ID plan-01JABC │
│ Project local/my-project │
│ Timestamp 2026-04-09 14:33:34 UTC │
│ │
│ Artifacts │
│ src/new_feature.py │
│ │
│ +1 insertion 0 deletions │
╰──────────────────────────────────────────────────────────────╯
╭─ Sandbox Cleanup ────────────────────────────────────────────╮
│ Worktree removed /tmp/ca-sandbox-plan-01JABC-xxxxx/ │
│ Branch deleted cleveragents/plan-01JABC │
╰──────────────────────────────────────────────────────────────╯
✓ OK Changes applied
Related Documentation
- ADR-015 Sandbox & Checkpoint — design rationale
- Custom Sandbox Strategy — implementing your own sandbox
- Architecture — Plan Lifecycle — where apply fits
- Shell Safety Module — another infrastructure safety module