Skip to content
Closed
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
25 changes: 24 additions & 1 deletion apps/server/src/lib/sdk-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,20 @@ export const TOOL_PRESETS = {
'WebFetch',
'TodoWrite',
] as const,

/** Full access plus AskUserQuestion for interactive planning mode */
fullAccessWithClarification: [
'Read',
'Write',
'Edit',
'Glob',
'Grep',
'Bash',
'WebSearch',
'WebFetch',
'TodoWrite',
'AskUserQuestion',
] as const,
} as const;

/**
Expand Down Expand Up @@ -351,6 +365,9 @@ export interface CreateSdkOptionsConfig {

/** Extended thinking level for Claude models */
thinkingLevel?: ThinkingLevel;

/** Enable AskUserQuestion tool for interactive planning mode */
enableClarificationQuestions?: boolean;
}

// Re-export MCP types from @automaker/types for convenience
Expand Down Expand Up @@ -508,6 +525,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Extended turns for thorough feature implementation
* - Uses default model (can be overridden)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
* - When enableClarificationQuestions is true, includes AskUserQuestion tool for interactive planning
*/
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Validate working directory before creating options
Expand All @@ -522,12 +540,17 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);

// Use full access with clarification tools when enabled, otherwise standard full access
const allowedTools = config.enableClarificationQuestions
? [...TOOL_PRESETS.fullAccessWithClarification]
: [...TOOL_PRESETS.fullAccess];

return {
...getBaseOptions(),
model: getModelForUseCase('auto', config.model),
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.fullAccess],
allowedTools,
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/routes/auto-mode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
import { createCommitFeatureHandler } from './routes/commit-feature.js';
import { createApprovePlanHandler } from './routes/approve-plan.js';
import { createResumeInterruptedHandler } from './routes/resume-interrupted.js';
import { createClarificationResponseHandler } from './routes/clarification-response.js';

export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
Expand Down Expand Up @@ -69,6 +70,11 @@ export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
validatePathParams('projectPath'),
createResumeInterruptedHandler(autoModeService)
);
router.post(
'/clarification-response',
validatePathParams('projectPath'),
createClarificationResponseHandler(autoModeService)
);

return router;
}
119 changes: 119 additions & 0 deletions apps/server/src/routes/auto-mode/routes/clarification-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* POST /clarification-response endpoint - Submit answers to clarification questions
*
* Used during interactive planning mode when the AI agent asks clarification
* questions via the AskUserQuestion tool.
*/

import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';

const logger = createLogger('AutoMode');

/** Shape of the clarification response request body */
interface ClarificationResponseBody {
featureId: string;
projectPath: string;
requestId: string;
answers: Record<string, string>;
}

/**
* Validates and parses the clarification response request body.
* Returns the validated body or an error message.
*/
function validateRequestBody(
body: unknown
): { success: true; data: ClarificationResponseBody } | { success: false; error: string } {
if (!body || typeof body !== 'object') {
return { success: false, error: 'Request body must be an object' };
}

const obj = body as Record<string, unknown>;

// Define required string fields
const requiredStringFields: Array<keyof ClarificationResponseBody> = [
'featureId',
'projectPath',
'requestId',
];

// Validate required string fields
for (const field of requiredStringFields) {
const value = obj[field];
if (typeof value !== 'string' || value.length === 0) {
return { success: false, error: `${field} is required and must be a non-empty string` };
}
}

// Validate answers object
const answers = obj.answers;
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) {
return { success: false, error: 'answers object is required' };
}

// Validate all answer values are strings
for (const [key, value] of Object.entries(answers as Record<string, unknown>)) {
if (typeof value !== 'string') {
return { success: false, error: `answers[${key}] must be a string` };
}
}

return {
success: true,
data: {
featureId: obj.featureId as string,
projectPath: obj.projectPath as string,
requestId: obj.requestId as string,
answers: answers as Record<string, string>,
},
};
}

export function createClarificationResponseHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
// Validate request body with type-safe validation
const validation = validateRequestBody(req.body);
if (!validation.success) {
res.status(400).json({
success: false,
error: validation.error,
});
return;
}

const { featureId, projectPath, requestId, answers } = validation.data;

logger.info(
`[AutoMode] Clarification response received for feature ${featureId}, requestId=${requestId}`
);

// Resolve the pending clarification
const result = await autoModeService.resolveClarification(
featureId,
requestId,
answers,
projectPath
);

if (!result.success) {
res.status(500).json({
success: false,
error: result.error,
});
return;
}

res.json({
success: true,
message: 'Clarification response submitted - agent will continue',
});
} catch (error) {
logError(error, 'Clarification response failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
Loading