diff --git a/scripts/agent/run-selected-prompt.mjs b/scripts/agent/run-selected-prompt.mjs new file mode 100644 index 0000000..4b94263 --- /dev/null +++ b/scripts/agent/run-selected-prompt.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const REQUIRED_ENV = ['SCHEDULER_PROMPT_PATH', 'SCHEDULER_AGENT', 'SCHEDULER_CADENCE']; + +function fail(message, code = 1) { + console.error(`[scheduler] ${message}`); + process.exit(code); +} + +function resolveRunner(promptMarkdown) { + const customRunner = process.env.SCHEDULER_AGENT_RUNNER_COMMAND?.trim(); + if (customRunner) { + return { command: 'bash', args: ['-lc', customRunner] }; + } + + const platform = (process.env.AGENT_PLATFORM || 'codex').trim().toLowerCase(); + if (platform === 'codex') { + return { command: 'codex', args: ['exec', promptMarkdown] }; + } + + if (platform === 'claude') { + return { command: 'claude', args: ['-p', promptMarkdown] }; + } + + fail(`Unsupported AGENT_PLATFORM="${platform}". Set SCHEDULER_AGENT_RUNNER_COMMAND to override.`); +} + +async function main() { + for (const key of REQUIRED_ENV) { + if (!process.env[key] || !String(process.env[key]).trim()) { + fail(`Missing required environment variable ${key}.`); + } + } + + const promptPath = path.resolve(process.cwd(), process.env.SCHEDULER_PROMPT_PATH); + let promptMarkdown; + + try { + promptMarkdown = await fs.readFile(promptPath, 'utf8'); + } catch { + fail(`Unable to read scheduler prompt at ${promptPath}.`); + } + + if (!promptMarkdown.trim()) { + fail(`Scheduler prompt at ${promptPath} is empty.`); + } + + const runner = resolveRunner(promptMarkdown); + const child = spawn(runner.command, runner.args, { + cwd: process.cwd(), + env: { + ...process.env, + SCHEDULER_PROMPT_PATH: promptPath, + SCHEDULER_PROMPT_MARKDOWN: promptMarkdown, + }, + stdio: 'inherit', + }); + + child.on('error', (error) => { + if (error.code === 'ENOENT') { + fail(`Runner command not found: ${runner.command}`, 127); + } + fail(`Runner execution failed: ${error.message}`); + }); + + child.on('close', (code) => { + process.exit(code ?? 1); + }); +} + +main(); diff --git a/test/run-selected-prompt.test.mjs b/test/run-selected-prompt.test.mjs new file mode 100644 index 0000000..c772916 --- /dev/null +++ b/test/run-selected-prompt.test.mjs @@ -0,0 +1,71 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const SCRIPT_PATH = path.resolve('scripts/agent/run-selected-prompt.mjs'); + +function runScript({ cwd, env }) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [SCRIPT_PATH], { + cwd, + env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('close', (code) => { + resolve({ code: code ?? 1, stdout, stderr }); + }); + }); +} + +test('runs selected scheduler prompt through configured runner command', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'run-selected-prompt-')); + const promptPath = path.join(root, 'prompt.md'); + await fs.writeFile(promptPath, '# hello from prompt\n', 'utf8'); + + const result = await runScript({ + cwd: root, + env: { + ...process.env, + SCHEDULER_PROMPT_PATH: promptPath, + SCHEDULER_AGENT: 'agent-a', + SCHEDULER_CADENCE: 'daily', + SCHEDULER_AGENT_RUNNER_COMMAND: 'node -e "if(!process.env.SCHEDULER_PROMPT_MARKDOWN.includes(\'hello from prompt\')) process.exit(6)"', + }, + }); + + assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`); +}); + +test('propagates non-zero runner exit code', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'run-selected-prompt-fail-')); + const promptPath = path.join(root, 'prompt.md'); + await fs.writeFile(promptPath, '# will fail\n', 'utf8'); + + const result = await runScript({ + cwd: root, + env: { + ...process.env, + SCHEDULER_PROMPT_PATH: promptPath, + SCHEDULER_AGENT: 'agent-b', + SCHEDULER_CADENCE: 'weekly', + SCHEDULER_AGENT_RUNNER_COMMAND: 'node -e "process.exit(7)"', + }, + }); + + assert.equal(result.code, 7, `stdout: ${result.stdout}\nstderr: ${result.stderr}`); +}); diff --git a/torch-config.json b/torch-config.json index 9827335..c491b72 100644 --- a/torch-config.json +++ b/torch-config.json @@ -19,8 +19,8 @@ "weekly": "bug-reproducer-agent" }, "handoffCommandByCadence": { - "daily": "test -n \"$SCHEDULER_AGENT\" && test -n \"$SCHEDULER_CADENCE\" && test -n \"$SCHEDULER_PROMPT_PATH\" && test -f \"$SCHEDULER_PROMPT_PATH\" && echo \"HANDOFF_OK cadence=$SCHEDULER_CADENCE agent=$SCHEDULER_AGENT prompt=$SCHEDULER_PROMPT_PATH\"", - "weekly": "test -n \"$SCHEDULER_AGENT\" && test -n \"$SCHEDULER_CADENCE\" && test -n \"$SCHEDULER_PROMPT_PATH\" && test -f \"$SCHEDULER_PROMPT_PATH\" && echo \"HANDOFF_OK cadence=$SCHEDULER_CADENCE agent=$SCHEDULER_AGENT prompt=$SCHEDULER_PROMPT_PATH\"" + "daily": "node scripts/agent/run-selected-prompt.mjs", + "weekly": "node scripts/agent/run-selected-prompt.mjs" }, "memoryPolicyByCadence": { "daily": {