Files
automaker/apps/server/src/routes/app-spec/generate-features-from-spec.ts
Kacper 3154121840 feat: integrate settings service for auto-load CLAUDE.md functionality
- Updated API routes to accept an optional settings service for loading the autoLoadClaudeMd setting.
- Introduced a new settings helper utility for retrieving project-specific settings.
- Enhanced feature generation and spec generation processes to utilize the autoLoadClaudeMd setting.
- Refactored relevant route handlers to support the new settings integration across various endpoints.
2025-12-24 22:34:22 +01:00

174 lines
5.9 KiB
TypeScript

/**
* Generate features from existing app_spec.txt
*/
import { query } from '@anthropic-ai/claude-agent-sdk';
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
import { logAuthStatus } from './common.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';
const logger = createLogger('SpecRegeneration');
const DEFAULT_MAX_FEATURES = 50;
export async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController,
maxFeatures?: number,
settingsService?: SettingsService
): Promise<void> {
const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES;
logger.debug('========== generateFeaturesFromSpec() started ==========');
logger.debug('projectPath:', projectPath);
logger.debug('maxFeatures:', featureCount);
// Read existing spec from .automaker directory
const specPath = getAppSpecPath(projectPath);
let spec: string;
logger.debug('Reading spec from:', specPath);
try {
spec = (await secureFs.readFile(specPath, 'utf-8')) as string;
logger.info(`Spec loaded successfully (${spec.length} chars)`);
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
logger.info(`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`);
} catch (readError) {
logger.error('❌ Failed to read spec file:', readError);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_error',
error: 'No project spec found. Generate spec first.',
projectPath: projectPath,
});
return;
}
const prompt = `Based on this project specification:
${spec}
Generate a prioritized list of implementable features. For each feature provide:
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.`;
logger.info('========== PROMPT BEING SENT ==========');
logger.info(`Prompt length: ${prompt.length} chars`);
logger.info(`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`);
logger.info('========== END PROMPT PREVIEW ==========');
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: 'Analyzing spec and generating features...\n',
projectPath: projectPath,
});
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[FeatureGeneration]'
);
const options = createFeatureGenerationOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
logger.info('Calling Claude Agent SDK query() for features...');
logAuthStatus('Right before SDK query() for features');
let stream;
try {
stream = query({ prompt, options });
logger.debug('query() returned stream successfully');
} catch (queryError) {
logger.error('❌ query() threw an exception:');
logger.error('Error:', queryError);
throw queryError;
}
let responseText = '';
let messageCount = 0;
logger.debug('Starting to iterate over feature stream...');
try {
for await (const msg of stream) {
messageCount++;
logger.debug(
`Feature stream message #${messageCount}:`,
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
);
if (msg.type === 'assistant' && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
responseText += block.text;
logger.debug(`Feature text block received (${block.text.length} chars)`);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: block.text,
projectPath: projectPath,
});
}
}
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
logger.debug('Received success result for features');
responseText = (msg as any).result || responseText;
} else if ((msg as { type: string }).type === 'error') {
logger.error('❌ Received error message from feature stream:');
logger.error('Error message:', JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
logger.error('❌ Error while iterating feature stream:');
logger.error('Stream error:', streamError);
throw streamError;
}
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
logger.info(`Feature response length: ${responseText.length} chars`);
logger.info('========== FULL RESPONSE TEXT ==========');
logger.info(responseText);
logger.info('========== END RESPONSE TEXT ==========');
await parseAndCreateFeatures(projectPath, responseText, events);
logger.debug('========== generateFeaturesFromSpec() completed ==========');
}