Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ LLM │ │Orchestr- │ │ Executor │ │
│ │classify │──>│ ator │ │ Claude / Codex CLI │ │
│ │decompose│ │ plan() │ │ git worktrees │ │
│ │classify │──>│ ator │ │ Claude / Codex / │ │
│ │decompose│ │ plan() │ │ OpenHands CLI │ │
│ └─────────┘ └──────────┘ └──────────────────────┘ │
│ │
│ OpenAI (gpt-5.2) Claude / Codex CLI (spawn)
│ OpenAI (gpt-5.2) Claude / Codex / OpenHands CLI
└─────────────────────────────────────────────────────────┘
```

Expand All @@ -57,9 +57,9 @@ User enters task User confirms plan
└──composite──> decompose(task) batch leaf tasks
│ │
[children] v
│ claude --dangerously-skip-permissions
plan(child) <────┐ -p "task + lineage context"
│ │ (per worktree)
│ claude / codex / openhands CLI
plan(child) <────┐ (per worktree)
│ │
└───────────┘
```

Expand All @@ -69,7 +69,7 @@ User enters task User confirms plan
2. **Decompose** -- server recursively breaks it into a tree
3. **Review** -- inspect the full plan tree before committing
4. **Workspace** -- provide a directory path (git-initialized automatically, defaults to `~/fractals/<task-slug>`)
5. **Execute** -- leaf tasks run via Claude CLI in batches, status updates poll in real-time
5. **Execute** -- leaf tasks run via Claude CLI, Codex CLI, or OpenHands CLI in batches, status updates poll in real-time

## Batch Strategies

Expand All @@ -86,10 +86,10 @@ Due to rate limits, leaf tasks execute in batches rather than all at once.
```
src/
server.ts Hono API server (:1618)
types.ts Shared types (Task, Session, BatchStrategy)
types.ts Shared types (Task, Session, BatchStrategy, ExecutorProvider)
llm.ts OpenAI calls: classify + decompose (structured output)
orchestrator.ts Recursive plan() -- builds the tree, no execution
executor.ts Claude CLI invocation per task in git worktree
executor.ts Claude / Codex / OpenHands CLI invocation per task in git worktree
workspace.ts git init + worktree management
batch.ts Batch execution strategies
index.ts CLI entry point (standalone, no server)
Expand Down Expand Up @@ -145,8 +145,8 @@ Port `1618` — the golden ratio, the constant behind fractal geometry.
## Roadmap

**Executor**
- [ ] OpenCode CLI as a third executor option
- [ ] Per-task executor override (mix Claude and Codex in one plan)
- [x] OpenHands CLI as a third executor option
- [ ] Per-task executor override (mix Claude, Codex and OpenHands in one plan)
- [ ] Merge worktree branches back to main after completion

**Backpropagation (merge agent)**
Expand Down
18 changes: 17 additions & 1 deletion src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function resolveBin(name: string): string {

const CLAUDE_BIN = resolveBin("claude");
const CODEX_BIN = resolveBin("codex");
const OPENHANDS_BIN = resolveBin("openhands");

function runCommand(command: string, args: string[], cwd: string): Promise<string> {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -77,6 +78,14 @@ function invokeCodex(message: string, cwd: string): Promise<string> {
});
}

function invokeOpenHands(message: string, cwd: string): Promise<string> {
return runCommand(
OPENHANDS_BIN,
["--headless", "--always-approve", "-t", message],
cwd
);
}

function buildPrompt(task: Task): string {
const hierarchy = formatLineage(task.lineage, task.description);
const siblingContext = task.lineage.length > 0
Expand Down Expand Up @@ -112,7 +121,14 @@ export async function executeTask(

const prompt = buildPrompt(task);

const invoke = provider === "codex" ? invokeCodex : invokeClaude;
let invoke: (message: string, cwd: string) => Promise<string>;
if (provider === "codex") {
invoke = invokeCodex;
} else if (provider === "openhands") {
invoke = invokeOpenHands;
} else {
invoke = invokeClaude;
}
const result = await invoke(prompt, worktreePath);
console.log(`[execute] [${task.id}] done`);
return result;
Expand Down
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ app.post("/api/execute", async (c) => {
if (strategy === "depth-first" || strategy === "breadth-first" || strategy === "layer-sequential") {
session.batchStrategy = strategy;
}
if (executor === "claude" || executor === "codex") {
if (executor === "claude" || executor === "codex" || executor === "openhands") {
session.executor = executor;
}

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type TaskKind = "atomic" | "composite";
export type TaskStatus = "pending" | "decomposing" | "ready" | "running" | "done" | "failed";
export type ExecutorProvider = "claude" | "codex";
export type ExecutorProvider = "claude" | "codex" | "openhands";

export interface Task {
id: string; // hierarchical: "1", "1.2", "1.2.3"
Expand Down
9 changes: 8 additions & 1 deletion web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Home() {
const [maxDepth, setMaxDepth] = useState(3);
const [tree, setTree] = useState<Task | null>(null);
const [workspace, setWorkspace] = useState("");
const [executor, setExecutor] = useState<"claude" | "codex">("claude");
const [executor, setExecutor] = useState<"claude" | "codex" | "openhands">("claude");
const [batches, setBatches] = useState<string[][]>([]);
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);

Expand Down Expand Up @@ -183,6 +183,13 @@ export default function Home() {
>
Codex
</Button>
<Button
size="sm"
variant={executor === "openhands" ? "default" : "outline"}
onClick={() => setExecutor("openhands")}
>
OpenHands
</Button>
</div>
<div className="flex-1" />
<Button onClick={handleSetupWorkspace} disabled={!workspace.trim()}>
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface Session {
tree: Task | null;
workspace: string | null;
batchStrategy: string;
executor: "claude" | "codex";
executor: "claude" | "codex" | "openhands";
phase: "idle" | "decomposing" | "planning" | "executing" | "done";
}

Expand Down