feat: add customizable AI prompts with enhanced UX

Add comprehensive prompt customization system allowing users to customize
all AI prompts (Auto Mode, Agent Runner, Backlog Plan, Enhancement) through
the Settings UI.

## Features

### Core Customization System
- New TypeScript types for prompt customization with enabled flag
- CustomPrompt interface with value and enabled state
- Prompts preserved even when disabled (no data loss)
- Merged prompt system (custom overrides defaults when enabled)
- Persistent storage in ~/.automaker/settings.json

### Settings UI
- New "Prompt Customization" section in Settings
- 4 tabs: Auto Mode, Agent, Backlog Plan, Enhancement
- Toggle-based editing (read-only default → editable custom)
- Dynamic textarea height based on prompt length (120px-600px)
- Visual state indicators (Custom/Default labels)

### Warning System
- Critical prompt warnings for Backlog Plan (JSON format requirement)
- Field-level warnings when editing critical prompts
- Info banners for Auto Mode planning markers
- Color-coded warnings (blue=info, amber=critical)

### Backend Integration
- Auto Mode service loads prompts from settings
- Agent service loads prompts from settings
- Backlog Plan service loads prompts from settings
- Enhancement endpoint loads prompts from settings
- Settings sync includes promptCustomization field

### Files Changed
- libs/types/src/prompts.ts - Type definitions
- libs/prompts/src/defaults.ts - Default prompt values
- libs/prompts/src/merge.ts - Merge utilities
- apps/ui/src/components/views/settings-view/prompts/ - UI components
- apps/server/src/lib/settings-helpers.ts - getPromptCustomization()
- All service files updated to use customizable prompts

## Technical Details

Prompt storage format:
```json
{
  "promptCustomization": {
    "autoMode": {
      "planningLite": {
        "value": "Custom prompt text...",
        "enabled": true
      }
    }
  }
}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stephan Rieche
2025-12-29 23:17:20 +01:00
parent 25c9259b50
commit bc0ef47323
20 changed files with 1338 additions and 265 deletions

View File

@@ -155,7 +155,7 @@ app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes());
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes());

View File

@@ -4,7 +4,13 @@
import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';
import type { MCPServerConfig, McpServerConfig } from '@automaker/types';
import type { MCPServerConfig, McpServerConfig, PromptCustomization } from '@automaker/types';
import {
mergeAutoModePrompts,
mergeAgentPrompts,
mergeBacklogPlanPrompts,
mergeEnhancementPrompts,
} from '@automaker/prompts';
/**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
@@ -255,3 +261,43 @@ function convertToSdkFormat(server: MCPServerConfig): McpServerConfig {
env: server.env,
};
}
/**
* Get prompt customization from global settings and merge with defaults.
* Returns prompts merged with built-in defaults - custom prompts override defaults.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to merged prompts for all categories
*/
export async function getPromptCustomization(
settingsService?: SettingsService | null,
logPrefix = '[PromptHelper]'
): Promise<{
autoMode: ReturnType<typeof mergeAutoModePrompts>;
agent: ReturnType<typeof mergeAgentPrompts>;
backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
enhancement: ReturnType<typeof mergeEnhancementPrompts>;
}> {
let customization: PromptCustomization = {};
if (settingsService) {
try {
const globalSettings = await settingsService.getGlobalSettings();
customization = globalSettings.promptCustomization || {};
console.log(`${logPrefix} Loaded prompt customization from settings`);
} catch (error) {
console.error(`${logPrefix} Failed to load prompt customization:`, error);
// Fall through to use empty customization (all defaults)
}
} else {
console.log(`${logPrefix} SettingsService not available, using default prompts`);
}
return {
autoMode: mergeAutoModePrompts(customization.autoMode),
agent: mergeAgentPrompts(customization.agent),
backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan),
enhancement: mergeEnhancementPrompts(customization.enhancement),
};
}

View File

@@ -8,7 +8,7 @@ import { FeatureLoader } from '../../services/feature-loader.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logger, setRunningState, getErrorMessage } from './common.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 featureLoader = new FeatureLoader();
@@ -79,72 +79,17 @@ export async function generateBacklogPlan(
content: `Loaded ${features.length} features from backlog`,
});
// Load prompts from settings
const prompts = await getPromptCustomization(settingsService, '[BacklogPlan]');
// Build the system prompt
const systemPrompt = `You are an AI assistant helping to modify a software project's feature backlog.
You will be given the current list of features and a user request to modify the backlog.
const systemPrompt = prompts.backlogPlan.systemPrompt;
IMPORTANT CONTEXT (automatically injected):
- Remember to update the dependency graph if deleting existing features
- Remember to define dependencies on new features hooked into relevant existing ones
- Maintain dependency graph integrity (no orphaned dependencies)
- When deleting a feature, identify which other features depend on it
Your task is to analyze the request and produce a structured JSON plan with:
1. Features to ADD (include title, description, category, and dependencies)
2. Features to UPDATE (specify featureId and the updates)
3. Features to DELETE (specify featureId)
4. A summary of the changes
5. Any dependency updates needed (removed dependencies due to deletions, new dependencies for new features)
Respond with ONLY a JSON object in this exact format:
\`\`\`json
{
"changes": [
{
"type": "add",
"feature": {
"title": "Feature title",
"description": "Feature description",
"category": "Category name",
"dependencies": ["existing-feature-id"],
"priority": 1
},
"reason": "Why this feature should be added"
},
{
"type": "update",
"featureId": "existing-feature-id",
"feature": {
"title": "Updated title"
},
"reason": "Why this feature should be updated"
},
{
"type": "delete",
"featureId": "feature-id-to-delete",
"reason": "Why this feature should be deleted"
}
],
"summary": "Brief overview of all proposed changes",
"dependencyUpdates": [
{
"featureId": "feature-that-depended-on-deleted",
"removedDependencies": ["deleted-feature-id"],
"addedDependencies": []
}
]
}
\`\`\``;
// Build the user prompt
const userPrompt = `Current Features in Backlog:
${formatFeaturesForPrompt(features)}
---
User Request: ${prompt}
Please analyze the current backlog and the user's request, then provide a JSON plan for the modifications.`;
// Build the user prompt from template
const currentFeatures = formatFeaturesForPrompt(features);
const userPrompt = prompts.backlogPlan.userPromptTemplate
.replace('{{currentFeatures}}', currentFeatures)
.replace('{{userRequest}}', prompt);
events.emit('backlog-plan:event', {
type: 'backlog_plan_progress',

View File

@@ -6,17 +6,19 @@
*/
import { Router } from 'express';
import type { SettingsService } from '../../services/settings-service.js';
import { createEnhanceHandler } from './routes/enhance.js';
/**
* Create the enhance-prompt router
*
* @param settingsService - Settings service for loading custom prompts
* @returns Express router with enhance-prompt endpoints
*/
export function createEnhancePromptRoutes(): Router {
export function createEnhancePromptRoutes(settingsService?: SettingsService): Router {
const router = Router();
router.post('/', createEnhanceHandler());
router.post('/', createEnhanceHandler(settingsService));
return router;
}

View File

@@ -10,8 +10,9 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { resolveModelString } from '@automaker/model-resolver';
import { CLAUDE_MODEL_MAP } from '@automaker/types';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
import {
getSystemPrompt,
buildUserPrompt,
isValidEnhancementMode,
type EnhancementMode,
@@ -83,9 +84,12 @@ async function extractTextFromStream(
/**
* Create the enhance request handler
*
* @param settingsService - Optional settings service for loading custom prompts
* @returns Express request handler for text enhancement
*/
export function createEnhanceHandler(): (req: Request, res: Response) => Promise<void> {
export function createEnhanceHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
@@ -128,8 +132,20 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
// Get the system prompt for this mode
const systemPrompt = getSystemPrompt(validMode);
// Load enhancement prompts from settings (merges custom + defaults)
const prompts = await getPromptCustomization(settingsService, '[EnhancePrompt]');
// Get the system prompt for this mode from merged prompts
const systemPrompt =
validMode === 'improve'
? prompts.enhancement.improveSystemPrompt
: validMode === 'technical'
? prompts.enhancement.technicalSystemPrompt
: validMode === 'simplify'
? prompts.enhancement.simplifySystemPrompt
: prompts.enhancement.acceptanceSystemPrompt;
logger.debug(`Using ${validMode} system prompt (length: ${systemPrompt.length} chars)`);
// Build the user prompt with few-shot examples
// This helps the model understand this is text transformation, not a coding task

View File

@@ -23,6 +23,7 @@ import {
filterClaudeMdFromContext,
getMCPServersFromSettings,
getMCPPermissionSettings,
getPromptCustomization,
} from '../lib/settings-helpers.js';
interface Message {
@@ -75,6 +76,7 @@ export class AgentService {
private metadataFile: string;
private events: EventEmitter;
private settingsService: SettingsService | null = null;
private agentSystemPrompt: string | null = null;
constructor(dataDir: string, events: EventEmitter, settingsService?: SettingsService) {
this.stateDir = path.join(dataDir, 'agent-sessions');
@@ -246,7 +248,7 @@ export class AgentService {
const contextFilesPrompt = filterClaudeMdFromContext(contextResult, autoLoadClaudeMd);
// Build combined system prompt with base prompt and context files
const baseSystemPrompt = this.getSystemPrompt();
const baseSystemPrompt = await this.getSystemPrompt();
const combinedSystemPrompt = contextFilesPrompt
? `${contextFilesPrompt}\n\n${baseSystemPrompt}`
: baseSystemPrompt;
@@ -781,38 +783,13 @@ export class AgentService {
this.events.emit('agent:stream', { sessionId, ...data });
}
private getSystemPrompt(): string {
return `You are an AI assistant helping users build software. You are part of the Automaker application,
which is designed to help developers plan, design, and implement software projects autonomously.
**Feature Storage:**
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
Use the UpdateFeatureStatus tool to manage features, not direct file edits.
Your role is to:
- Help users define their project requirements and specifications
- Ask clarifying questions to better understand their needs
- Suggest technical approaches and architectures
- Guide them through the development process
- Be conversational and helpful
- Write, edit, and modify code files as requested
- Execute commands and tests
- Search and analyze the codebase
When discussing projects, help users think through:
- Core functionality and features
- Technical stack choices
- Data models and architecture
- User experience considerations
- Testing strategies
You have full access to the codebase and can:
- Read files to understand existing code
- Write new files
- Edit existing files
- Run bash commands
- Search for code patterns
- Execute tests and builds`;
private async getSystemPrompt(): Promise<string> {
// Load from settings if not already cached
if (!this.agentSystemPrompt) {
const prompts = await getPromptCustomization(this.settingsService, '[AgentService]');
this.agentSystemPrompt = prompts.agent.systemPrompt;
}
return this.agentSystemPrompt;
}
private generateId(): string {

View File

@@ -39,6 +39,7 @@ import {
filterClaudeMdFromContext,
getMCPServersFromSettings,
getMCPPermissionSettings,
getPromptCustomization,
} from '../lib/settings-helpers.js';
const execAsync = promisify(exec);
@@ -67,162 +68,6 @@ interface PlanSpec {
tasks?: ParsedTask[];
}
const PLANNING_PROMPTS = {
lite: `## Planning Phase (Lite Mode)
IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the plan. Start DIRECTLY with the planning outline format below. Silently analyze the codebase first, then output ONLY the structured plan.
Create a brief planning outline:
1. **Goal**: What are we accomplishing? (1 sentence)
2. **Approach**: How will we do it? (2-3 sentences)
3. **Files to Touch**: List files and what changes
4. **Tasks**: Numbered task list (3-7 items)
5. **Risks**: Any gotchas to watch for
After generating the outline, output:
"[PLAN_GENERATED] Planning outline complete."
Then proceed with implementation.`,
lite_with_approval: `## Planning Phase (Lite Mode)
IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the plan. Start DIRECTLY with the planning outline format below. Silently analyze the codebase first, then output ONLY the structured plan.
Create a brief planning outline:
1. **Goal**: What are we accomplishing? (1 sentence)
2. **Approach**: How will we do it? (2-3 sentences)
3. **Files to Touch**: List files and what changes
4. **Tasks**: Numbered task list (3-7 items)
5. **Risks**: Any gotchas to watch for
After generating the outline, output:
"[SPEC_GENERATED] Please review the planning outline above. Reply with 'approved' to proceed or provide feedback for revisions."
DO NOT proceed with implementation until you receive explicit approval.`,
spec: `## Specification Phase (Spec Mode)
IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the spec. Start DIRECTLY with the specification format below. Silently analyze the codebase first, then output ONLY the structured specification.
Generate a specification with an actionable task breakdown. WAIT for approval before implementing.
### Specification Format
1. **Problem**: What problem are we solving? (user perspective)
2. **Solution**: Brief approach (1-2 sentences)
3. **Acceptance Criteria**: 3-5 items in GIVEN-WHEN-THEN format
- GIVEN [context], WHEN [action], THEN [outcome]
4. **Files to Modify**:
| File | Purpose | Action |
|------|---------|--------|
| path/to/file | description | create/modify/delete |
5. **Implementation Tasks**:
Use this EXACT format for each task (the system will parse these):
\`\`\`tasks
- [ ] T001: [Description] | File: [path/to/file]
- [ ] T002: [Description] | File: [path/to/file]
- [ ] T003: [Description] | File: [path/to/file]
\`\`\`
Task ID rules:
- Sequential: T001, T002, T003, etc.
- Description: Clear action (e.g., "Create user model", "Add API endpoint")
- File: Primary file affected (helps with context)
- Order by dependencies (foundational tasks first)
6. **Verification**: How to confirm feature works
After generating the spec, output on its own line:
"[SPEC_GENERATED] Please review the specification above. Reply with 'approved' to proceed or provide feedback for revisions."
DO NOT proceed with implementation until you receive explicit approval.
When approved, execute tasks SEQUENTIALLY in order. For each task:
1. BEFORE starting, output: "[TASK_START] T###: Description"
2. Implement the task
3. AFTER completing, output: "[TASK_COMPLETE] T###: Brief summary"
This allows real-time progress tracking during implementation.`,
full: `## Full Specification Phase (Full SDD Mode)
IMPORTANT: Do NOT output exploration text, tool usage, or thinking before the spec. Start DIRECTLY with the specification format below. Silently analyze the codebase first, then output ONLY the structured specification.
Generate a comprehensive specification with phased task breakdown. WAIT for approval before implementing.
### Specification Format
1. **Problem Statement**: 2-3 sentences from user perspective
2. **User Story**: As a [user], I want [goal], so that [benefit]
3. **Acceptance Criteria**: Multiple scenarios with GIVEN-WHEN-THEN
- **Happy Path**: GIVEN [context], WHEN [action], THEN [expected outcome]
- **Edge Cases**: GIVEN [edge condition], WHEN [action], THEN [handling]
- **Error Handling**: GIVEN [error condition], WHEN [action], THEN [error response]
4. **Technical Context**:
| Aspect | Value |
|--------|-------|
| Affected Files | list of files |
| Dependencies | external libs if any |
| Constraints | technical limitations |
| Patterns to Follow | existing patterns in codebase |
5. **Non-Goals**: What this feature explicitly does NOT include
6. **Implementation Tasks**:
Use this EXACT format for each task (the system will parse these):
\`\`\`tasks
## Phase 1: Foundation
- [ ] T001: [Description] | File: [path/to/file]
- [ ] T002: [Description] | File: [path/to/file]
## Phase 2: Core Implementation
- [ ] T003: [Description] | File: [path/to/file]
- [ ] T004: [Description] | File: [path/to/file]
## Phase 3: Integration & Testing
- [ ] T005: [Description] | File: [path/to/file]
- [ ] T006: [Description] | File: [path/to/file]
\`\`\`
Task ID rules:
- Sequential across all phases: T001, T002, T003, etc.
- Description: Clear action verb + target
- File: Primary file affected
- Order by dependencies within each phase
- Phase structure helps organize complex work
7. **Success Metrics**: How we know it's done (measurable criteria)
8. **Risks & Mitigations**:
| Risk | Mitigation |
|------|------------|
| description | approach |
After generating the spec, output on its own line:
"[SPEC_GENERATED] Please review the comprehensive specification above. Reply with 'approved' to proceed or provide feedback for revisions."
DO NOT proceed with implementation until you receive explicit approval.
When approved, execute tasks SEQUENTIALLY by phase. For each task:
1. BEFORE starting, output: "[TASK_START] T###: Description"
2. Implement the task
3. AFTER completing, output: "[TASK_COMPLETE] T###: Brief summary"
After completing all tasks in a phase, output:
"[PHASE_COMPLETE] Phase N complete"
This allows real-time progress tracking during implementation.`,
};
/**
* Parse tasks from generated spec content
* Looks for the ```tasks code block and extracts task lines
@@ -355,6 +200,7 @@ export class AutoModeService {
private config: AutoModeConfig | null = null;
private pendingApprovals = new Map<string, PendingApproval>();
private settingsService: SettingsService | null = null;
private planningPrompts: Record<string, string> | null = null;
constructor(events: EventEmitter, settingsService?: SettingsService) {
this.events = events;
@@ -593,7 +439,7 @@ export class AutoModeService {
} else {
// Normal flow: build prompt with planning phase
const featurePrompt = this.buildFeaturePrompt(feature);
const planningPrefix = this.getPlanningPromptPrefix(feature);
const planningPrefix = await this.getPlanningPromptPrefix(feature);
prompt = planningPrefix + featurePrompt;
// Emit planning mode info
@@ -1756,23 +1602,43 @@ Format your response as a structured markdown document.`;
return firstLine.substring(0, 57) + '...';
}
/**
* Load planning prompts from settings
*/
private async loadPlanningPrompts(): Promise<void> {
if (this.planningPrompts) {
return; // Already loaded
}
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
this.planningPrompts = {
lite: prompts.autoMode.planningLite,
lite_with_approval: prompts.autoMode.planningLiteWithApproval,
spec: prompts.autoMode.planningSpec,
full: prompts.autoMode.planningFull,
};
}
/**
* Get the planning prompt prefix based on feature's planning mode
*/
private getPlanningPromptPrefix(feature: Feature): string {
private async getPlanningPromptPrefix(feature: Feature): Promise<string> {
const mode = feature.planningMode || 'skip';
if (mode === 'skip') {
return ''; // No planning phase
}
// Load prompts if not already loaded
await this.loadPlanningPrompts();
// For lite mode, use the approval variant if requirePlanApproval is true
let promptKey: string = mode;
if (mode === 'lite' && feature.requirePlanApproval === true) {
promptKey = 'lite_with_approval';
}
const planningPrompt = PLANNING_PROMPTS[promptKey as keyof typeof PLANNING_PROMPTS];
const planningPrompt = this.planningPrompts![promptKey];
if (!planningPrompt) {
return '';
}