Merge pull request #501 from AutoMaker-Org/feature/v0.11.0rc-1768426435282-1ogl

feat: centralize prompts and add customization UI for App Spec, Context, Suggestions, Tasks
This commit is contained in:
Shirone
2026-01-15 20:20:56 +00:00
committed by GitHub
25 changed files with 1925 additions and 885 deletions

View File

@@ -219,7 +219,7 @@ app.get('/api/health/detailed', createDetailedHandler());
app.use('/api/fs', createFsRoutes(events));
app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/features', createFeaturesRoutes(featureLoader, settingsService));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
app.use('/api/worktree', createWorktreeRoutes(events, settingsService));

View File

@@ -11,6 +11,14 @@ import {
mergeAgentPrompts,
mergeBacklogPlanPrompts,
mergeEnhancementPrompts,
mergeCommitMessagePrompts,
mergeTitleGenerationPrompts,
mergeIssueValidationPrompts,
mergeIdeationPrompts,
mergeAppSpecPrompts,
mergeContextDescriptionPrompts,
mergeSuggestionsPrompts,
mergeTaskExecutionPrompts,
} from '@automaker/prompts';
const logger = createLogger('SettingsHelper');
@@ -218,6 +226,14 @@ export async function getPromptCustomization(
agent: ReturnType<typeof mergeAgentPrompts>;
backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
enhancement: ReturnType<typeof mergeEnhancementPrompts>;
commitMessage: ReturnType<typeof mergeCommitMessagePrompts>;
titleGeneration: ReturnType<typeof mergeTitleGenerationPrompts>;
issueValidation: ReturnType<typeof mergeIssueValidationPrompts>;
ideation: ReturnType<typeof mergeIdeationPrompts>;
appSpec: ReturnType<typeof mergeAppSpecPrompts>;
contextDescription: ReturnType<typeof mergeContextDescriptionPrompts>;
suggestions: ReturnType<typeof mergeSuggestionsPrompts>;
taskExecution: ReturnType<typeof mergeTaskExecutionPrompts>;
}> {
let customization: PromptCustomization = {};
@@ -239,6 +255,14 @@ export async function getPromptCustomization(
agent: mergeAgentPrompts(customization.agent),
backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan),
enhancement: mergeEnhancementPrompts(customization.enhancement),
commitMessage: mergeCommitMessagePrompts(customization.commitMessage),
titleGeneration: mergeTitleGenerationPrompts(customization.titleGeneration),
issueValidation: mergeIssueValidationPrompts(customization.issueValidation),
ideation: mergeIdeationPrompts(customization.ideation),
appSpec: mergeAppSpecPrompts(customization.appSpec),
contextDescription: mergeContextDescriptionPrompts(customization.contextDescription),
suggestions: mergeSuggestionsPrompts(customization.suggestions),
taskExecution: mergeTaskExecutionPrompts(customization.taskExecution),
};
}

View File

@@ -14,7 +14,7 @@ import { streamingQuery } from '../../providers/simple-query-service.js';
import { parseAndCreateFeatures } from './parse-and-create-features.js';
import { getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -53,38 +53,16 @@ export async function generateFeaturesFromSpec(
return;
}
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[FeatureGeneration]');
const prompt = `Based on this project specification:
${spec}
Generate a prioritized list of implementable features. For each feature provide:
${prompts.appSpec.generateFeaturesFromSpecPrompt}
1. **id**: A unique lowercase-hyphenated identifier
2. **category**: Functional category (e.g., "Core", "UI", "API", "Authentication", "Database")
3. **title**: Short descriptive title
4. **description**: What this feature does (2-3 sentences)
5. **priority**: 1 (high), 2 (medium), or 3 (low)
6. **complexity**: "simple", "moderate", or "complex"
7. **dependencies**: Array of feature IDs this depends on (can be empty)
Format as JSON:
{
"features": [
{
"id": "feature-id",
"category": "Feature Category",
"title": "Feature Title",
"description": "What it does",
"priority": 1,
"complexity": "moderate",
"dependencies": []
}
]
}
Generate ${featureCount} features that build on each other logically.
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
Generate ${featureCount} features that build on each other logically.`;
logger.info('========== PROMPT BEING SENT ==========');
logger.info(`Prompt length: ${prompt.length} chars`);

View File

@@ -7,12 +7,7 @@
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import {
specOutputSchema,
specToXml,
getStructuredSpecPromptInstruction,
type SpecOutput,
} from '../../lib/app-spec-format.js';
import { specOutputSchema, specToXml, type SpecOutput } from '../../lib/app-spec-format.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
@@ -21,7 +16,7 @@ import { streamingQuery } from '../../providers/simple-query-service.js';
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -43,6 +38,9 @@ export async function generateSpec(
logger.info('analyzeProject:', analyzeProject);
logger.info('maxFeatures:', maxFeatures);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[SpecRegeneration]');
// Build the prompt based on whether we should analyze the project
let analysisInstructions = '';
let techStackDefaults = '';
@@ -66,9 +64,7 @@ export async function generateSpec(
Use these technologies as the foundation for the specification.`;
}
const prompt = `You are helping to define a software project specification.
IMPORTANT: Never ask for clarification or additional information. Use the information provided and make reasonable assumptions to create the best possible specification. If details are missing, infer them based on common patterns and best practices.
const prompt = `${prompts.appSpec.generateSpecSystemPrompt}
Project Overview:
${projectOverview}
@@ -77,7 +73,7 @@ ${techStackDefaults}
${analysisInstructions}
${getStructuredSpecPromptInstruction()}`;
${prompts.appSpec.structuredSpecInstructions}`;
logger.info('========== PROMPT BEING SENT ==========');
logger.info(`Prompt length: ${prompt.length} chars`);

View File

@@ -19,7 +19,10 @@ import { simpleQuery } from '../../../providers/simple-query-service.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeFile');
@@ -130,11 +133,12 @@ export function createDescribeFileHandler(
// Get the filename for context
const fileName = path.basename(resolvedPath);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[DescribeFile]');
// Build prompt with file content passed as structured data
// The file content is included directly, not via tool invocation
const prompt = `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
Respond with ONLY the description text, no additional formatting, preamble, or explanation.
const prompt = `${prompts.contextDescription.describeFilePrompt}
File: ${fileName}${truncated ? ' (truncated)' : ''}

View File

@@ -19,7 +19,10 @@ import { simpleQuery } from '../../../providers/simple-query-service.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeImage');
@@ -278,12 +281,11 @@ export function createDescribeImageHandler(
logger.info(`[${requestId}] Using model: ${model}`);
// Build the instruction text
const instructionText =
`Describe this image in 1-2 sentences suitable for use as context in an AI coding assistant. ` +
`Focus on what the image shows and its purpose (e.g., "UI mockup showing login form with email/password fields", ` +
`"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
`Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[DescribeImage]');
// Build the instruction text from centralized prompts
const instructionText = prompts.contextDescription.describeImagePrompt;
// Build prompt based on provider capability
// Some providers (like Cursor) may not support image content blocks

View File

@@ -4,6 +4,7 @@
import { Router } from 'express';
import { FeatureLoader } from '../../services/feature-loader.js';
import type { SettingsService } from '../../services/settings-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from './routes/list.js';
import { createGetHandler } from './routes/get.js';
@@ -15,7 +16,10 @@ import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
export function createFeaturesRoutes(
featureLoader: FeatureLoader,
settingsService?: SettingsService
): Router {
const router = Router();
router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
@@ -35,7 +39,7 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/raw-output', createRawOutputHandler(featureLoader));
router.post('/generate-title', createGenerateTitleHandler());
router.post('/generate-title', createGenerateTitleHandler(settingsService));
return router;
}

View File

@@ -9,6 +9,8 @@ import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
const logger = createLogger('GenerateTitle');
@@ -26,16 +28,9 @@ interface GenerateTitleErrorResponse {
error: string;
}
const SYSTEM_PROMPT = `You are a title generator. Your task is to create a concise, descriptive title (5-10 words max) for a software feature based on its description.
Rules:
- Output ONLY the title, nothing else
- Keep it short and action-oriented (e.g., "Add dark mode toggle", "Fix login validation")
- Start with a verb when possible (Add, Fix, Update, Implement, Create, etc.)
- No quotes, periods, or extra formatting
- Capture the essence of the feature in a scannable way`;
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
export function createGenerateTitleHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { description } = req.body as GenerateTitleRequestBody;
@@ -61,11 +56,15 @@ export function createGenerateTitleHandler(): (req: Request, res: Response) => P
logger.info(`Generating title for description: ${trimmedDescription.substring(0, 50)}...`);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[GenerateTitle]');
const systemPrompt = prompts.titleGeneration.systemPrompt;
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
// Use simpleQuery - provider abstraction handles all the streaming/extraction
const result = await simpleQuery({
prompt: `${SYSTEM_PROMPT}\n\n${userPrompt}`,
prompt: `${systemPrompt}\n\n${userPrompt}`,
model: CLAUDE_MODEL_MAP.haiku,
cwd: process.cwd(),
maxTurns: 1,

View File

@@ -30,11 +30,11 @@ import { writeValidation } from '../../../lib/validation-storage.js';
import { streamingQuery } from '../../../providers/simple-query-service.js';
import {
issueValidationSchema,
ISSUE_VALIDATION_SYSTEM_PROMPT,
buildValidationPrompt,
ValidationComment,
ValidationLinkedPR,
} from './validation-schema.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
import {
trySetValidationRunning,
clearValidationStatus,
@@ -117,13 +117,17 @@ async function runValidation(
let responseText = '';
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[ValidateIssue]');
const issueValidationSystemPrompt = prompts.issueValidation.systemPrompt;
// Determine if we should use structured output (Claude/Codex support it, Cursor/OpenCode don't)
const useStructuredOutput = isClaudeModel(model) || isCodexModel(model);
// Build the final prompt - for Cursor, include system prompt and JSON schema instructions
let finalPrompt = basePrompt;
if (!useStructuredOutput) {
finalPrompt = `${ISSUE_VALIDATION_SYSTEM_PROMPT}
finalPrompt = `${issueValidationSystemPrompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. Return the JSON in your response only.
@@ -167,7 +171,7 @@ ${basePrompt}`;
prompt: finalPrompt,
model: model as string,
cwd: projectPath,
systemPrompt: useStructuredOutput ? ISSUE_VALIDATION_SYSTEM_PROMPT : undefined,
systemPrompt: useStructuredOutput ? issueValidationSystemPrompt : undefined,
abortController,
thinkingLevel: effectiveThinkingLevel,
reasoningEffort: effectiveReasoningEffort,

View File

@@ -1,8 +1,11 @@
/**
* Issue Validation Schema and System Prompt
* Issue Validation Schema and Prompt Building
*
* Defines the JSON schema for Claude's structured output and
* the system prompt that guides the validation process.
* helper functions for building validation prompts.
*
* Note: The system prompt is now centralized in @automaker/prompts
* and accessed via getPromptCustomization() in validate-issue.ts
*/
/**
@@ -82,76 +85,6 @@ export const issueValidationSchema = {
additionalProperties: false,
} as const;
/**
* System prompt that guides Claude in validating GitHub issues.
* Instructs the model to use read-only tools to analyze the codebase.
*/
export const ISSUE_VALIDATION_SYSTEM_PROMPT = `You are an expert code analyst validating GitHub issues against a codebase.
Your task is to analyze a GitHub issue and determine if it's valid by scanning the codebase.
## Validation Process
1. **Read the issue carefully** - Understand what is being reported or requested
2. **Search the codebase** - Use Glob to find relevant files by pattern, Grep to search for keywords
3. **Examine the code** - Use Read to look at the actual implementation in relevant files
4. **Check linked PRs** - If there are linked pull requests, use \`gh pr diff <PR_NUMBER>\` to review the changes
5. **Form your verdict** - Based on your analysis, determine if the issue is valid
## Verdicts
- **valid**: The issue describes a real problem that exists in the codebase, or a clear feature request that can be implemented. The referenced files/components exist and the issue is actionable.
- **invalid**: The issue describes behavior that doesn't exist, references non-existent files or components, is based on a misunderstanding of the code, or the described "bug" is actually expected behavior.
- **needs_clarification**: The issue lacks sufficient detail to verify. Specify what additional information is needed in the missingInfo field.
## For Bug Reports, Check:
- Do the referenced files/components exist?
- Does the code match what the issue describes?
- Is the described behavior actually a bug or expected?
- Can you locate the code that would cause the reported issue?
## For Feature Requests, Check:
- Does the feature already exist?
- Is the implementation location clear?
- Is the request technically feasible given the codebase structure?
## Analyzing Linked Pull Requests
When an issue has linked PRs (especially open ones), you MUST analyze them:
1. **Run \`gh pr diff <PR_NUMBER>\`** to see what changes the PR makes
2. **Run \`gh pr view <PR_NUMBER>\`** to see PR description and status
3. **Evaluate if the PR fixes the issue** - Does the diff address the reported problem?
4. **Provide a recommendation**:
- \`wait_for_merge\`: The PR appears to fix the issue correctly. No additional work needed - just wait for it to be merged.
- \`pr_needs_work\`: The PR attempts to fix the issue but is incomplete or has problems.
- \`no_pr\`: No relevant PR exists for this issue.
5. **Include prAnalysis in your response** with:
- hasOpenPR: true/false
- prFixesIssue: true/false (based on diff analysis)
- prNumber: the PR number you analyzed
- prSummary: brief description of what the PR changes
- recommendation: one of the above values
## Response Guidelines
- **Always include relatedFiles** when you find relevant code
- **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the code
- **Provide a suggestedFix** when you have a clear idea of how to address the issue
- **Use missingInfo** when the verdict is needs_clarification to list what's needed
- **Include prAnalysis** when there are linked PRs - this is critical for avoiding duplicate work
- **Set estimatedComplexity** to help prioritize:
- trivial: Simple text changes, one-line fixes
- simple: Small changes to one file
- moderate: Changes to multiple files or moderate logic changes
- complex: Significant refactoring or new feature implementation
- very_complex: Major architectural changes or cross-cutting concerns
Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
/**
* Comment data structure for validation prompt
*/

View File

@@ -15,7 +15,7 @@ import { FeatureLoader } from '../../services/feature-loader.js';
import { getAppSpecPath } from '@automaker/platform';
import * as secureFs from '../../lib/secure-fs.js';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
const logger = createLogger('Suggestions');
@@ -137,11 +137,15 @@ export async function generateSuggestions(
modelOverride?: string,
thinkingLevelOverride?: ThinkingLevel
): Promise<void> {
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[Suggestions]');
// Map suggestion types to their prompts
const typePrompts: Record<string, string> = {
features: 'Analyze this project and suggest new features that would add value.',
refactoring: 'Analyze this project and identify refactoring opportunities.',
security: 'Analyze this project for security vulnerabilities and suggest fixes.',
performance: 'Analyze this project for performance issues and suggest optimizations.',
features: prompts.suggestions.featuresPrompt,
refactoring: prompts.suggestions.refactoringPrompt,
security: prompts.suggestions.securityPrompt,
performance: prompts.suggestions.performancePrompt,
};
// Load existing context to avoid duplicates
@@ -151,15 +155,7 @@ export async function generateSuggestions(
${existingContext}
${existingContext ? '\nIMPORTANT: Do NOT suggest features that are already implemented or already in the backlog above. Focus on NEW ideas that complement what already exists.\n' : ''}
Look at the codebase and provide 3-5 concrete suggestions.
For each suggestion, provide:
1. A category (e.g., "User Experience", "Security", "Performance")
2. A clear description of what to implement
3. Priority (1=high, 2=medium, 3=low)
4. Brief reasoning for why this would help
The response will be automatically formatted as structured JSON.`;
${prompts.suggestions.baseTemplate}`;
// Don't send initial message - let the agent output speak for itself
// The first agent message will be captured as an info entry

View File

@@ -579,6 +579,9 @@ export class AutoModeService {
'[AutoMode]'
);
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
// Build the prompt - use continuation prompt if provided (for recovery after plan approval)
let prompt: string;
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) and memory files
@@ -604,7 +607,7 @@ export class AutoModeService {
logger.info(`Using continuation prompt for feature ${featureId}`);
} else {
// Normal flow: build prompt with planning phase
const featurePrompt = this.buildFeaturePrompt(feature);
const featurePrompt = this.buildFeaturePrompt(feature, prompts.taskExecution);
const planningPrefix = await this.getPlanningPromptPrefix(feature);
prompt = planningPrefix + featurePrompt;
@@ -783,6 +786,9 @@ export class AutoModeService {
): Promise<void> {
logger.info(`Executing ${steps.length} pipeline step(s) for feature ${featureId}`);
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
// Load context files once with feature context for smart memory selection
const contextResult = await loadContextFiles({
projectPath,
@@ -827,7 +833,12 @@ export class AutoModeService {
});
// Build prompt for this pipeline step
const prompt = this.buildPipelineStepPrompt(step, feature, previousContext);
const prompt = this.buildPipelineStepPrompt(
step,
feature,
previousContext,
prompts.taskExecution
);
// Get model from feature
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
@@ -882,14 +893,18 @@ export class AutoModeService {
private buildPipelineStepPrompt(
step: PipelineStep,
feature: Feature,
previousContext: string
previousContext: string,
taskExecutionPrompts: {
implementationInstructions: string;
playwrightVerificationInstructions: string;
}
): string {
let prompt = `## Pipeline Step: ${step.name}
This is an automated pipeline step following the initial feature implementation.
### Feature Context
${this.buildFeaturePrompt(feature)}
${this.buildFeaturePrompt(feature, taskExecutionPrompts)}
`;
@@ -1279,6 +1294,9 @@ Complete the pipeline step instructions above. Review the previous work and appl
'[AutoMode]'
);
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) - passed as system prompt
const contextResult = await loadContextFiles({
projectPath,
@@ -1296,7 +1314,7 @@ Complete the pipeline step instructions above. Review the previous work and appl
// Build complete prompt with feature info, previous context, and follow-up instructions
let fullPrompt = `## Follow-up on Feature Implementation
${feature ? this.buildFeaturePrompt(feature) : `**Feature ID:** ${featureId}`}
${feature ? this.buildFeaturePrompt(feature, prompts.taskExecution) : `**Feature ID:** ${featureId}`}
`;
if (previousContext) {
@@ -1888,13 +1906,17 @@ Format your response as a structured markdown document.`;
content: editedPlan || feature.planSpec.content,
});
// Build continuation prompt and re-run the feature
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
// Build continuation prompt using centralized template
const planContent = editedPlan || feature.planSpec.content || '';
let continuationPrompt = `The plan/specification has been approved. `;
if (feedback) {
continuationPrompt += `\n\nUser feedback: ${feedback}\n\n`;
}
continuationPrompt += `Now proceed with the implementation as specified in the plan:\n\n${planContent}\n\nImplement the feature now.`;
let continuationPrompt = prompts.taskExecution.continuationAfterApprovalTemplate;
continuationPrompt = continuationPrompt.replace(
/\{\{userFeedback\}\}/g,
feedback || ''
);
continuationPrompt = continuationPrompt.replace(/\{\{approvedPlan\}\}/g, planContent);
logger.info(`Starting recovery execution for feature ${featureId}`);
@@ -2225,7 +2247,13 @@ Format your response as a structured markdown document.`;
return planningPrompt + '\n\n---\n\n## Feature Request\n\n';
}
private buildFeaturePrompt(feature: Feature): string {
private buildFeaturePrompt(
feature: Feature,
taskExecutionPrompts: {
implementationInstructions: string;
playwrightVerificationInstructions: string;
}
): string {
const title = this.extractTitleFromDescription(feature.description);
let prompt = `## Feature Implementation Task
@@ -2267,80 +2295,10 @@ You can use the Read tool to view these images at any time during implementation
// Add verification instructions based on testing mode
if (feature.skipTests) {
// Manual verification - just implement the feature
prompt += `
## Instructions
Implement this feature by:
1. First, explore the codebase to understand the existing structure
2. Plan your implementation approach
3. Write the necessary code changes
4. Ensure the code follows existing patterns and conventions
When done, wrap your final summary in <summary> tags like this:
<summary>
## Summary: [Feature Title]
### Changes Implemented
- [List of changes made]
### Files Modified
- [List of files]
### Notes for Developer
- [Any important notes]
</summary>
This helps parse your summary correctly in the output logs.`;
prompt += `\n${taskExecutionPrompts.implementationInstructions}`;
} else {
// Automated testing - implement and verify with Playwright
prompt += `
## Instructions
Implement this feature by:
1. First, explore the codebase to understand the existing structure
2. Plan your implementation approach
3. Write the necessary code changes
4. Ensure the code follows existing patterns and conventions
## Verification with Playwright (REQUIRED)
After implementing the feature, you MUST verify it works correctly using Playwright:
1. **Create a temporary Playwright test** to verify the feature works as expected
2. **Run the test** to confirm the feature is working
3. **Delete the test file** after verification - this is a temporary verification test, not a permanent test suite addition
Example verification workflow:
\`\`\`bash
# Create a simple verification test
npx playwright test my-verification-test.spec.ts
# After successful verification, delete the test
rm my-verification-test.spec.ts
\`\`\`
The test should verify the core functionality of the feature. If the test fails, fix the implementation and re-test.
When done, wrap your final summary in <summary> tags like this:
<summary>
## Summary: [Feature Title]
### Changes Implemented
- [List of changes made]
### Files Modified
- [List of files]
### Verification Status
- [Describe how the feature was verified with Playwright]
### Notes for Developer
- [Any important notes]
</summary>
This helps parse your summary correctly in the output logs.`;
prompt += `\n${taskExecutionPrompts.implementationInstructions}\n\n${taskExecutionPrompts.playwrightVerificationInstructions}`;
}
return prompt;
@@ -2910,6 +2868,12 @@ After generating the revised spec, output:
`Starting multi-agent execution: ${parsedTasks.length} tasks for feature ${featureId}`
);
// Get customized prompts for task execution
const taskPrompts = await getPromptCustomization(
this.settingsService,
'[AutoMode]'
);
// Execute each task with a separate agent
for (let taskIndex = 0; taskIndex < parsedTasks.length; taskIndex++) {
const task = parsedTasks[taskIndex];
@@ -2941,6 +2905,7 @@ After generating the revised spec, output:
parsedTasks,
taskIndex,
approvedPlanContent,
taskPrompts.taskExecution.taskPromptTemplate,
userFeedback
);
@@ -3023,15 +2988,21 @@ After generating the revised spec, output:
`No parsed tasks, using single-agent execution for feature ${featureId}`
);
const continuationPrompt = `The plan/specification has been approved. Now implement it.
${userFeedback ? `\n## User Feedback\n${userFeedback}\n` : ''}
## Approved Plan
${approvedPlanContent}
## Instructions
Implement all the changes described in the plan above.`;
// Get customized prompts for continuation
const taskPrompts = await getPromptCustomization(
this.settingsService,
'[AutoMode]'
);
let continuationPrompt =
taskPrompts.taskExecution.continuationAfterApprovalTemplate;
continuationPrompt = continuationPrompt.replace(
/\{\{userFeedback\}\}/g,
userFeedback || ''
);
continuationPrompt = continuationPrompt.replace(
/\{\{approvedPlan\}\}/g,
approvedPlanContent
);
const continuationStream = provider.executeQuery({
prompt: continuationPrompt,
@@ -3151,17 +3122,16 @@ Implement all the changes described in the plan above.`;
throw new Error(`Feature ${featureId} not found`);
}
const prompt = `## Continuing Feature Implementation
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
${this.buildFeaturePrompt(feature)}
// Build the feature prompt
const featurePrompt = this.buildFeaturePrompt(feature, prompts.taskExecution);
## Previous Context
The following is the output from a previous implementation attempt. Continue from where you left off:
${context}
## Instructions
Review the previous work and continue the implementation. If the feature appears complete, verify it works correctly.`;
// Use the resume feature template with variable substitution
let prompt = prompts.taskExecution.resumeFeatureTemplate;
prompt = prompt.replace(/\{\{featurePrompt\}\}/g, featurePrompt);
prompt = prompt.replace(/\{\{previousContext\}\}/g, context);
return this.executeFeature(projectPath, featureId, useWorktrees, false, undefined, {
continuationPrompt: prompt,
@@ -3282,68 +3252,42 @@ Review the previous work and continue the implementation. If the feature appears
allTasks: ParsedTask[],
taskIndex: number,
planContent: string,
taskPromptTemplate: string,
userFeedback?: string
): string {
const completedTasks = allTasks.slice(0, taskIndex);
const remainingTasks = allTasks.slice(taskIndex + 1);
let prompt = `# Task Execution: ${task.id}
// Build completed tasks string
const completedTasksStr =
completedTasks.length > 0
? `### Already Completed (${completedTasks.length} tasks)\n${completedTasks.map((t) => `- [x] ${t.id}: ${t.description}`).join('\n')}\n`
: '';
You are executing a specific task as part of a larger feature implementation.
// Build remaining tasks string
const remainingTasksStr =
remainingTasks.length > 0
? `### Coming Up Next (${remainingTasks.length} tasks remaining)\n${remainingTasks
.slice(0, 3)
.map((t) => `- [ ] ${t.id}: ${t.description}`)
.join(
'\n'
)}${remainingTasks.length > 3 ? `\n... and ${remainingTasks.length - 3} more tasks` : ''}\n`
: '';
## Your Current Task
// Build user feedback string
const userFeedbackStr = userFeedback ? `### User Feedback\n${userFeedback}\n` : '';
**Task ID:** ${task.id}
**Description:** ${task.description}
${task.filePath ? `**Primary File:** ${task.filePath}` : ''}
${task.phase ? `**Phase:** ${task.phase}` : ''}
## Context
`;
// Show what's already done
if (completedTasks.length > 0) {
prompt += `### Already Completed (${completedTasks.length} tasks)
${completedTasks.map((t) => `- [x] ${t.id}: ${t.description}`).join('\n')}
`;
}
// Show remaining tasks
if (remainingTasks.length > 0) {
prompt += `### Coming Up Next (${remainingTasks.length} tasks remaining)
${remainingTasks
.slice(0, 3)
.map((t) => `- [ ] ${t.id}: ${t.description}`)
.join('\n')}
${remainingTasks.length > 3 ? `... and ${remainingTasks.length - 3} more tasks` : ''}
`;
}
// Add user feedback if any
if (userFeedback) {
prompt += `### User Feedback
${userFeedback}
`;
}
// Add relevant excerpt from plan (just the task-related part to save context)
prompt += `### Reference: Full Plan
<details>
${planContent}
</details>
## Instructions
1. Focus ONLY on completing task ${task.id}: "${task.description}"
2. Do not work on other tasks
3. Use the existing codebase patterns
4. When done, summarize what you implemented
Begin implementing task ${task.id} now.`;
// Use centralized template with variable substitution
let prompt = taskPromptTemplate;
prompt = prompt.replace(/\{\{taskId\}\}/g, task.id);
prompt = prompt.replace(/\{\{taskDescription\}\}/g, task.description);
prompt = prompt.replace(/\{\{taskFilePath\}\}/g, task.filePath || '');
prompt = prompt.replace(/\{\{taskPhase\}\}/g, task.phase || '');
prompt = prompt.replace(/\{\{completedTasks\}\}/g, completedTasksStr);
prompt = prompt.replace(/\{\{remainingTasks\}\}/g, remainingTasksStr);
prompt = prompt.replace(/\{\{userFeedback\}\}/g, userFeedbackStr);
prompt = prompt.replace(/\{\{planContent\}\}/g, planContent);
return prompt;
}
@@ -3553,32 +3497,13 @@ Begin implementing task ${task.id} now.`;
// Limit output to avoid token limits
const truncatedOutput = agentOutput.length > 10000 ? agentOutput.slice(-10000) : agentOutput;
const userPrompt = `You are an Architecture Decision Record (ADR) extractor. Analyze this implementation and return ONLY JSON with learnings. No explanations.
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
Feature: "${feature.title}"
Implementation log:
${truncatedOutput}
Extract MEANINGFUL learnings - not obvious things. For each, capture:
- DECISIONS: Why this approach vs alternatives? What would break if changed?
- GOTCHAS: What was unexpected? What's the root cause? How to avoid?
- PATTERNS: Why this pattern? What problem does it solve? Trade-offs?
JSON format ONLY (no markdown, no text):
{"learnings": [{
"category": "architecture|api|ui|database|auth|testing|performance|security|gotchas",
"type": "decision|gotcha|pattern",
"content": "What was done/learned",
"context": "Problem being solved or situation faced",
"why": "Reasoning - why this approach",
"rejected": "Alternative considered and why rejected",
"tradeoffs": "What became easier/harder",
"breaking": "What breaks if this is changed/removed"
}]}
IMPORTANT: Only include NON-OBVIOUS learnings with real reasoning. Skip trivial patterns.
If nothing notable: {"learnings": []}`;
// Build user prompt using centralized template with variable substitution
let userPrompt = prompts.taskExecution.learningExtractionUserPromptTemplate;
userPrompt = userPrompt.replace(/\{\{featureTitle\}\}/g, feature.title || '');
userPrompt = userPrompt.replace(/\{\{implementationLog\}\}/g, truncatedOutput);
try {
// Get model from phase settings
@@ -3612,8 +3537,7 @@ If nothing notable: {"learnings": []}`;
cwd: projectPath,
maxTurns: 1,
allowedTools: [],
systemPrompt:
'You are a JSON extraction assistant. You MUST respond with ONLY valid JSON, no explanations, no markdown, no other text. Extract learnings from the provided implementation context and return them as JSON.',
systemPrompt: prompts.taskExecution.learningExtractionSystemPrompt,
});
const responseText = result.text;

View File

@@ -41,6 +41,7 @@ import type { FeatureLoader } from './feature-loader.js';
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
import { resolveModelString } from '@automaker/model-resolver';
import { stripProviderPrefix } from '@automaker/types';
import { getPromptCustomization } from '../lib/settings-helpers.js';
const logger = createLogger('IdeationService');
@@ -195,8 +196,12 @@ export class IdeationService {
// Gather existing features and ideas to prevent duplicate suggestions
const existingWorkContext = await this.gatherExistingWorkContext(projectPath);
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[IdeationService]');
// Build system prompt for ideation
const systemPrompt = this.buildIdeationSystemPrompt(
prompts.ideation.ideationSystemPrompt,
contextResult.formattedPrompt,
activeSession.session.promptCategory,
existingWorkContext
@@ -645,8 +650,12 @@ export class IdeationService {
// Gather existing features and ideas to prevent duplicates
const existingWorkContext = await this.gatherExistingWorkContext(projectPath);
// Get customized prompts from settings
const prompts = await getPromptCustomization(this.settingsService, '[IdeationService]');
// Build system prompt for structured suggestions
const systemPrompt = this.buildSuggestionsSystemPrompt(
prompts.ideation.suggestionsSystemPrompt,
contextPrompt,
category,
count,
@@ -721,8 +730,14 @@ export class IdeationService {
/**
* Build system prompt for structured suggestion generation
* @param basePrompt - The base system prompt from settings
* @param contextFilesPrompt - Project context from loaded files
* @param category - The idea category to focus on
* @param count - Number of suggestions to generate
* @param existingWorkContext - Context about existing features/ideas
*/
private buildSuggestionsSystemPrompt(
basePrompt: string,
contextFilesPrompt: string | undefined,
category: IdeaCategory,
count: number = 10,
@@ -734,35 +749,18 @@ export class IdeationService {
const existingWorkSection = existingWorkContext ? `\n\n${existingWorkContext}` : '';
return `You are an AI product strategist helping brainstorm feature ideas for a software project.
// Replace placeholder {{count}} if present, otherwise append count instruction
let prompt = basePrompt;
if (prompt.includes('{{count}}')) {
prompt = prompt.replace(/\{\{count\}\}/g, String(count));
} else {
prompt += `\n\nGenerate exactly ${count} suggestions.`;
}
IMPORTANT: You do NOT have access to any tools. You CANNOT read files, search code, or run commands.
You must generate suggestions based ONLY on the project context provided below.
Do NOT say "I'll analyze" or "Let me explore" - you cannot do those things.
Based on the project context and the user's prompt, generate exactly ${count} creative and actionable feature suggestions.
YOUR RESPONSE MUST BE ONLY A JSON ARRAY - nothing else. No explanation, no preamble, no markdown code fences.
Each suggestion must have this structure:
{
"title": "Short, actionable title (max 60 chars)",
"description": "Clear description of what to build or improve (2-3 sentences)",
"rationale": "Why this is valuable - the problem it solves or opportunity it creates",
"priority": "high" | "medium" | "low"
}
return `${prompt}
Focus area: ${this.getCategoryDescription(category)}
Guidelines:
- Generate exactly ${count} suggestions
- Be specific and actionable - avoid vague ideas
- Mix different priority levels (some high, some medium, some low)
- Each suggestion should be independently implementable
- Think creatively - include both obvious improvements and innovative ideas
- Consider the project's domain and target users
- IMPORTANT: Do NOT suggest features or ideas that already exist in the "Existing Features" or "Existing Ideas" sections below
${contextSection}${existingWorkSection}`;
}
@@ -1269,30 +1267,11 @@ ${contextSection}${existingWorkSection}`;
// ============================================================================
private buildIdeationSystemPrompt(
basePrompt: string,
contextFilesPrompt: string | undefined,
category?: IdeaCategory,
existingWorkContext?: string
): string {
const basePrompt = `You are an AI product strategist and UX expert helping brainstorm ideas for improving a software project.
Your role is to:
- Analyze the codebase structure and patterns
- Identify opportunities for improvement
- Suggest actionable ideas with clear rationale
- Consider user experience, technical feasibility, and business value
- Be specific and reference actual files/components when possible
When suggesting ideas:
1. Provide a clear, concise title
2. Explain the problem or opportunity
3. Describe the proposed solution
4. Highlight the expected benefit
5. Note any dependencies or considerations
IMPORTANT: Do NOT suggest features or ideas that already exist in the project. Check the "Existing Features" and "Existing Ideas" sections below to avoid duplicates.
Focus on practical, implementable suggestions that would genuinely improve the product.`;
const categoryContext = category
? `\n\nFocus area: ${this.getCategoryDescription(category)}`
: '';

View File

@@ -286,6 +286,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
model: 'claude-opus-4-5-20251101',
cwd: '/test',
});
@@ -312,6 +313,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
model: 'claude-opus-4-5-20251101',
cwd: '/test',
});
@@ -339,6 +341,7 @@ describe('claude-provider.ts', () => {
const generator = provider.executeQuery({
prompt: 'Test',
model: 'claude-opus-4-5-20251101',
cwd: '/test',
});

View File

@@ -202,8 +202,17 @@ describe('auto-mode-service.ts - Planning Mode', () => {
});
describe('buildFeaturePrompt', () => {
const buildFeaturePrompt = (svc: any, feature: any) => {
return svc.buildFeaturePrompt(feature);
const defaultTaskExecutionPrompts = {
implementationInstructions: 'Test implementation instructions',
playwrightVerificationInstructions: 'Test playwright instructions',
};
const buildFeaturePrompt = (
svc: any,
feature: any,
taskExecutionPrompts = defaultTaskExecutionPrompts
) => {
return svc.buildFeaturePrompt(feature, taskExecutionPrompts);
};
it('should include feature ID and description', () => {
@@ -242,14 +251,15 @@ describe('auto-mode-service.ts - Planning Mode', () => {
expect(result).toContain('/tmp/image2.jpg');
});
it('should include summary tags instruction', () => {
it('should include implementation instructions', () => {
const feature = {
id: 'feat-123',
description: 'Test feature',
};
const result = buildFeaturePrompt(service, feature);
expect(result).toContain('<summary>');
expect(result).toContain('</summary>');
// The prompt should include the implementation instructions passed to it
expect(result).toContain('Test implementation instructions');
expect(result).toContain('Test playwright instructions');
});
});

View File

@@ -91,7 +91,7 @@ describe('claude-usage-service.ts', () => {
it("should use 'where' command on Windows", async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const windowsService = new ClaudeUsageService(); // Create new service after platform mock
const ptyService = new ClaudeUsageService(); // Create new service after platform mock
mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
if (event === 'close') {
@@ -100,7 +100,7 @@ describe('claude-usage-service.ts', () => {
return mockSpawnProcess;
});
await windowsService.isAvailable();
await ptyService.isAvailable();
expect(spawn).toHaveBeenCalledWith('where', ['claude']);
});
@@ -403,120 +403,22 @@ Resets Jan 15, 3pm
});
});
describe('executeClaudeUsageCommandMac', () => {
beforeEach(() => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({ HOME: '/Users/testuser' });
});
it('should execute expect script and return output', async () => {
const mockOutput = `
Current session
65% left
Resets in 2h
`;
let stdoutCallback: Function;
let closeCallback: Function;
mockSpawnProcess.stdout = {
on: vi.fn((event: string, callback: Function) => {
if (event === 'data') {
stdoutCallback = callback;
}
}),
};
mockSpawnProcess.stderr = {
on: vi.fn(),
};
mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
if (event === 'close') {
closeCallback = callback;
}
return mockSpawnProcess;
});
const promise = service.fetchUsageData();
// Simulate stdout data
stdoutCallback!(Buffer.from(mockOutput));
// Simulate successful close
closeCallback!(0);
const result = await promise;
expect(result.sessionPercentage).toBe(35); // 100 - 65
expect(spawn).toHaveBeenCalledWith(
'expect',
expect.arrayContaining(['-c']),
expect.any(Object)
);
});
it('should handle authentication errors', async () => {
const mockOutput = 'token_expired';
let stdoutCallback: Function;
let closeCallback: Function;
mockSpawnProcess.stdout = {
on: vi.fn((event: string, callback: Function) => {
if (event === 'data') {
stdoutCallback = callback;
}
}),
};
mockSpawnProcess.stderr = {
on: vi.fn(),
};
mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
if (event === 'close') {
closeCallback = callback;
}
return mockSpawnProcess;
});
const promise = service.fetchUsageData();
stdoutCallback!(Buffer.from(mockOutput));
closeCallback!(1);
await expect(promise).rejects.toThrow('Authentication required');
});
it('should handle timeout with no data', async () => {
vi.useFakeTimers();
mockSpawnProcess.stdout = {
on: vi.fn(),
};
mockSpawnProcess.stderr = {
on: vi.fn(),
};
mockSpawnProcess.on = vi.fn(() => mockSpawnProcess);
mockSpawnProcess.kill = vi.fn();
const promise = service.fetchUsageData();
// Advance time past timeout (30 seconds)
vi.advanceTimersByTime(31000);
await expect(promise).rejects.toThrow('Command timed out');
vi.useRealTimers();
// Note: executeClaudeUsageCommandMac tests removed - the service now uses PTY for all platforms
// The executeClaudeUsageCommandMac method exists but is dead code (never called)
describe.skip('executeClaudeUsageCommandMac (deprecated - uses PTY now)', () => {
it('should be skipped - service now uses PTY for all platforms', () => {
expect(true).toBe(true);
});
});
describe('executeClaudeUsageCommandWindows', () => {
describe('executeClaudeUsageCommandPty', () => {
// Note: The service now uses PTY for all platforms, using process.cwd() as the working directory
beforeEach(() => {
vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(os.homedir).mockReturnValue('C:\\Users\\testuser');
vi.spyOn(process, 'env', 'get').mockReturnValue({ USERPROFILE: 'C:\\Users\\testuser' });
});
it('should use node-pty on Windows and return output', async () => {
const windowsService = new ClaudeUsageService(); // Create new service for Windows platform
it('should use node-pty and return output', async () => {
const ptyService = new ClaudeUsageService();
const mockOutput = `
Current session
65% left
@@ -538,7 +440,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
const promise = windowsService.fetchUsageData();
const promise = ptyService.fetchUsageData();
// Simulate data
dataCallback!(mockOutput);
@@ -549,16 +451,19 @@ Resets in 2h
const result = await promise;
expect(result.sessionPercentage).toBe(35);
// Service uses process.cwd() for --add-dir
expect(pty.spawn).toHaveBeenCalledWith(
'cmd.exe',
['/c', 'claude', '--add-dir', 'C:\\Users\\testuser'],
expect.any(Object)
['/c', 'claude', '--add-dir', process.cwd()],
expect.objectContaining({
cwd: process.cwd(),
})
);
});
it('should send escape key after seeing usage data', async () => {
vi.useFakeTimers();
const windowsService = new ClaudeUsageService();
const ptyService = new ClaudeUsageService();
const mockOutput = 'Current session\n65% left';
@@ -577,7 +482,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
const promise = windowsService.fetchUsageData();
const promise = ptyService.fetchUsageData();
// Simulate seeing usage data
dataCallback!(mockOutput);
@@ -594,8 +499,8 @@ Resets in 2h
vi.useRealTimers();
});
it('should handle authentication errors on Windows', async () => {
const windowsService = new ClaudeUsageService();
it('should handle authentication errors', async () => {
const ptyService = new ClaudeUsageService();
let dataCallback: Function | undefined;
let exitCallback: Function | undefined;
@@ -611,7 +516,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
const promise = windowsService.fetchUsageData();
const promise = ptyService.fetchUsageData();
dataCallback!('authentication_error');
@@ -620,9 +525,9 @@ Resets in 2h
);
});
it('should handle timeout with no data on Windows', async () => {
it('should handle timeout with no data', async () => {
vi.useFakeTimers();
const windowsService = new ClaudeUsageService();
const ptyService = new ClaudeUsageService();
const mockPty = {
onData: vi.fn(),
@@ -633,7 +538,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
const promise = windowsService.fetchUsageData();
const promise = ptyService.fetchUsageData();
// Advance time past timeout (45 seconds)
vi.advanceTimersByTime(46000);
@@ -648,7 +553,7 @@ Resets in 2h
it('should return data on timeout if data was captured', async () => {
vi.useFakeTimers();
const windowsService = new ClaudeUsageService();
const ptyService = new ClaudeUsageService();
let dataCallback: Function | undefined;
@@ -663,7 +568,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
const promise = windowsService.fetchUsageData();
const promise = ptyService.fetchUsageData();
// Simulate receiving usage data
dataCallback!('Current session\n65% left\nResets in 2h');
@@ -681,7 +586,7 @@ Resets in 2h
it('should send SIGTERM after ESC if process does not exit', async () => {
vi.useFakeTimers();
const windowsService = new ClaudeUsageService();
const ptyService = new ClaudeUsageService();
let dataCallback: Function | undefined;
@@ -696,7 +601,7 @@ Resets in 2h
};
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
windowsService.fetchUsageData();
ptyService.fetchUsageData();
// Simulate seeing usage data
dataCallback!('Current session\n65% left');

View File

@@ -0,0 +1,159 @@
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { Info, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { PromptCustomization, CustomPrompt } from '@automaker/types';
import type { BannerConfig, PromptFieldConfig, PromptFieldProps } from './types';
/**
* Calculate dynamic minimum height based on content length.
* Ensures long prompts have adequate space.
*/
export function calculateMinHeight(text: string): string {
const lines = text.split('\n').length;
const estimatedLines = Math.max(lines, Math.ceil(text.length / 80));
const minHeight = Math.min(Math.max(120, estimatedLines * 20), 600);
return `${minHeight}px`;
}
/**
* Renders an info or warning banner.
*/
export function Banner({ config }: { config: BannerConfig }) {
const isWarning = config.type === 'warning';
const Icon = isWarning ? AlertTriangle : Info;
return (
<div
className={cn(
'flex items-start gap-3 p-4 rounded-xl',
isWarning
? 'bg-amber-500/10 border border-amber-500/20'
: 'bg-blue-500/10 border border-blue-500/20'
)}
>
<Icon
className={cn('w-5 h-5 mt-0.5 shrink-0', isWarning ? 'text-amber-500' : 'text-blue-500')}
/>
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">{config.title}</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">{config.description}</p>
</div>
</div>
);
}
/**
* PromptField Component
*
* Shows a prompt with a toggle to switch between default and custom mode.
* - Toggle OFF: Shows default prompt in read-only mode
* - Toggle ON: Allows editing, custom value is used instead of default
*
* Custom value is always preserved, even when toggle is OFF.
*/
export function PromptField({
label,
description,
defaultValue,
customValue,
onCustomValueChange,
critical = false,
}: PromptFieldProps) {
const isEnabled = customValue?.enabled ?? false;
const displayValue = isEnabled ? (customValue?.value ?? defaultValue) : defaultValue;
const minHeight = calculateMinHeight(displayValue);
const handleToggle = (enabled: boolean) => {
const value = customValue?.value ?? defaultValue;
onCustomValueChange({ value, enabled });
};
const handleTextChange = (newValue: string) => {
if (isEnabled) {
onCustomValueChange({ value: newValue, enabled: true });
}
};
return (
<div className="space-y-2">
{critical && isEnabled && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-4 h-4 text-amber-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-xs font-medium text-amber-500">Critical Prompt</p>
<p className="text-xs text-muted-foreground mt-1">
This prompt requires a specific output format. Changing it incorrectly may break
functionality. Only modify if you understand the expected structure.
</p>
</div>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor={label} className="text-sm font-medium">
{label}
</Label>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{isEnabled ? 'Custom' : 'Default'}</span>
<Switch
checked={isEnabled}
onCheckedChange={handleToggle}
className="data-[state=checked]:bg-brand-500"
/>
</div>
</div>
<Textarea
id={label}
value={displayValue}
onChange={(e) => handleTextChange(e.target.value)}
readOnly={!isEnabled}
style={{ minHeight }}
className={cn(
'font-mono text-xs resize-y',
!isEnabled && 'cursor-not-allowed bg-muted/50 text-muted-foreground'
)}
/>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
);
}
/**
* Renders a list of prompt fields from configuration.
*/
export function PromptFieldList({
fields,
category,
promptCustomization,
updatePrompt,
}: {
fields: PromptFieldConfig[];
category: keyof PromptCustomization;
promptCustomization?: PromptCustomization;
updatePrompt: (
category: keyof PromptCustomization,
field: string,
value: CustomPrompt | undefined
) => void;
}) {
return (
<>
{fields.map((field) => (
<PromptField
key={field.key}
label={field.label}
description={field.description}
defaultValue={field.defaultValue}
customValue={
(promptCustomization?.[category] as Record<string, CustomPrompt> | undefined)?.[
field.key
]
}
onCustomValueChange={(value) => updatePrompt(category, field.key, value)}
critical={field.critical}
/>
))}
</>
);
}

View File

@@ -1,135 +1,17 @@
import { useState } from 'react';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
MessageSquareText,
Bot,
KanbanSquare,
Sparkles,
RotateCcw,
Info,
AlertTriangle,
GitCommitHorizontal,
} from 'lucide-react';
import { MessageSquareText, RotateCcw, Info } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { PromptCustomization, CustomPrompt } from '@automaker/types';
import {
DEFAULT_AUTO_MODE_PROMPTS,
DEFAULT_AGENT_PROMPTS,
DEFAULT_BACKLOG_PLAN_PROMPTS,
DEFAULT_ENHANCEMENT_PROMPTS,
DEFAULT_COMMIT_MESSAGE_PROMPTS,
} from '@automaker/prompts';
import { TAB_CONFIGS } from './tab-configs';
import { Banner, PromptFieldList } from './components';
interface PromptCustomizationSectionProps {
promptCustomization?: PromptCustomization;
onPromptCustomizationChange: (customization: PromptCustomization) => void;
}
interface PromptFieldProps {
label: string;
description: string;
defaultValue: string;
customValue?: CustomPrompt;
onCustomValueChange: (value: CustomPrompt | undefined) => void;
critical?: boolean; // Whether this prompt requires strict output format
}
/**
* Calculate dynamic minimum height based on content length
* Ensures long prompts have adequate space
*/
function calculateMinHeight(text: string): string {
const lines = text.split('\n').length;
const estimatedLines = Math.max(lines, Math.ceil(text.length / 80));
// Min 120px, scales up for longer content, max 600px
const minHeight = Math.min(Math.max(120, estimatedLines * 20), 600);
return `${minHeight}px`;
}
/**
* PromptField Component
*
* Shows a prompt with a toggle to switch between default and custom mode.
* - Toggle OFF: Shows default prompt in read-only mode, custom value is preserved but not used
* - Toggle ON: Allows editing, custom value is used instead of default
*
* IMPORTANT: Custom value is ALWAYS preserved, even when toggle is OFF.
* This prevents users from losing their work when temporarily switching to default.
*/
function PromptField({
label,
description,
defaultValue,
customValue,
onCustomValueChange,
critical = false,
}: PromptFieldProps) {
const isEnabled = customValue?.enabled ?? false;
const displayValue = isEnabled ? (customValue?.value ?? defaultValue) : defaultValue;
const minHeight = calculateMinHeight(displayValue);
const handleToggle = (enabled: boolean) => {
// When toggling, preserve the existing custom value if it exists,
// otherwise initialize with the default value.
const value = customValue?.value ?? defaultValue;
onCustomValueChange({ value, enabled });
};
const handleTextChange = (newValue: string) => {
// Only allow editing when enabled
if (isEnabled) {
onCustomValueChange({ value: newValue, enabled: true });
}
};
return (
<div className="space-y-2">
{critical && isEnabled && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-xs font-medium text-amber-500">Critical Prompt</p>
<p className="text-xs text-muted-foreground mt-1">
This prompt requires a specific output format. Changing it incorrectly may break
functionality. Only modify if you understand the expected structure.
</p>
</div>
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor={label} className="text-sm font-medium">
{label}
</Label>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{isEnabled ? 'Custom' : 'Default'}</span>
<Switch
checked={isEnabled}
onCheckedChange={handleToggle}
className="data-[state=checked]:bg-brand-500"
/>
</div>
</div>
<Textarea
id={label}
value={displayValue}
onChange={(e) => handleTextChange(e.target.value)}
readOnly={!isEnabled}
style={{ minHeight }}
className={cn(
'font-mono text-xs resize-y',
!isEnabled && 'cursor-not-allowed bg-muted/50 text-muted-foreground'
)}
/>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
);
}
/**
* PromptCustomizationSection Component
*
@@ -138,6 +20,7 @@ function PromptField({
* - Agent Runner (interactive chat)
* - Backlog Plan (Kanban planning)
* - Enhancement (feature description improvement)
* - And many more...
*/
export function PromptCustomizationSection({
promptCustomization = {},
@@ -145,9 +28,9 @@ export function PromptCustomizationSection({
}: PromptCustomizationSectionProps) {
const [activeTab, setActiveTab] = useState('auto-mode');
const updatePrompt = <T extends keyof PromptCustomization>(
category: T,
field: keyof NonNullable<PromptCustomization[T]>,
const updatePrompt = (
category: keyof PromptCustomization,
field: string,
value: CustomPrompt | undefined
) => {
const updated = {
@@ -206,7 +89,7 @@ export function PromptCustomizationSection({
{/* Info Banner */}
<div className="px-6 pt-6">
<div className="flex items-start gap-3 p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
<Info className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<Info className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">How to Customize Prompts</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
@@ -221,262 +104,71 @@ export function PromptCustomizationSection({
{/* Tabs */}
<div className="p-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-5 w-full">
<TabsTrigger value="auto-mode" className="gap-2">
<Bot className="w-4 h-4" />
Auto Mode
</TabsTrigger>
<TabsTrigger value="agent" className="gap-2">
<MessageSquareText className="w-4 h-4" />
Agent
</TabsTrigger>
<TabsTrigger value="backlog-plan" className="gap-2">
<KanbanSquare className="w-4 h-4" />
Backlog Plan
</TabsTrigger>
<TabsTrigger value="enhancement" className="gap-2">
<Sparkles className="w-4 h-4" />
Enhancement
</TabsTrigger>
<TabsTrigger value="commit-message" className="gap-2">
<GitCommitHorizontal className="w-4 h-4" />
Commit
</TabsTrigger>
<TabsList className="grid grid-cols-4 gap-1 h-auto w-full bg-transparent p-0">
{TAB_CONFIGS.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id} className="gap-2">
<tab.icon className="w-4 h-4" />
{tab.label}
</TabsTrigger>
))}
</TabsList>
{/* Auto Mode Tab */}
<TabsContent value="auto-mode" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Auto Mode Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('autoMode')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
{/* Info Banner for Auto Mode */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
<Info className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">Planning Mode Markers</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Planning prompts use special markers like{' '}
<code className="px-1 py-0.5 rounded bg-muted text-xs">[PLAN_GENERATED]</code> and{' '}
<code className="px-1 py-0.5 rounded bg-muted text-xs">[SPEC_GENERATED]</code> to
control the Auto Mode workflow. These markers must be preserved for proper
functionality.
</p>
{TAB_CONFIGS.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="space-y-6 mt-6">
{/* Tab Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">{tab.title}</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults(tab.category)}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
</div>
<div className="space-y-4">
<PromptField
label="Planning: Lite Mode"
description="Quick planning outline without approval requirement"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningLite}
customValue={promptCustomization?.autoMode?.planningLite}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningLite', value)}
critical={true}
/>
{/* Tab Banner */}
{tab.banner && <Banner config={tab.banner} />}
<PromptField
label="Planning: Lite with Approval"
description="Planning outline that waits for user approval"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningLiteWithApproval}
customValue={promptCustomization?.autoMode?.planningLiteWithApproval}
onCustomValueChange={(value) =>
updatePrompt('autoMode', 'planningLiteWithApproval', value)
}
critical={true}
/>
{/* Main Fields */}
{tab.fields.length > 0 && (
<div className="space-y-4">
<PromptFieldList
fields={tab.fields}
category={tab.category}
promptCustomization={promptCustomization}
updatePrompt={updatePrompt}
/>
</div>
)}
<PromptField
label="Planning: Spec Mode"
description="Detailed specification with task breakdown"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningSpec}
customValue={promptCustomization?.autoMode?.planningSpec}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningSpec', value)}
critical={true}
/>
<PromptField
label="Planning: Full SDD Mode"
description="Comprehensive Software Design Document with phased implementation"
defaultValue={DEFAULT_AUTO_MODE_PROMPTS.planningFull}
customValue={promptCustomization?.autoMode?.planningFull}
onCustomValueChange={(value) => updatePrompt('autoMode', 'planningFull', value)}
critical={true}
/>
</div>
</TabsContent>
{/* Agent Tab */}
<TabsContent value="agent" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Agent Runner Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('agent')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
<div className="space-y-4">
<PromptField
label="System Prompt"
description="Defines the AI's role and behavior in interactive chat sessions"
defaultValue={DEFAULT_AGENT_PROMPTS.systemPrompt}
customValue={promptCustomization?.agent?.systemPrompt}
onCustomValueChange={(value) => updatePrompt('agent', 'systemPrompt', value)}
/>
</div>
</TabsContent>
{/* Backlog Plan Tab */}
<TabsContent value="backlog-plan" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Backlog Planning Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('backlogPlan')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
{/* Critical Warning for Backlog Plan */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm text-foreground font-medium">Warning: Critical Prompts</p>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Backlog plan prompts require a strict JSON output format. Modifying these prompts
incorrectly can break the backlog planning feature and potentially corrupt your
feature data. Only customize if you fully understand the expected output
structure.
</p>
</div>
</div>
<div className="space-y-4">
<PromptField
label="System Prompt"
description="Defines how the AI modifies the feature backlog (Plan button on Kanban board)"
defaultValue={DEFAULT_BACKLOG_PLAN_PROMPTS.systemPrompt}
customValue={promptCustomization?.backlogPlan?.systemPrompt}
onCustomValueChange={(value) => updatePrompt('backlogPlan', 'systemPrompt', value)}
critical={true}
/>
</div>
</TabsContent>
{/* Enhancement Tab */}
<TabsContent value="enhancement" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Enhancement Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('enhancement')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
<div className="space-y-4">
<PromptField
label="Improve Mode"
description="Transform vague requests into clear, actionable tasks"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.improveSystemPrompt}
customValue={promptCustomization?.enhancement?.improveSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'improveSystemPrompt', value)
}
/>
<PromptField
label="Technical Mode"
description="Add implementation details and technical specifications"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.technicalSystemPrompt}
customValue={promptCustomization?.enhancement?.technicalSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'technicalSystemPrompt', value)
}
/>
<PromptField
label="Simplify Mode"
description="Make verbose descriptions concise and focused"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.simplifySystemPrompt}
customValue={promptCustomization?.enhancement?.simplifySystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'simplifySystemPrompt', value)
}
/>
<PromptField
label="Acceptance Criteria Mode"
description="Add testable acceptance criteria to descriptions"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.acceptanceSystemPrompt}
customValue={promptCustomization?.enhancement?.acceptanceSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'acceptanceSystemPrompt', value)
}
/>
<PromptField
label="User Experience Mode"
description="Review and enhance from a user experience and design perspective"
defaultValue={DEFAULT_ENHANCEMENT_PROMPTS.uxReviewerSystemPrompt}
customValue={promptCustomization?.enhancement?.uxReviewerSystemPrompt}
onCustomValueChange={(value) =>
updatePrompt('enhancement', 'uxReviewerSystemPrompt', value)
}
/>
</div>
</TabsContent>
{/* Commit Message Tab */}
<TabsContent value="commit-message" className="space-y-6 mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-foreground">Commit Message Prompts</h3>
<Button
variant="ghost"
size="sm"
onClick={() => resetToDefaults('commitMessage')}
className="gap-2"
>
<RotateCcw className="w-3 h-3" />
Reset Section
</Button>
</div>
<div className="space-y-4">
<PromptField
label="System Prompt"
description="Instructions for generating git commit messages from diffs. The AI will receive the git diff and generate a conventional commit message."
defaultValue={DEFAULT_COMMIT_MESSAGE_PROMPTS.systemPrompt}
customValue={promptCustomization?.commitMessage?.systemPrompt}
onCustomValueChange={(value) =>
updatePrompt('commitMessage', 'systemPrompt', value)
}
/>
</div>
</TabsContent>
{/* Sections (for tabs like Auto Mode with grouped fields) */}
{tab.sections?.map((section, idx) => (
<div key={idx} className="pt-4 border-t border-border/50">
{section.title && (
<h4 className="text-sm font-medium text-muted-foreground mb-4">
{section.title}
</h4>
)}
{section.banner && (
<div className="mb-4">
<Banner config={section.banner} />
</div>
)}
<div className="space-y-4">
<PromptFieldList
fields={section.fields}
category={tab.category}
promptCustomization={promptCustomization}
updatePrompt={updatePrompt}
/>
</div>
</div>
))}
</TabsContent>
))}
</Tabs>
</div>
</div>

View File

@@ -0,0 +1,448 @@
import {
MessageSquareText,
Bot,
KanbanSquare,
Sparkles,
GitCommitHorizontal,
Type,
CheckCircle,
Lightbulb,
FileCode,
FileText,
Wand2,
Cog,
} from 'lucide-react';
import {
DEFAULT_AUTO_MODE_PROMPTS,
DEFAULT_AGENT_PROMPTS,
DEFAULT_BACKLOG_PLAN_PROMPTS,
DEFAULT_ENHANCEMENT_PROMPTS,
DEFAULT_COMMIT_MESSAGE_PROMPTS,
DEFAULT_TITLE_GENERATION_PROMPTS,
DEFAULT_ISSUE_VALIDATION_PROMPTS,
DEFAULT_IDEATION_PROMPTS,
DEFAULT_APP_SPEC_PROMPTS,
DEFAULT_CONTEXT_DESCRIPTION_PROMPTS,
DEFAULT_SUGGESTIONS_PROMPTS,
DEFAULT_TASK_EXECUTION_PROMPTS,
} from '@automaker/prompts';
import type { TabConfig } from './types';
/**
* Configuration for all prompt customization tabs.
* Each tab defines its fields, banners, and optional sections.
*/
export const TAB_CONFIGS: TabConfig[] = [
{
id: 'auto-mode',
label: 'Auto Mode',
icon: Bot,
title: 'Auto Mode Prompts',
category: 'autoMode',
banner: {
type: 'info',
title: 'Planning Mode Markers',
description:
'Planning prompts use special markers like [PLAN_GENERATED] and [SPEC_GENERATED] to control the Auto Mode workflow. These markers must be preserved for proper functionality.',
},
fields: [
{
key: 'planningLite',
label: 'Planning: Lite Mode',
description: 'Quick planning outline without approval requirement',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.planningLite,
critical: true,
},
{
key: 'planningLiteWithApproval',
label: 'Planning: Lite with Approval',
description: 'Planning outline that waits for user approval',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.planningLiteWithApproval,
critical: true,
},
{
key: 'planningSpec',
label: 'Planning: Spec Mode',
description: 'Detailed specification with task breakdown',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.planningSpec,
critical: true,
},
{
key: 'planningFull',
label: 'Planning: Full SDD Mode',
description: 'Comprehensive Software Design Document with phased implementation',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.planningFull,
critical: true,
},
],
sections: [
{
title: 'Template Prompts',
banner: {
type: 'info',
title: 'Template Variables',
description:
'Template prompts use Handlebars syntax for variable substitution. Available variables include {{featureId}}, {{title}}, {{description}}, etc.',
},
fields: [
{
key: 'featurePromptTemplate',
label: 'Feature Prompt Template',
description:
'Template for building feature implementation prompts. Variables: featureId, title, description, spec, imagePaths, dependencies, verificationInstructions',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.featurePromptTemplate,
},
{
key: 'followUpPromptTemplate',
label: 'Follow-up Prompt Template',
description:
'Template for follow-up prompts when resuming work. Variables: featurePrompt, previousContext, followUpInstructions',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.followUpPromptTemplate,
},
{
key: 'continuationPromptTemplate',
label: 'Continuation Prompt Template',
description:
'Template for continuation prompts. Variables: featurePrompt, previousContext',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.continuationPromptTemplate,
},
{
key: 'pipelineStepPromptTemplate',
label: 'Pipeline Step Prompt Template',
description:
'Template for pipeline step execution prompts. Variables: stepName, featurePrompt, previousContext, stepInstructions',
defaultValue: DEFAULT_AUTO_MODE_PROMPTS.pipelineStepPromptTemplate,
},
],
},
],
},
{
id: 'agent',
label: 'Agent',
icon: MessageSquareText,
title: 'Agent Runner Prompts',
category: 'agent',
fields: [
{
key: 'systemPrompt',
label: 'System Prompt',
description: "Defines the AI's role and behavior in interactive chat sessions",
defaultValue: DEFAULT_AGENT_PROMPTS.systemPrompt,
},
],
},
{
id: 'backlog-plan',
label: 'Backlog',
icon: KanbanSquare,
title: 'Backlog Planning Prompts',
category: 'backlogPlan',
banner: {
type: 'warning',
title: 'Warning: Critical Prompts',
description:
'Backlog plan prompts require a strict JSON output format. Modifying these prompts incorrectly can break the backlog planning feature and potentially corrupt your feature data. Only customize if you fully understand the expected output structure.',
},
fields: [
{
key: 'systemPrompt',
label: 'System Prompt',
description:
'Defines how the AI modifies the feature backlog (Plan button on Kanban board)',
defaultValue: DEFAULT_BACKLOG_PLAN_PROMPTS.systemPrompt,
critical: true,
},
{
key: 'userPromptTemplate',
label: 'User Prompt Template',
description:
'Template for the user prompt sent to the AI. Variables: currentFeatures, userRequest',
defaultValue: DEFAULT_BACKLOG_PLAN_PROMPTS.userPromptTemplate,
critical: true,
},
],
},
{
id: 'enhancement',
label: 'Enhancement',
icon: Sparkles,
title: 'Enhancement Prompts',
category: 'enhancement',
fields: [
{
key: 'improveSystemPrompt',
label: 'Improve Mode',
description: 'Transform vague requests into clear, actionable tasks',
defaultValue: DEFAULT_ENHANCEMENT_PROMPTS.improveSystemPrompt,
},
{
key: 'technicalSystemPrompt',
label: 'Technical Mode',
description: 'Add implementation details and technical specifications',
defaultValue: DEFAULT_ENHANCEMENT_PROMPTS.technicalSystemPrompt,
},
{
key: 'simplifySystemPrompt',
label: 'Simplify Mode',
description: 'Make verbose descriptions concise and focused',
defaultValue: DEFAULT_ENHANCEMENT_PROMPTS.simplifySystemPrompt,
},
{
key: 'acceptanceSystemPrompt',
label: 'Acceptance Criteria Mode',
description: 'Add testable acceptance criteria to descriptions',
defaultValue: DEFAULT_ENHANCEMENT_PROMPTS.acceptanceSystemPrompt,
},
{
key: 'uxReviewerSystemPrompt',
label: 'User Experience Mode',
description: 'Review and enhance from a user experience and design perspective',
defaultValue: DEFAULT_ENHANCEMENT_PROMPTS.uxReviewerSystemPrompt,
},
],
},
{
id: 'commit-message',
label: 'Commit',
icon: GitCommitHorizontal,
title: 'Commit Message Prompts',
category: 'commitMessage',
fields: [
{
key: 'systemPrompt',
label: 'System Prompt',
description:
'Instructions for generating git commit messages from diffs. The AI will receive the git diff and generate a conventional commit message.',
defaultValue: DEFAULT_COMMIT_MESSAGE_PROMPTS.systemPrompt,
},
],
},
{
id: 'title-generation',
label: 'Title',
icon: Type,
title: 'Title Generation Prompts',
category: 'titleGeneration',
fields: [
{
key: 'systemPrompt',
label: 'System Prompt',
description:
'Instructions for generating concise, descriptive feature titles from descriptions. Used when auto-generating titles for new features.',
defaultValue: DEFAULT_TITLE_GENERATION_PROMPTS.systemPrompt,
},
],
},
{
id: 'issue-validation',
label: 'Issues',
icon: CheckCircle,
title: 'Issue Validation Prompts',
category: 'issueValidation',
banner: {
type: 'warning',
title: 'Warning: Critical Prompt',
description:
'The issue validation prompt guides the AI through a structured validation process and expects specific output format. Modifying this prompt incorrectly may affect validation accuracy.',
},
fields: [
{
key: 'systemPrompt',
label: 'System Prompt',
description:
'Instructions for validating GitHub issues against the codebase. Guides the AI to determine if issues are valid, invalid, or need clarification.',
defaultValue: DEFAULT_ISSUE_VALIDATION_PROMPTS.systemPrompt,
critical: true,
},
],
},
{
id: 'ideation',
label: 'Ideation',
icon: Lightbulb,
title: 'Ideation Prompts',
category: 'ideation',
fields: [
{
key: 'ideationSystemPrompt',
label: 'Ideation Chat System Prompt',
description:
'System prompt for AI-powered ideation chat conversations. Guides the AI to brainstorm and suggest feature ideas.',
defaultValue: DEFAULT_IDEATION_PROMPTS.ideationSystemPrompt,
},
{
key: 'suggestionsSystemPrompt',
label: 'Suggestions System Prompt',
description:
'System prompt for generating structured feature suggestions. Used when generating batch suggestions from prompts.',
defaultValue: DEFAULT_IDEATION_PROMPTS.suggestionsSystemPrompt,
critical: true,
},
],
},
{
id: 'app-spec',
label: 'App Spec',
icon: FileCode,
title: 'App Specification Prompts',
category: 'appSpec',
fields: [
{
key: 'generateSpecSystemPrompt',
label: 'Generate Spec System Prompt',
description: 'System prompt for generating project specifications from overview',
defaultValue: DEFAULT_APP_SPEC_PROMPTS.generateSpecSystemPrompt,
},
{
key: 'structuredSpecInstructions',
label: 'Structured Spec Instructions',
description: 'Instructions for structured specification output format',
defaultValue: DEFAULT_APP_SPEC_PROMPTS.structuredSpecInstructions,
critical: true,
},
{
key: 'generateFeaturesFromSpecPrompt',
label: 'Generate Features from Spec',
description: 'Prompt for generating features from a project specification',
defaultValue: DEFAULT_APP_SPEC_PROMPTS.generateFeaturesFromSpecPrompt,
critical: true,
},
],
},
{
id: 'context-description',
label: 'Context',
icon: FileText,
title: 'Context Description Prompts',
category: 'contextDescription',
fields: [
{
key: 'describeFilePrompt',
label: 'Describe File Prompt',
description: 'Prompt for generating descriptions of text files added as context',
defaultValue: DEFAULT_CONTEXT_DESCRIPTION_PROMPTS.describeFilePrompt,
},
{
key: 'describeImagePrompt',
label: 'Describe Image Prompt',
description: 'Prompt for generating descriptions of images added as context',
defaultValue: DEFAULT_CONTEXT_DESCRIPTION_PROMPTS.describeImagePrompt,
},
],
},
{
id: 'suggestions',
label: 'Suggestions',
icon: Wand2,
title: 'Suggestions Prompts',
category: 'suggestions',
fields: [
{
key: 'featuresPrompt',
label: 'Features Suggestion Prompt',
description: 'Prompt for analyzing the project and suggesting new features',
defaultValue: DEFAULT_SUGGESTIONS_PROMPTS.featuresPrompt,
},
{
key: 'refactoringPrompt',
label: 'Refactoring Suggestion Prompt',
description: 'Prompt for identifying refactoring opportunities',
defaultValue: DEFAULT_SUGGESTIONS_PROMPTS.refactoringPrompt,
},
{
key: 'securityPrompt',
label: 'Security Suggestion Prompt',
description: 'Prompt for analyzing security vulnerabilities',
defaultValue: DEFAULT_SUGGESTIONS_PROMPTS.securityPrompt,
},
{
key: 'performancePrompt',
label: 'Performance Suggestion Prompt',
description: 'Prompt for identifying performance issues',
defaultValue: DEFAULT_SUGGESTIONS_PROMPTS.performancePrompt,
},
{
key: 'baseTemplate',
label: 'Base Template',
description: 'Base template applied to all suggestion types',
defaultValue: DEFAULT_SUGGESTIONS_PROMPTS.baseTemplate,
},
],
},
{
id: 'task-execution',
label: 'Tasks',
icon: Cog,
title: 'Task Execution Prompts',
category: 'taskExecution',
banner: {
type: 'info',
title: 'Template Variables',
description:
'Task execution prompts use Handlebars syntax for variable substitution. Variables include {{taskId}}, {{taskDescription}}, {{completedTasks}}, etc.',
},
fields: [
{
key: 'taskPromptTemplate',
label: 'Task Prompt Template',
description: 'Template for building individual task execution prompts',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.taskPromptTemplate,
},
{
key: 'implementationInstructions',
label: 'Implementation Instructions',
description: 'Instructions appended to feature implementation prompts',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.implementationInstructions,
},
{
key: 'playwrightVerificationInstructions',
label: 'Playwright Verification Instructions',
description: 'Instructions for automated Playwright verification (when enabled)',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.playwrightVerificationInstructions,
},
{
key: 'learningExtractionSystemPrompt',
label: 'Learning Extraction System Prompt',
description: 'System prompt for extracting learnings/ADRs from implementation output',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.learningExtractionSystemPrompt,
critical: true,
},
{
key: 'learningExtractionUserPromptTemplate',
label: 'Learning Extraction User Template',
description:
'User prompt template for learning extraction. Variables: featureTitle, implementationLog',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.learningExtractionUserPromptTemplate,
critical: true,
},
{
key: 'planRevisionTemplate',
label: 'Plan Revision Template',
description:
'Template for prompting plan revisions. Variables: planVersion, previousPlan, userFeedback',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.planRevisionTemplate,
},
{
key: 'continuationAfterApprovalTemplate',
label: 'Continuation After Approval Template',
description:
'Template for continuation after plan approval. Variables: userFeedback, approvedPlan',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.continuationAfterApprovalTemplate,
},
{
key: 'resumeFeatureTemplate',
label: 'Resume Feature Template',
description:
'Template for resuming interrupted features. Variables: featurePrompt, previousContext',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.resumeFeatureTemplate,
},
{
key: 'projectAnalysisPrompt',
label: 'Project Analysis Prompt',
description: 'Prompt for AI-powered project analysis',
defaultValue: DEFAULT_TASK_EXECUTION_PROMPTS.projectAnalysisPrompt,
},
],
},
];

View File

@@ -0,0 +1,51 @@
import type { LucideIcon } from 'lucide-react';
import type { PromptCustomization, CustomPrompt } from '@automaker/types';
/** Props for the PromptField component */
export interface PromptFieldProps {
label: string;
description: string;
defaultValue: string;
customValue?: CustomPrompt;
onCustomValueChange: (value: CustomPrompt | undefined) => void;
critical?: boolean;
}
/** Configuration for a single prompt field */
export interface PromptFieldConfig {
key: string;
label: string;
description: string;
defaultValue: string;
critical?: boolean;
}
/** Banner type for tabs */
export type BannerType = 'info' | 'warning';
/** Configuration for info/warning banners */
export interface BannerConfig {
type: BannerType;
title: string;
description: string;
}
/** Configuration for a section within a tab */
export interface TabSectionConfig {
title?: string;
banner?: BannerConfig;
fields: PromptFieldConfig[];
}
/** Configuration for a tab with prompt fields */
export interface TabConfig {
id: string;
label: string;
icon: LucideIcon;
title: string;
category: keyof PromptCustomization;
banner?: BannerConfig;
fields: PromptFieldConfig[];
/** For tabs with grouped sections (like Auto Mode) */
sections?: TabSectionConfig[];
}