diff --git a/index.ts b/index.ts index 3fef764..56b0a02 100644 --- a/index.ts +++ b/index.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { createRunArtifacts } from "./src/artifacts.ts"; import { monitorRun } from "./src/monitor.ts"; import { listAvailableModelReferences } from "./src/models.ts"; -import { createTmuxSingleRunner } from "./src/runner.ts"; +import { createTmuxSingleRunner } from "./src/tmux-runner.ts"; import { buildCurrentWindowArgs, buildKillPaneArgs, diff --git a/src/runner.ts b/src/runner.ts index 2c384a8..f33741f 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,44 +1,9 @@ -export function createTmuxSingleRunner(deps: { - assertInsideTmux(): void; - getCurrentWindowId: () => Promise; - createArtifacts: (cwd: string, meta: Record) => Promise; - buildWrapperCommand: (metaPath: string) => string; - createPane: (input: { windowId: string; cwd: string; command: string }) => Promise; - monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise; - killPane: (paneId: string) => Promise; -}) { - return async function runSingleTask(input: { - cwd: string; - meta: Record; - onEvent?: (event: any) => void; - }) { - deps.assertInsideTmux(); +import type { SubagentRunResult } from "./schema.ts"; - const artifacts = await deps.createArtifacts(input.cwd, input.meta); - const windowId = await deps.getCurrentWindowId(); - const command = deps.buildWrapperCommand(artifacts.metaPath); - const paneId = await deps.createPane({ windowId, cwd: input.cwd, command }); - - try { - const result = await deps.monitorRun({ - eventsPath: artifacts.eventsPath, - resultPath: artifacts.resultPath, - onEvent: input.onEvent, - }); - - return { - ...result, - runId: result.runId ?? artifacts.runId, - paneId, - windowId, - sessionPath: result.sessionPath ?? artifacts.sessionPath, - stdoutPath: result.stdoutPath ?? artifacts.stdoutPath, - stderrPath: result.stderrPath ?? artifacts.stderrPath, - resultPath: artifacts.resultPath, - eventsPath: artifacts.eventsPath, - }; - } finally { - await deps.killPane(paneId); - } - }; +export interface RunSingleTaskInput { + cwd: string; + meta: Record; + onEvent?: (event: any) => void; } + +export type RunSingleTask = (input: RunSingleTaskInput) => Promise; diff --git a/src/runner.test.ts b/src/tmux-runner.test.ts similarity index 50% rename from src/runner.test.ts rename to src/tmux-runner.test.ts index 2b92351..f090922 100644 --- a/src/runner.test.ts +++ b/src/tmux-runner.test.ts @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { createTmuxSingleRunner } from "./runner.ts"; +import { createTmuxSingleRunner } from "./tmux-runner.ts"; test("createTmuxSingleRunner always kills the pane after monitor completion", async () => { const killed: string[] = []; @@ -31,3 +31,30 @@ test("createTmuxSingleRunner always kills the pane after monitor completion", as assert.equal(result.finalText, "done"); assert.deepEqual(killed, ["%9"]); }); + +test("createTmuxSingleRunner surfaces explicit tmux precondition errors", async () => { + const runSingleTask = createTmuxSingleRunner({ + assertInsideTmux() { + throw new Error("tmux-backed subagents require pi to be running inside tmux."); + }, + getCurrentWindowId: async () => "@1", + createArtifacts: async () => ({ + metaPath: "/tmp/meta.json", + runId: "run-1", + eventsPath: "/tmp/events.jsonl", + resultPath: "/tmp/result.json", + sessionPath: "/tmp/child-session.jsonl", + stdoutPath: "/tmp/stdout.log", + stderrPath: "/tmp/stderr.log", + }), + buildWrapperCommand: () => "'node' '/wrapper.mjs' '/tmp/meta.json'", + createPane: async () => "%9", + monitorRun: async () => ({ finalText: "done", exitCode: 0 }), + killPane: async () => {}, + }); + + await assert.rejects( + () => runSingleTask({ cwd: "/repo", meta: { task: "inspect auth" } as any }), + /tmux-backed subagents require pi to be running inside tmux/, + ); +}); diff --git a/src/tmux-runner.ts b/src/tmux-runner.ts new file mode 100644 index 0000000..2c384a8 --- /dev/null +++ b/src/tmux-runner.ts @@ -0,0 +1,44 @@ +export function createTmuxSingleRunner(deps: { + assertInsideTmux(): void; + getCurrentWindowId: () => Promise; + createArtifacts: (cwd: string, meta: Record) => Promise; + buildWrapperCommand: (metaPath: string) => string; + createPane: (input: { windowId: string; cwd: string; command: string }) => Promise; + monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise; + killPane: (paneId: string) => Promise; +}) { + return async function runSingleTask(input: { + cwd: string; + meta: Record; + onEvent?: (event: any) => void; + }) { + deps.assertInsideTmux(); + + const artifacts = await deps.createArtifacts(input.cwd, input.meta); + const windowId = await deps.getCurrentWindowId(); + const command = deps.buildWrapperCommand(artifacts.metaPath); + const paneId = await deps.createPane({ windowId, cwd: input.cwd, command }); + + try { + const result = await deps.monitorRun({ + eventsPath: artifacts.eventsPath, + resultPath: artifacts.resultPath, + onEvent: input.onEvent, + }); + + return { + ...result, + runId: result.runId ?? artifacts.runId, + paneId, + windowId, + sessionPath: result.sessionPath ?? artifacts.sessionPath, + stdoutPath: result.stdoutPath ?? artifacts.stdoutPath, + stderrPath: result.stderrPath ?? artifacts.stderrPath, + resultPath: artifacts.resultPath, + eventsPath: artifacts.eventsPath, + }; + } finally { + await deps.killPane(paneId); + } + }; +}