/** * KHAELOR * File: src/cli/subtasks.ts * Description: Subtask wiring — SubtaskManager factory whose child runner assembles a full headless engine per worktree (v2 design §6). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import type { ResolvedConfig } from "../config/index.js"; import type { DurableEventInput } from "../session/index.js"; import { SubtaskManager } from "../tasks/index.js"; import type { ExecFn } from "../tasks/index.js"; import type { Workspace } from "../workspace/index.js"; import { runHeadlessTurn } from "./headless.js"; import type { FileLogger } from "./logger.js"; const CHILD_PROMPT_SUFFIX = "\n\nYou are running as an isolated KHAELOR subtask inside a dedicated git worktree. " + "Work only inside this worktree. Do not push, merge, or touch branches — the orchestrator " + "supervises the merge. Design first, verify before finishing."; /** Adapt Workspace.exec to the tasks module's minimal exec seam. */ export function workspaceExec(workspace: Workspace): ExecFn { return async (cmd, cwd) => { const result = await workspace.exec({ cmd, timeoutMs: 120_000, ...(cwd !== undefined ? { cwd } : {}) }); return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr }; }; } export interface CreateSubtaskManagerOptions { config: ResolvedConfig; workspace: Workspace; projectRoot: string; logger: FileLogger; publish: (event: DurableEventInput) => void; /** Orchestrator session id — recorded as the child's meta.parent. */ parentSessionId: string; } /** * Build the SubtaskManager with a child runner that assembles a complete * headless engine over the worktree: own JSONL (meta.parent → orchestrator), * non-interactive permissions (capability attenuation: asks resolve deny), * phase gates in the configured mode, native verify active (v2 §6). */ export function createSubtaskManager(options: CreateSubtaskManagerOptions): SubtaskManager { return new SubtaskManager({ publish: options.publish, exec: workspaceExec(options.workspace), projectRoot: options.projectRoot, runChild: async (args) => { const result = await runHeadlessTurn({ config: options.config, cwd: args.worktreePath, prompt: args.description + CHILD_PROMPT_SUFFIX, logger: options.logger, meta: { parent: options.parentSessionId }, }); return { status: result.outcome === "done" ? "done" : result.outcome === "interrupted" ? "interrupted" : "failed", verifyOk: result.verifyOk, detail: result.outcome === "done" ? result.finalText.slice(0, 2000) : result.detail || result.finalText.slice(0, 2000), }; }, }); }