Depends on: #587 (Extract kild-git crate)
Blocked until: A second isolation backend is needed (container, VM, etc.)
Problem
Session lifecycle code is hardcoded to git worktrees as the only isolation mechanism. Git calls are spread across `create_session()`, `destroy_session()`, and `complete_session()` with no abstraction boundary — making it impossible to add alternative isolation backends (containers, VMs) without rewriting session lifecycle code.
| Location |
Git operation |
Severity |
| `create.rs:113-116` |
`detect_project()` → `Repository::discover()` |
Hard — blocks all creation |
| `create.rs:210-217` |
`create_worktree()` → `repo.branch()`, `repo.worktree()` |
Hard — blocks non-`--main` creation |
| `destroy.rs:308-350` |
`find_main_repo_root()`, `remove_worktree_*()`, `delete_branch_if_exists()` |
Hard/Soft mix |
| `destroy.rs:378-441` |
`has_remote_configured()`, `get_worktree_status()` |
Soft — safety checks |
| `complete.rs:53-174` |
`kild_branch_name()`, `delete_remote_branch()`, then full destroy |
Delegates to destroy |
| `session.rs:21-96` |
`project_id`, `branch`, `worktree_path`, `use_main_worktree` fields |
Data model assumption |
The codebase already has three proven trait+registry+factory patterns (`TerminalBackend`, `ForgeBackend`, `AgentBackend`) that solve this exact problem for other domains.
Key Design Decision: Worktrees are git, not isolation
Worktrees are git's built-in branch isolation — orthogonal to environment isolation. A container backend would still mount a worktree inside Docker. They compose:
```
kild-git → "what code does the agent see?" (worktree = branch isolation)
IsolationBackend → "what environment does it run in?" (host / container / VM)
session lifecycle → composes both
```
Example flows:
```
Host (current): create worktree → agent runs directly in worktree directory
Container (future): create worktree → docker run -v :/workspace → agent runs in container
VM (future): create worktree → mount worktree in VM → agent runs in VM
No worktree (--main): skip worktree → agent runs in project root (host or container)
```
Prerequisite: #587
The `kild-git` crate extraction (#587) must land first. It:
- Gives us a clean git API boundary that the isolation trait will compose with
- Eliminates the 5 direct `git2` leaks outside the git module
- Makes the isolation trait's composition point obvious: `kild-git` for code isolation, `IsolationBackend` for environment isolation
When to implement this issue
Not now. The `IsolationBackend` trait is premature until there's a second backend to justify the abstraction (YAGNI). With only `HostBackend` (a no-op), the trait adds indirection for zero value.
Trigger: When we're ready to implement container or VM isolation, extract the trait then. The `kild-git` crate boundary (#587) ensures the extraction will be straightforward — the API surface is already the exact seam.
Proposal (for when the time comes)
Trait
```rust
pub trait IsolationBackend: Send + Sync {
fn name(&self) -> &'static str;
fn display_name(&self) -> &'static str;
fn is_available(&self) -> bool;
/// Prepare the execution environment around a working directory.
fn create(&self, request: &IsolationRequest) -> Result<IsolationEnv, IsolationError>;
/// Tear down the execution environment.
fn destroy(&self, env: &IsolationEnv) -> Result<(), IsolationError>;
/// Transform the agent command to run inside the environment.
/// Host backend returns the command unchanged.
/// Container backend wraps it in \`docker exec -it <id>\`.
fn wrap_command(&self, env: &IsolationEnv, command: &str) -> String {
command.to_string()
}
/// Backend-specific safety warnings before destroy.
fn safety_info(&self, env: &IsolationEnv) -> Result<SafetyInfo, IsolationError> {
Ok(SafetyInfo::default())
}
}
```
Key types
```rust
pub struct IsolationRequest {
pub working_dir: PathBuf, // worktree path or project root
pub session_name: BranchName,
pub config: IsolationConfig, // backend-specific (image, mounts, etc.)
}
pub struct IsolationEnv {
pub working_dir: PathBuf, // may differ from request (e.g., /workspace in container)
pub backend_name: String,
pub metadata: IsolationMeta,
}
#[non_exhaustive]
pub enum IsolationMeta {
Host,
// Future: Container { id: String, image: String },
// Future: Vm { id: String },
}
```
Session lifecycle flow (after)
```
create_session():
- detect_project() ← kild-git
- if !use_main: create_worktree() ← kild-git
- isolation.create(working_dir) ← IsolationBackend
- wrap_command(agent_command) ← IsolationBackend
- spawn agent in daemon/terminal
destroy_session():
- isolation.destroy() ← IsolationBackend
- remove_worktree() ← kild-git
- delete_branch() ← kild-git
```
Session struct changes
```rust
pub struct Session {
#[serde(alias = "worktree_path")]
pub working_dir: PathBuf,
#[serde(default)]
pub isolation: IsolationMeta,
// project_id and branch stay unchanged
}
```
Config
```toml
[isolation]
default = "host" # only option initially
Future:
[isolation.container]
image = "ubuntu:24.04"
mount_source = true
```
Non-goals
- Container, VM, devcontainer, or Nix backends (separate issues per backend)
- Changing project detection to work without git (separate concern)
- Renaming `BranchName` → `SessionName`
References
Problem
Session lifecycle code is hardcoded to git worktrees as the only isolation mechanism. Git calls are spread across `create_session()`, `destroy_session()`, and `complete_session()` with no abstraction boundary — making it impossible to add alternative isolation backends (containers, VMs) without rewriting session lifecycle code.
The codebase already has three proven trait+registry+factory patterns (`TerminalBackend`, `ForgeBackend`, `AgentBackend`) that solve this exact problem for other domains.
Key Design Decision: Worktrees are git, not isolation
Worktrees are git's built-in branch isolation — orthogonal to environment isolation. A container backend would still mount a worktree inside Docker. They compose:
```
kild-git → "what code does the agent see?" (worktree = branch isolation)
IsolationBackend → "what environment does it run in?" (host / container / VM)
session lifecycle → composes both
```
Example flows:
```
Host (current): create worktree → agent runs directly in worktree directory
Container (future): create worktree → docker run -v :/workspace → agent runs in container
VM (future): create worktree → mount worktree in VM → agent runs in VM
No worktree (--main): skip worktree → agent runs in project root (host or container)
```
Prerequisite: #587
The `kild-git` crate extraction (#587) must land first. It:
When to implement this issue
Not now. The `IsolationBackend` trait is premature until there's a second backend to justify the abstraction (YAGNI). With only `HostBackend` (a no-op), the trait adds indirection for zero value.
Trigger: When we're ready to implement container or VM isolation, extract the trait then. The `kild-git` crate boundary (#587) ensures the extraction will be straightforward — the API surface is already the exact seam.
Proposal (for when the time comes)
Trait
```rust
pub trait IsolationBackend: Send + Sync {
fn name(&self) -> &'static str;
fn display_name(&self) -> &'static str;
fn is_available(&self) -> bool;
}
```
Key types
```rust
pub struct IsolationRequest {
pub working_dir: PathBuf, // worktree path or project root
pub session_name: BranchName,
pub config: IsolationConfig, // backend-specific (image, mounts, etc.)
}
pub struct IsolationEnv {
pub working_dir: PathBuf, // may differ from request (e.g., /workspace in container)
pub backend_name: String,
pub metadata: IsolationMeta,
}
#[non_exhaustive]
pub enum IsolationMeta {
Host,
// Future: Container { id: String, image: String },
// Future: Vm { id: String },
}
```
Session lifecycle flow (after)
```
create_session():
destroy_session():
```
Session struct changes
```rust
pub struct Session {
#[serde(alias = "worktree_path")]
pub working_dir: PathBuf,
#[serde(default)]
pub isolation: IsolationMeta,
// project_id and branch stay unchanged
}
```
Config
```toml
[isolation]
default = "host" # only option initially
Future:
[isolation.container]
image = "ubuntu:24.04"
mount_source = true
```
Non-goals
References