From 9f5aff99b68b0dbd9c23f4ac6907f29d2a7036df Mon Sep 17 00:00:00 2001 From: Gabi Simons Date: Wed, 18 Mar 2026 13:43:56 +0200 Subject: [PATCH] feat: add script execution phase to agent-runner Co-Authored-By: Claude Opus 4.6 (1M context) --- container/agent-runner/src/index.ts | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 2cd34c9..382439f 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -16,6 +16,7 @@ import fs from 'fs'; import path from 'path'; +import { execFile } from 'child_process'; import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk'; import { fileURLToPath } from 'url'; @@ -465,6 +466,55 @@ async function runQuery( return { newSessionId, lastAssistantUuid, closedDuringQuery }; } +interface ScriptResult { + wakeAgent: boolean; + data?: unknown; +} + +const SCRIPT_TIMEOUT_MS = 30_000; + +async function runScript(script: string): Promise { + const scriptPath = '/tmp/task-script.sh'; + fs.writeFileSync(scriptPath, script, { mode: 0o755 }); + + return new Promise((resolve) => { + execFile('bash', [scriptPath], { + timeout: SCRIPT_TIMEOUT_MS, + maxBuffer: 1024 * 1024, + env: process.env, + }, (error, stdout, stderr) => { + if (stderr) { + log(`Script stderr: ${stderr.slice(0, 500)}`); + } + + if (error) { + log(`Script error: ${error.message}`); + return resolve(null); + } + + // Parse last non-empty line of stdout as JSON + const lines = stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + if (!lastLine) { + log('Script produced no output'); + return resolve(null); + } + + try { + const result = JSON.parse(lastLine); + if (typeof result.wakeAgent !== 'boolean') { + log(`Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`); + return resolve(null); + } + resolve(result as ScriptResult); + } catch { + log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`); + resolve(null); + } + }); + }); +} + async function main(): Promise { let containerInput: ContainerInput; @@ -506,6 +556,26 @@ async function main(): Promise { prompt += '\n' + pending.join('\n'); } + // Script phase: run script before waking agent + if (containerInput.script && containerInput.isScheduledTask) { + log('Running task script...'); + const scriptResult = await runScript(containerInput.script); + + if (!scriptResult || !scriptResult.wakeAgent) { + const reason = scriptResult ? 'wakeAgent=false' : 'script error/no output'; + log(`Script decided not to wake agent: ${reason}`); + writeOutput({ + status: 'success', + result: `Script: ${reason}`, + }); + return; + } + + // Script says wake agent — enrich prompt with script data + log(`Script wakeAgent=true, enriching prompt with data`); + prompt = `[SCHEDULED TASK]\n\nScript output:\n${JSON.stringify(scriptResult.data, null, 2)}\n\nInstructions:\n${containerInput.prompt}`; + } + // Query loop: run query → wait for IPC message → run new query → repeat let resumeAt: string | undefined; try {