From cd3d23687996e99457def5b3228c4f3227239845 Mon Sep 17 00:00:00 2001 From: QMTCHL Date: Thu, 22 Jan 2026 23:12:21 -0500 Subject: [PATCH] feat(config): make preflight timeout configurable Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/commands/create-prd.tsx | 21 ++++++++++++++------- src/commands/doctor.ts | 3 ++- src/commands/run.tsx | 3 ++- src/config/index.test.ts | 3 +++ src/config/index.ts | 3 +++ src/config/schema.test.ts | 8 ++++++++ src/config/schema.ts | 1 + src/config/types.ts | 3 +++ src/setup/types.ts | 3 +++ src/setup/wizard.ts | 11 ++++++++++- 10 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/commands/create-prd.tsx b/src/commands/create-prd.tsx index c5423378..b7960336 100644 --- a/src/commands/create-prd.tsx +++ b/src/commands/create-prd.tsx @@ -11,7 +11,7 @@ import { constants } from 'node:fs'; import { join, resolve } from 'node:path'; import { PrdChatApp } from '../tui/components/PrdChatApp.js'; import type { PrdCreationResult } from '../tui/components/PrdChatApp.js'; -import { loadStoredConfig, requireSetup } from '../config/index.js'; +import { loadStoredConfig, requireSetup, type StoredConfig } from '../config/index.js'; import { getAgentRegistry } from '../plugins/agents/registry.js'; import { registerBuiltinAgents } from '../plugins/agents/builtin/index.js'; import type { AgentPlugin, AgentPluginConfig } from '../plugins/agents/types.js'; @@ -78,10 +78,14 @@ export function parseCreatePrdArgs(args: string[]): CreatePrdArgs { process.exit(0); } } - return result; } +interface LoadedAgent { + agent: AgentPlugin; + storedConfig: StoredConfig; +} + /** * Print help for the create-prd command. */ @@ -225,7 +229,7 @@ async function loadPrdSkillSource( /** * Get the configured agent plugin. */ -async function getAgent(agentName?: string): Promise { +async function getAgent(agentName?: string): Promise { try { const cwd = process.cwd(); const storedConfig = await loadStoredConfig(cwd); @@ -260,7 +264,7 @@ async function getAgent(agentName?: string): Promise { } } - return agent; + return { agent, storedConfig }; } catch (error) { console.error('Failed to load agent:', error instanceof Error ? error.message : error); return null; @@ -273,8 +277,8 @@ async function getAgent(agentName?: string): Promise { */ async function runChatMode(parsedArgs: CreatePrdArgs): Promise { // Get agent - const agent = await getAgent(parsedArgs.agent); - if (!agent) { + const loadedAgent = await getAgent(parsedArgs.agent); + if (!loadedAgent) { console.error(''); console.error('Chat mode requires an AI agent. Options:'); console.error(' 1. Run "ralph-tui setup" to configure an agent'); @@ -282,6 +286,8 @@ async function runChatMode(parsedArgs: CreatePrdArgs): Promise { const agentRegistry = getAgentRegistry(); const agentInstance = await agentRegistry.getInstance(config.agent); - const preflightResult = await agentInstance.preflight({ timeout: 30000 }); + const preflightTimeoutMs = storedConfig.preflightTimeoutMs ?? 30000; + const preflightResult = await agentInstance.preflight({ timeout: preflightTimeoutMs }); if (preflightResult.success) { console.log('✓ Agent is ready'); diff --git a/src/config/index.test.ts b/src/config/index.test.ts index 90287ab5..91f7777b 100644 --- a/src/config/index.test.ts +++ b/src/config/index.test.ts @@ -89,6 +89,7 @@ describe('loadStoredConfig', () => { maxIterations: 15, agent: 'claude', iterationDelay: 1000, + preflightTimeoutMs: 30000, }); // Write project config @@ -97,6 +98,7 @@ describe('loadStoredConfig', () => { await writeTomlConfig(join(projectConfigDir, 'config.toml'), { maxIterations: 30, // Override tracker: 'json', // New field + preflightTimeoutMs: 180000, }); const config = await loadStoredConfig(tempDir, globalConfigPath); @@ -104,6 +106,7 @@ describe('loadStoredConfig', () => { expect(config.agent).toBe('claude'); // From global expect(config.tracker).toBe('json'); // From project expect(config.iterationDelay).toBe(1000); // From global + expect(config.preflightTimeoutMs).toBe(180000); // Project override }); test('handles empty config files', async () => { diff --git a/src/config/index.ts b/src/config/index.ts index 64b6deeb..00b66d12 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -147,6 +147,9 @@ function mergeConfigs(global: StoredConfig, project: StoredConfig): StoredConfig if (project.defaultTracker !== undefined) merged.defaultTracker = project.defaultTracker; if (project.maxIterations !== undefined) merged.maxIterations = project.maxIterations; if (project.iterationDelay !== undefined) merged.iterationDelay = project.iterationDelay; + if (project.preflightTimeoutMs !== undefined) { + merged.preflightTimeoutMs = project.preflightTimeoutMs; + } if (project.outputDir !== undefined) merged.outputDir = project.outputDir; if (project.agent !== undefined) merged.agent = project.agent; if (project.agentCommand !== undefined) merged.agentCommand = project.agentCommand; diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 461a3d9d..439228b0 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -285,6 +285,7 @@ describe('StoredConfigSchema', () => { defaultTracker: 'beads-bv', maxIterations: 20, iterationDelay: 2000, + preflightTimeoutMs: 180000, outputDir: './output', autoCommit: true, agents: [ @@ -323,6 +324,13 @@ describe('StoredConfigSchema', () => { expect(() => StoredConfigSchema.parse({ iterationDelay: 300001 })).toThrow(); }); + test('validates preflightTimeoutMs as non-negative integer', () => { + expect(() => StoredConfigSchema.parse({ preflightTimeoutMs: -1 })).toThrow(); + expect(() => StoredConfigSchema.parse({ preflightTimeoutMs: 1.5 })).toThrow(); + expect(StoredConfigSchema.parse({ preflightTimeoutMs: 0 }).preflightTimeoutMs).toBe(0); + expect(StoredConfigSchema.parse({ preflightTimeoutMs: 30000 }).preflightTimeoutMs).toBe(30000); + }); + test('rejects unknown fields (strict mode)', () => { expect(() => StoredConfigSchema.parse({ unknownField: 'value' })).toThrow(); }); diff --git a/src/config/schema.ts b/src/config/schema.ts index 5f2cbf66..b9f8a41d 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -113,6 +113,7 @@ export const StoredConfigSchema = z // Core settings maxIterations: z.number().int().min(0).max(1000).optional(), iterationDelay: z.number().int().min(0).max(300000).optional(), + preflightTimeoutMs: z.number().int().min(0).optional(), outputDir: z.string().optional(), autoCommit: z.boolean().optional(), diff --git a/src/config/types.ts b/src/config/types.ts index 90ae03e6..457ed648 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -160,6 +160,9 @@ export interface StoredConfig { /** Default iteration delay in milliseconds */ iterationDelay?: number; + /** Default preflight timeout in milliseconds (0 = no timeout) */ + preflightTimeoutMs?: number; + /** Configured agent plugins */ agents?: AgentPluginConfig[]; diff --git a/src/setup/types.ts b/src/setup/types.ts index f4e643a8..db736d8c 100644 --- a/src/setup/types.ts +++ b/src/setup/types.ts @@ -50,6 +50,9 @@ export interface SetupAnswers { /** Maximum iterations per run (0 = unlimited) */ maxIterations: number; + /** Preflight timeout in milliseconds (0 = no timeout) */ + preflightTimeoutMs: number; + /** Whether to auto-commit on task completion */ autoCommit: boolean; } diff --git a/src/setup/wizard.ts b/src/setup/wizard.ts index 7ae265e4..e6efabb6 100644 --- a/src/setup/wizard.ts +++ b/src/setup/wizard.ts @@ -182,6 +182,7 @@ async function saveConfig( agent: answers.agent, agentOptions: answers.agentOptions, maxIterations: answers.maxIterations, + preflightTimeoutMs: answers.preflightTimeoutMs, autoCommit: answers.autoCommit, }; @@ -329,6 +330,13 @@ export async function runSetupWizard( } ); + const preflightTimeoutMs = await promptNumber('Agent preflight timeout (ms)?', { + default: selectedAgent === 'droid' ? 120000 : 30000, + min: 0, + max: 3600000, + help: 'Timeout for agent verification checks (setup/doctor/--verify). 0 = no timeout.', + }); + const autoCommit = await promptBoolean( 'Auto-commit on task completion?', { @@ -406,6 +414,7 @@ export async function runSetupWizard( agent: selectedAgent, agentOptions, maxIterations, + preflightTimeoutMs, autoCommit, }; @@ -431,7 +440,7 @@ export async function runSetupWizard( }); // Run preflight check - const preflightResult = await agentInstance.preflight({ timeout: 30000 }); + const preflightResult = await agentInstance.preflight({ timeout: preflightTimeoutMs }); if (preflightResult.success) { printSuccess(`✓ Agent is configured correctly and responding`);