Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/commands/create-prd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,10 +82,14 @@ export function parseCreatePrdArgs(args: string[]): CreatePrdArgs {
process.exit(0);
}
}

return result;
}

interface LoadedAgent {
agent: AgentPlugin;
storedConfig: StoredConfig;
}

/**
* Parse tracker labels from config trackerOptions.
* Handles both string (comma-separated) and array formats.
Expand Down Expand Up @@ -252,7 +256,7 @@ async function loadPrdSkillSource(
/**
* Get the configured agent plugin.
*/
async function getAgent(agentName?: string): Promise<AgentPlugin | null> {
async function getAgent(agentName?: string): Promise<LoadedAgent | null> {
try {
const cwd = process.cwd();
const storedConfig = await loadStoredConfig(cwd);
Expand Down Expand Up @@ -288,7 +292,7 @@ async function getAgent(agentName?: string): Promise<AgentPlugin | null> {
}
}

return agent;
return { agent, storedConfig };
} catch (error) {
console.error('Failed to load agent:', error instanceof Error ? error.message : error);
return null;
Expand All @@ -301,15 +305,17 @@ async function getAgent(agentName?: string): Promise<AgentPlugin | null> {
*/
async function runChatMode(parsedArgs: CreatePrdArgs): Promise<PrdCreationResult | null> {
// 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');
console.error(' 2. Use "--agent claude" or "--agent opencode" to specify one');
process.exit(1);
}

const { agent, storedConfig } = loadedAgent;

const cwd = parsedArgs.cwd || process.cwd();
const outputDir = parsedArgs.output || 'tasks';
const timeout = parsedArgs.timeout ?? 0;
Expand Down Expand Up @@ -343,7 +349,8 @@ async function runChatMode(parsedArgs: CreatePrdArgs): Promise<PrdCreationResult

// Run preflight check to verify agent can respond before starting conversation
console.log('Verifying agent configuration...');
const preflightResult = await agent.preflight({ timeout: 30000 });
const preflightTimeoutMs = storedConfig.preflightTimeoutMs ?? 30000;
const preflightResult = await agent.preflight({ timeout: preflightTimeoutMs });

if (!preflightResult.success) {
console.error('');
Expand Down
3 changes: 2 additions & 1 deletion src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ async function runDiagnostics(
log('\n Step 2: Preflight (testing if agent can respond)...');
log(' Running test prompt...');

const preflight = await agent.preflight({ timeout: 30000 });
const preflightTimeoutMs = storedConfig.preflightTimeoutMs ?? 30000;
const preflight = await agent.preflight({ timeout: preflightTimeoutMs });

if (!preflight.success) {
return {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,8 @@ export async function executeRunCommand(args: string[]): Promise<void> {
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');
Expand Down
3 changes: 3 additions & 0 deletions src/config/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ describe('loadStoredConfig', () => {
maxIterations: 15,
agent: 'claude',
iterationDelay: 1000,
preflightTimeoutMs: 30000,
});

// Write project config
Expand All @@ -97,13 +98,15 @@ describe('loadStoredConfig', () => {
await writeTomlConfig(join(projectConfigDir, 'config.toml'), {
maxIterations: 30, // Override
tracker: 'json', // New field
preflightTimeoutMs: 180000,
});

const config = await loadStoredConfig(tempDir, globalConfigPath);
expect(config.maxIterations).toBe(30); // Project override
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 () => {
Expand Down
3 changes: 3 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/config/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ describe('StoredConfigSchema', () => {
defaultTracker: 'beads-bv',
maxIterations: 20,
iterationDelay: 2000,
preflightTimeoutMs: 180000,
outputDir: './output',
autoCommit: true,
agents: [
Expand Down Expand Up @@ -384,6 +385,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();
});
Expand Down
1 change: 1 addition & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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(),

Expand Down
3 changes: 3 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];

Expand Down
3 changes: 3 additions & 0 deletions src/setup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
11 changes: 10 additions & 1 deletion src/setup/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ async function saveConfig(
agent: answers.agent,
agentOptions: answers.agentOptions,
maxIterations: answers.maxIterations,
preflightTimeoutMs: answers.preflightTimeoutMs,
autoCommit: answers.autoCommit,
};

Expand Down Expand Up @@ -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?',
{
Expand Down Expand Up @@ -398,6 +406,7 @@ export async function runSetupWizard(
agent: selectedAgent,
agentOptions,
maxIterations,
preflightTimeoutMs,
autoCommit,
};

Expand All @@ -423,7 +432,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`);
Expand Down