docs(modules): add git worktree sandbox and context hydration docs
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 20s
CI / helm (pull_request) Successful in 23s
CI / build (pull_request) Successful in 27s
CI / quality (pull_request) Successful in 41s
CI / lint (pull_request) Successful in 40s
CI / security (pull_request) Successful in 59s
CI / typecheck (pull_request) Successful in 1m8s
CI / e2e_tests (pull_request) Successful in 3m16s
CI / integration_tests (pull_request) Successful in 4m2s
CI / unit_tests (pull_request) Successful in 5m2s
CI / docker (pull_request) Successful in 21s
CI / coverage (pull_request) Successful in 10m36s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Has been cancelled

ISSUES CLOSED: #6841
This commit is contained in:
2026-04-09 21:01:50 +00:00
parent 51aab18411
commit ea21723a52
3 changed files with 481 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
# Context Tier Hydration
The **Context Tier Hydrator** bridges the gap between the resource registry (files on
disk) and the ACMS `ContextTierService` (in-memory fragments). Without it, the tier
service starts empty on every CLI process invocation and the LLM receives zero file
context during plan execution.
Introduced in PR #5998 (merged 2026-04-09), fixing bug #1028.
---
## Problem
The Advanced Context Management System (ACMS) stores file content as `TieredFragment`
objects in `ContextTierService`. This service is in-memory and is not persisted
between CLI invocations. When a plan is executed, the LLM actor calls the context
assembler to build a context window from the tier service — but the service is empty
because no files have been loaded.
The hydrator solves this by reading files from the project's linked resources and
storing them as `TieredFragment` objects before context assembly begins.
---
## Architecture
```
CLI: agents plan execute
LLMExecuteActor.execute()
├─── hydrate_tiers_for_plan() ← Context Tier Hydrator
│ │
│ ├── project_repository.get(project_name)
│ ├── resource_registry.show_resource(resource_id)
│ └── hydrate_tiers_from_project()
│ │
│ ├── _list_files()
│ │ ├── git ls-files (git-checkout resources)
│ │ └── os.walk (other resource types)
│ └── tier_service.store(TieredFragment)
└─── context_assembler.assemble() ← reads from tier_service
```
---
## Key Functions
**Module**: `cleveragents.application.services.context_tier_hydrator`
### `hydrate_tiers_for_plan`
```python
def hydrate_tiers_for_plan(
tier_service: ContextTierService,
project_names: list[str],
project_repository: Any,
resource_registry: Any,
) -> int:
```
Top-level entry point. Iterates over all projects linked to a plan, resolves their
linked resources, and calls `hydrate_tiers_from_project` for each.
**Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| `tier_service` | `ContextTierService` | The tier service to populate |
| `project_names` | `list[str]` | Namespaced project names (e.g. `local/my-project`) |
| `project_repository` | `NamespacedProjectRepository` | Repository for project lookups |
| `resource_registry` | `ResourceRegistryService` | Registry for resource lookups |
**Returns**: Total number of `TieredFragment` objects stored across all projects.
---
### `hydrate_tiers_from_project`
```python
def hydrate_tiers_from_project(
tier_service: ContextTierService,
project_name: str,
resource_id: str,
resource_location: str,
resource_type: str = "git-checkout",
) -> int:
```
Reads files from a single resource directory and stores them as `TieredFragment`
objects in the tier service.
**Parameters**:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `tier_service` | `ContextTierService` | — | The tier service to populate |
| `project_name` | `str` | — | Namespaced project name |
| `resource_id` | `str` | — | ULID of the resource |
| `resource_location` | `str` | — | Filesystem path to the resource root |
| `resource_type` | `str` | `"git-checkout"` | Resource type (affects file listing strategy) |
**Returns**: Number of fragments stored.
**Limits enforced**:
| Limit | Value | Description |
|-------|-------|-------------|
| Max file size | 256 KB | Files larger than this are skipped |
| Max total bytes | 10 MB | Hydration stops when this budget is reached |
---
## File Listing Strategy
The hydrator uses two strategies to enumerate files, selected by resource type:
### `git ls-files` (git-checkout resources)
For `git-checkout` and `git` resource types, the hydrator runs:
```bash
git ls-files --cached --others --exclude-standard
```
This lists tracked files and untracked files that are not gitignored. Binary
extensions (`.pyc`, `.so`, `.png`, etc.) are filtered out.
Falls back to `os.walk` if the git command fails or times out (30 s timeout).
### `os.walk` (other resource types)
For non-git resources, the hydrator walks the directory tree, skipping:
**Directories**: `.git`, `.hg`, `.svn`, `__pycache__`, `node_modules`, `.venv`,
`venv`, `.nox`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, `dist`,
`build`, `.eggs`, `.cleveragents`, and any directory starting with `.` or ending
with `.egg-info`.
**File extensions**: `.pyc`, `.pyo`, `.so`, `.o`, `.a`, `.dll`, `.exe`, `.png`,
`.jpg`, `.jpeg`, `.gif`, `.bmp`, `.ico`, `.pdf`, `.zip`, `.tar`, `.gz`, `.bz2`,
`.xz`, `.whl`, `.egg`, `.db`, `.sqlite`, `.sqlite3`.
**Hidden files**: Files starting with `.` are skipped.
---
## Fragment Format
Each file is stored as a `TieredFragment` with:
| Field | Value |
|-------|-------|
| `fragment_id` | `<resource_id>:<relative_path>` |
| `content` | UTF-8 file content |
| `tier` | `ContextTier.HOT` |
| `resource_id` | Resource ULID |
| `project_name` | Namespaced project name |
| `token_count` | `len(content) // 4` (rough estimate) |
| `metadata.path` | Relative path within the resource |
| `metadata.detail_depth` | `"1"` (string, not int) |
| `metadata.relevance_score` | `"0.5"` (string, not float) |
!!! note "Metadata types"
`detail_depth` and `relevance_score` must be **strings**, not `int` or `float`.
Pydantic v2 validation on `ContextFragment` enforces this. Passing numeric
values causes a `ValidationError` that silently drops all context from the LLM
call. This was the root cause of bug #1028.
---
## Usage Example
The hydrator is called automatically by `LLMExecuteActor.execute()` before context
assembly. You can also call it directly for testing or custom integrations:
```python
from cleveragents.application.services.context_tier_hydrator import (
hydrate_tiers_from_project,
)
from cleveragents.application.services.context_tiers import ContextTierService
tier_service = ContextTierService()
count = hydrate_tiers_from_project(
tier_service=tier_service,
project_name="local/my-project",
resource_id="01ABCDEF...",
resource_location="/path/to/repo",
resource_type="git-checkout",
)
print(f"Stored {count} fragments")
```
---
## Logging
The hydrator emits structured log events via `structlog`:
| Event | Level | Fields |
|-------|-------|--------|
| `context_hydrator.skip_missing_location` | WARNING | `project`, `resource_id`, `location` |
| `context_hydrator.store_failed` | DEBUG | `fragment_id`, `exc_info` |
| `context_hydrator.hydrated` | INFO | `project`, `resource_id`, `fragments_stored`, `total_bytes` |
| `context_hydrator.project_not_found` | DEBUG | `project` |
| `context_hydrator.no_linked_resources` | DEBUG | `project` |
| `context_hydrator.resource_not_found` | DEBUG | `resource_id` |
---
## Gotchas
1. **Binary files are silently skipped**. If a file cannot be decoded as UTF-8, it
is skipped without error.
2. **The 10 MB budget is per-hydration call**, not per-project. If a project has
many large files, the budget may be exhausted before all files are indexed.
3. **`git ls-files` requires a 30-second timeout**. On very large repositories or
slow disks, the command may time out and fall back to `os.walk`.
4. **Fragments are stored in the HOT tier**. All files are treated as equally
relevant. Future versions may use relevance scoring to assign files to
HOT/WARM/COLD tiers.
---
## Related
- [ADR-014 Context Management (ACMS)](../adr/ADR-014-context-management-acms.md)
- [Git Worktree Sandbox](git-worktree-sandbox.md)
- [API Reference — ACMS](../api/core.md)
+244
View File
@@ -0,0 +1,244 @@
# Git Worktree Sandbox
The **Git Worktree Sandbox** provides isolated, reversible staging for LLM-generated
file changes during plan execution. It uses `git worktree` to create a detached
working tree on a dedicated branch, so modifications never touch the original branch
until the plan is explicitly applied.
Introduced in PR #5998 (merged 2026-04-09) as part of the M1 plan lifecycle
implementation.
---
## Overview
When a plan is executed against a `git-checkout` resource, CleverAgents creates a
temporary git worktree on a branch named `cleveragents/plan-<plan_id>`. The LLM
actor writes files into this worktree. On `plan apply`, the sandbox branch is merged
back into the project's current branch via `git merge`. If anything goes wrong, the
sandbox can be rolled back without affecting the original working tree.
Non-git projects fall back to the flat directory sandbox (`shutil.copy2`).
---
## Lifecycle
```
create(plan_id)
│ Creates branch cleveragents/plan-<plan_id>
│ Creates worktree at /tmp/ca-sandbox-<plan_id>-<random>/
get_path(resource_path)
│ Translates resource-relative paths to worktree paths
│ Actor writes files here
commit(message)
│ git add -A → git commit → git merge <branch> (in original repo)
cleanup()
│ git worktree remove --force
│ git branch -D cleveragents/plan-<plan_id>
│ git worktree prune
CLEANED_UP
```
Rollback is available from `ACTIVE` or `COMMITTED` states:
```
rollback()
│ ACTIVE → git reset --hard <base_commit> + git clean -fd (in worktree)
│ COMMITTED → git reset --hard <pre_merge_commit> (in original repo)
│ + git reset --hard <base_commit> (in worktree)
ROLLED_BACK
```
---
## Status State Machine
| Status | Meaning |
|--------|---------|
| `PENDING` | Sandbox created but `create()` not yet called |
| `CREATED` | Worktree and branch exist; no writes yet |
| `ACTIVE` | Actor has written at least one file |
| `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`
**Module**: `cleveragents.infrastructure.sandbox.git_worktree`
```python
from cleveragents.infrastructure.sandbox.git_worktree import GitWorktreeSandbox
sandbox = GitWorktreeSandbox(
resource_id="01ABCDEF...", # ULID of the git-checkout resource
original_path="/path/to/repo",
git_timeout=30, # seconds per git command (default: 30)
)
# Create the worktree
ctx = sandbox.create(plan_id="01PLAN...")
print(ctx.sandbox_path) # /tmp/ca-sandbox-01PLAN-xxxx/
print(ctx.metadata["branch"]) # cleveragents/plan-01PLAN...
# Resolve a path inside the worktree
worktree_file = sandbox.get_path("src/calculator.py")
# After the actor writes files...
result = sandbox.commit("feat: implement calculator")
print(result.commit_ref) # git SHA of the sandbox commit
print(result.added_files) # ["src/calculator.py"]
# Cleanup
sandbox.cleanup()
```
### Constructor Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resource_id` | `str` | — | ULID 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.
- Verifies `original_path` is a git repository root (not a subdirectory).
- Creates branch `cleveragents/plan-<sanitised_plan_id>`.
- Creates a temporary directory for the worktree.
- Returns a `SandboxContext` with `sandbox_path`, `original_path`, `resource_id`,
`plan_id`, `created_at`, and `metadata` (strategy, branch, original_branch,
base_commit, worktree_path).
#### `get_path(resource_path: str) → str`
Translates a resource-relative path to an absolute path inside the worktree.
Raises `ValueError` on directory traversal attempts (`..` components).
#### `commit(message: str | None = None) → CommitResult`
Stages all changes (`git add -A`), commits them in the worktree, then merges
the sandbox branch into the original branch (`git merge --no-edit`).
Returns a `CommitResult` with:
| Field | Type | Description |
|-------|------|-------------|
| `sandbox_id` | `str` | Sandbox ULID |
| `success` | `bool` | `True` on success |
| `commit_ref` | `str` | SHA of the sandbox commit |
| `changed_files` | `list[str]` | Modified files |
| `added_files` | `list[str]` | New files |
| `deleted_files` | `list[str]` | Removed files |
| `error` | `str \| None` | Error message on failure |
| `timestamp` | `datetime` | Commit timestamp |
If there are no staged changes, returns a `CommitResult` with `success=True`
and empty file lists (no-op commit).
#### `rollback() → None`
Discards all changes. Behaviour depends on current status:
- **`ACTIVE`**: Resets the worktree branch to `base_commit` and runs `git clean -fd`.
- **`COMMITTED`**: Also resets the original branch to `pre_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.
#### `cleanup() → None`
Removes the worktree and deletes the sandbox branch. Idempotent — safe to call
multiple times. Falls back to `shutil.rmtree` if `git worktree remove` fails.
---
## Error Types
All errors are defined in `cleveragents.infrastructure.sandbox.protocol`.
| Exception | When raised |
|-----------|-------------|
| `SandboxCreationError` | `git worktree add` or prerequisite git commands fail |
| `SandboxCommitError` | `git commit` or `git merge` fails |
| `SandboxRollbackError` | `git reset` or `git clean` fails |
| `SandboxStateError` | Method called in an invalid status |
---
## Branch Naming
Branch names are sanitised to be git-safe:
- Only alphanumeric characters, hyphens, underscores, slashes, and dots are kept.
- Runs of disallowed characters are collapsed into a single hyphen.
- The final name is `cleveragents/plan-<sanitised_plan_id>`.
Example: plan ID `01JQABC-xyz!foo` → branch `cleveragents/plan-01JQABC-xyz-foo`.
---
## Integration with Plan Lifecycle
The `GitWorktreeSandbox` is instantiated by the CLI `plan execute` command when the
plan's linked resource is a `git-checkout` type. The `plan apply` command calls
`commit()` followed by `cleanup()`.
```
agents plan execute <plan_id>
→ GitWorktreeSandbox.create(plan_id)
→ LLM actor writes files via get_path()
agents plan apply <plan_id>
→ GitWorktreeSandbox.commit("cleveragents: apply plan <plan_id>")
→ GitWorktreeSandbox.cleanup()
→ Displays Apply Summary panel
```
The Apply Summary panel shows:
- Plan ID
- Artifact count (added + changed files)
- Insertions / deletions (from `git diff --stat`)
- Project name
- Applied-at timestamp
---
## Gotchas
1. **`original_path` must be the repository root**, not a subdirectory. The sandbox
verifies this with `git rev-parse --show-toplevel`.
2. **Merge conflicts are not auto-resolved**. If the original branch has diverged
since the sandbox was created, `git merge` may fail. The sandbox transitions to
`ERRORED` and raises `SandboxCommitError`.
3. **Cleanup is not automatic on error**. Always call `cleanup()` in a `finally`
block or use a context manager wrapper to avoid orphaned worktrees.
4. **Git timeout defaults to 30 seconds**. For large repositories or slow disks,
increase `git_timeout` when constructing the sandbox.
---
## Related
- [ADR-015 Sandbox & Checkpoint](../adr/ADR-015-sandbox-and-checkpoint.md)
- [ADR-038 Cross-Mechanism Sandbox Coordination](../adr/ADR-038-cross-mechanism-sandbox-coordination.md)
- [Context Tier Hydration](context-tier-hydration.md)
- [Custom Sandbox Strategy](../development/custom_sandbox_strategy.md)
+2
View File
@@ -27,6 +27,8 @@ nav:
- Shell Safety: modules/shell-safety.md
- UKO Provenance Tracking: modules/uko-provenance.md
- Invariant Reconciliation: modules/invariant-reconciliation.md
- Git Worktree Sandbox: modules/git-worktree-sandbox.md
- Context Tier Hydration: modules/context-tier-hydration.md
- Development:
- CI/CD Pipeline: development/ci-cd.md
- Quality Automation: development/quality-automation.md