Skip to content

Commit 65b740b

Browse files
committed
Add Docker and Claude Code skill integration
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent e26e3d9 commit 65b740b

4 files changed

Lines changed: 350 additions & 0 deletions

File tree

integration/SKILL.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# branching — Copy-on-Write Workspace Branching for AI Agents
2+
3+
Explore multiple strategies without consequences. Fork the workspace into
4+
parallel copy-on-write branches, run speculative attempts in each, commit
5+
the winner, and abort the rest — instantly.
6+
7+
## Workspace
8+
9+
A workspace is a directory backed by a copy-on-write filesystem (branchfs
10+
or daxfs). All branching operations happen within a workspace.
11+
12+
**Prerequisite:** The workspace must be mounted before use. Two backends
13+
are supported, auto-detected at runtime:
14+
15+
| Backend | Mount |
16+
|---------|-------|
17+
| [BranchFS](https://github.com/multikernel/branchfs) (FUSE) | `branchfs /mnt/workspace` |
18+
| [DaxFS](https://github.com/multikernel/daxfs) (kernel) | `mount -t daxfs <device> /mnt/workspace` |
19+
20+
The CLI auto-detects the workspace from your current directory if you are
21+
inside a branchfs/daxfs mount. Otherwise pass `-w /mnt/workspace`.
22+
23+
## CLI
24+
25+
### branching run — single branch execution
26+
27+
Run a command in an isolated branch. Commits on exit 0, aborts on non-zero.
28+
29+
```bash
30+
branching run -- ./build.sh
31+
branching run --ask -- make test # prompt before commit/abort
32+
branching run --on-error none -- python train.py # keep branch on failure
33+
branching run --memory-limit 512M -- ./agent.sh
34+
```
35+
36+
### branching speculate — race N approaches
37+
38+
Race multiple commands in parallel. First success wins, rest are aborted.
39+
40+
Use when multiple approaches could work and you want the fastest success.
41+
42+
```bash
43+
branching speculate -c "./fix_a.sh" -c "./fix_b.sh" -c "./fix_c.sh"
44+
branching speculate --timeout 60 -c "python v1.py" -c "python v2.py"
45+
```
46+
47+
### branching best-of-n — parallel with scoring
48+
49+
Run the same command N times in parallel. Commit the highest-scoring success.
50+
Child writes score to fd 3 (`echo 0.95 >&3`). Gets `BRANCHING_ATTEMPT` env var.
51+
52+
Use when quality matters: pick the best result across multiple attempts.
53+
54+
```bash
55+
branching best-of-n -n 5 -- ./solve.py
56+
branching best-of-n -n 3 -- bash -c 'python run.py && echo "$SCORE" >&3'
57+
```
58+
59+
### branching reflexion — retry with feedback
60+
61+
Sequential retry with critique. Child gets `BRANCHING_ATTEMPT` and
62+
`BRANCHING_FEEDBACK` (empty on first try, critique output on retries).
63+
64+
Use when failure output helps improve the next attempt.
65+
66+
```bash
67+
branching reflexion --retries 5 -- ./fix.sh
68+
branching reflexion --retries 3 --critique "./review.sh" -- ./solve.py
69+
```
70+
71+
### branching status — workspace info
72+
73+
```bash
74+
branching status
75+
branching status --json
76+
```
77+
78+
### Common CLI flags
79+
80+
| Flag | Description |
81+
|------|-------------|
82+
| `-w PATH` | Workspace path (auto-detected from cwd if omitted) |
83+
| `-b NAME` | Branch name (auto-generated if omitted) |
84+
| `--json` | Machine-readable JSON output |
85+
| `--timeout SEC` | Timeout for parallel commands |
86+
| `--memory-limit SIZE` | Per-branch memory cap (e.g. `512M`, `1G`) |
87+
| `--cpu-limit FRAC` | Per-branch CPU limit (e.g. `0.5` = 50%) |
88+
| `--group-memory-limit SIZE` | Total memory budget for all branches |
89+
| `--group-cpu-limit FRAC` | Total CPU budget for all branches |
90+
91+
## Python API
92+
93+
Install: `pip install branchcontext`
94+
95+
### Workspace and Branch
96+
97+
```python
98+
from branching import Workspace
99+
100+
ws = Workspace("/mnt/workspace")
101+
102+
# Auto-commit on success, auto-abort on exception
103+
with ws.branch("attempt") as b:
104+
# b.path is an isolated copy-on-write view of the workspace
105+
subprocess.run(["agent", "--workdir", str(b.path)], check=True)
106+
107+
# Manual control
108+
with ws.branch("attempt", on_success=None, on_error=None) as b:
109+
result = run_agent(workdir=b.path)
110+
if result.confident:
111+
b.commit()
112+
else:
113+
b.abort()
114+
115+
# Nested branches
116+
with ws.branch("strategy_a") as a:
117+
apply_strategy(a.path)
118+
with a.branch("variant_1") as v1:
119+
tweak(v1.path)
120+
# v1 auto-commits into a on success
121+
# a auto-commits into workspace on success
122+
```
123+
124+
### Speculate — race N candidates, first wins
125+
126+
```python
127+
from branching import Workspace, Speculate
128+
129+
def try_fix_a(path: Path) -> bool:
130+
apply_patch(path / "a.patch")
131+
return run_tests(path)
132+
133+
def try_fix_b(path: Path) -> bool:
134+
apply_patch(path / "b.patch")
135+
return run_tests(path)
136+
137+
outcome = Speculate([try_fix_a, try_fix_b], first_wins=True, timeout=60)(ws)
138+
if outcome.committed:
139+
print(f"Fix {outcome.winner.branch_index} succeeded!")
140+
```
141+
142+
### BestOfN — parallel attempts, highest score wins
143+
144+
```python
145+
from branching import BestOfN
146+
147+
def scored_task(path: Path, attempt: int) -> tuple[bool, float]:
148+
result = run_agent(workdir=path, seed=attempt)
149+
return result.passed, result.quality_score
150+
151+
outcome = BestOfN(scored_task, n=5)(ws)
152+
```
153+
154+
### Reflexion — retry with critique feedback
155+
156+
```python
157+
from branching import Reflexion
158+
159+
def task(path: Path, attempt: int, feedback: str | None) -> bool:
160+
if feedback:
161+
(path / "critique.txt").write_text(feedback)
162+
return run_and_test(path)
163+
164+
def critique(path: Path) -> str:
165+
return analyze_failure(path / "test_output.log")
166+
167+
outcome = Reflexion(task, max_retries=3, critique=critique)(ws)
168+
```
169+
170+
### TreeOfThoughts — hierarchical strategy exploration
171+
172+
```python
173+
from branching import TreeOfThoughts
174+
175+
def strategy_a(path: Path) -> tuple[bool, float]:
176+
apply_approach_a(path)
177+
return run_tests(path), evaluate_quality(path)
178+
179+
outcome = TreeOfThoughts(
180+
[strategy_a, strategy_b],
181+
expand=lambda path, depth: generate_refinements(path),
182+
max_depth=2,
183+
)(ws)
184+
```
185+
186+
### BeamSearch — multi-level with top-K survival
187+
188+
```python
189+
from branching import BeamSearch
190+
191+
outcome = BeamSearch(
192+
[strat_a, strat_b, strat_c, strat_d],
193+
expand=lambda path, depth: generate_refinements(path),
194+
beam_width=2,
195+
max_depth=3,
196+
)(ws)
197+
```
198+
199+
### Tournament — pairwise elimination
200+
201+
```python
202+
from branching import Tournament
203+
204+
def generate_patch(path: Path, index: int) -> bool:
205+
return run_agent(workdir=path, seed=index)
206+
207+
def judge(path_a: Path, path_b: Path) -> int:
208+
# 0 = a wins, 1 = b wins
209+
return llm_compare(path_a / "diff.patch", path_b / "diff.patch")
210+
211+
outcome = Tournament(generate_patch, n=8, judge=judge)(ws)
212+
```
213+
214+
### Process isolation and resource limits
215+
216+
```python
217+
from branching import BranchContext, ResourceLimits
218+
219+
# Run untrusted code in a sandboxed child process
220+
with ws.branch("sandboxed", on_success=None, on_error=None) as b:
221+
with BranchContext(run_untrusted, workspace=b.path) as ctx:
222+
ctx.wait(timeout=30)
223+
b.commit()
224+
225+
# Resource limits (automatically enables process isolation)
226+
limits = ResourceLimits(memory=512 * 1024 * 1024, cpu=0.5)
227+
outcome = BestOfN(scored_task, n=5, resource_limits=limits)(ws)
228+
229+
# All patterns accept resource_limits and group_limits
230+
outcome = Speculate(candidates, resource_limits=limits, timeout=60)(ws)
231+
```
232+
233+
### Result types
234+
235+
```python
236+
# SpeculationOutcome — returned by all patterns
237+
outcome.committed # bool — did a winner get committed?
238+
outcome.winner # SpeculationResult | None
239+
outcome.all_results # list[SpeculationResult]
240+
241+
# SpeculationResult — per-candidate result
242+
result.branch_index # int — which candidate
243+
result.success # bool
244+
result.score # float
245+
result.return_value # Any — raw return from the callable
246+
result.exception # Exception | None
247+
result.branch_path # Path | None
248+
```
249+
250+
## When to use which pattern
251+
252+
| Situation | CLI | Python |
253+
|-----------|-----|--------|
254+
| Try one thing safely, rollback on failure | `branching run` | `ws.branch()` |
255+
| Multiple fix strategies, any success is fine | `branching speculate` | `Speculate` |
256+
| Same task N times, pick the best result | `branching best-of-n` | `BestOfN` |
257+
| Iterative fix with error feedback | `branching reflexion` | `Reflexion` |
258+
| Hierarchical exploration (pick strategy, then refine) || `TreeOfThoughts` |
259+
| Multi-level with multiple survivors per level || `BeamSearch` |
260+
| Pairwise comparison, no absolute scoring || `Tournament` |

integration/docker/Dockerfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM rust:1.86-slim-bookworm AS branchfs-builder
2+
3+
RUN apt-get update && apt-get install -y pkg-config libfuse3-dev && rm -rf /var/lib/apt/lists/*
4+
RUN cargo install branchfs --locked
5+
6+
FROM python:3.12-slim-bookworm
7+
8+
RUN apt-get update \
9+
&& apt-get install -y --no-install-recommends fuse3 \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
COPY --from=branchfs-builder /usr/local/cargo/bin/branchfs /usr/local/bin/
13+
14+
COPY . /src/branching
15+
RUN pip install --no-cache-dir /src/branching && rm -rf /src/branching
16+
17+
RUN mkdir /workspace
18+
19+
COPY docker-entrypoint.sh /usr/local/bin/
20+
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
21+
22+
ENTRYPOINT ["docker-entrypoint.sh"]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
IMAGE="${BRANCHING_IMAGE:-branching}"
5+
WORKSPACE=""
6+
ARGS=""
7+
8+
usage() {
9+
cat <<EOF
10+
Usage: branching-docker -w <workspace> [branching args...]
11+
12+
Run branching inside a Docker container with BranchFS.
13+
14+
The workspace directory is bind-mounted from the host and backed by
15+
a BranchFS copy-on-write layer inside the container. Committed changes
16+
are written back to the host directory.
17+
18+
Options:
19+
-w PATH Host directory to use as workspace (required)
20+
-h Show this help
21+
22+
Environment:
23+
BRANCHING_IMAGE Docker image name (default: branching)
24+
25+
Examples:
26+
branching-docker -w ./myproject run -- make test
27+
branching-docker -w /home/user/repo speculate -c "./fix_a.sh" -c "./fix_b.sh"
28+
branching-docker -w . best-of-n -n 5 -- ./solve.py
29+
branching-docker -w . reflexion --retries 3 -- ./fix.sh
30+
EOF
31+
exit "${1:-0}"
32+
}
33+
34+
# Parse -w (workspace) before forwarding the rest to branching
35+
while [ $# -gt 0 ]; do
36+
case "$1" in
37+
-w)
38+
[ $# -ge 2 ] || { echo "error: -w requires a path" >&2; exit 1; }
39+
WORKSPACE="$2"
40+
shift 2
41+
;;
42+
-h|--help)
43+
usage 0
44+
;;
45+
*)
46+
break
47+
;;
48+
esac
49+
done
50+
51+
if [ -z "$WORKSPACE" ]; then
52+
echo "error: -w <workspace> is required" >&2
53+
usage 1
54+
fi
55+
56+
WORKSPACE="$(cd "$WORKSPACE" && pwd)"
57+
58+
exec docker run --rm \
59+
--device /dev/fuse --cap-add SYS_ADMIN \
60+
-v "$WORKSPACE":/workspace \
61+
"$IMAGE" "$@"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
set -e
3+
4+
# Mount branchfs over /workspace (host bind-mount is the backing store)
5+
branchfs /workspace
6+
7+
exec branching -w /workspace "$@"

0 commit comments

Comments
 (0)