diff --git a/README.md b/README.md index 675fd20..d56c223 100644 --- a/README.md +++ b/README.md @@ -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 │ └─────────────────────────────────────────────────────────┘ ``` @@ -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) + │ │ └───────────┘ ``` @@ -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/`) -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 @@ -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) @@ -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)** diff --git a/src/executor.ts b/src/executor.ts index a836c46..5f12737 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -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 { return new Promise((resolve, reject) => { @@ -77,6 +78,14 @@ function invokeCodex(message: string, cwd: string): Promise { }); } +function invokeOpenHands(message: string, cwd: string): Promise { + 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 @@ -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; + 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; diff --git a/src/server.ts b/src/server.ts index 69e131e..7073778 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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; } diff --git a/src/types.ts b/src/types.ts index a082265..94a3c62 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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" diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 5610d3e..7942baf 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -31,7 +31,7 @@ export default function Home() { const [maxDepth, setMaxDepth] = useState(3); const [tree, setTree] = useState(null); const [workspace, setWorkspace] = useState(""); - const [executor, setExecutor] = useState<"claude" | "codex">("claude"); + const [executor, setExecutor] = useState<"claude" | "codex" | "openhands">("claude"); const [batches, setBatches] = useState([]); const pollingRef = useRef | null>(null); @@ -183,6 +183,13 @@ export default function Home() { > Codex +