refactor: extract auto-mode-service into modular services

Reduce auto-mode-service.ts from 1308 to 516 lines (60% reduction) by
extracting reusable functionality into shared packages and services:

- Add feature prompt builders to @automaker/prompts (buildFeaturePrompt,
  buildFollowUpPrompt, buildContinuationPrompt, extractTitleFromDescription)
- Add planning prompts and task parsing to @automaker/prompts
- Add stream processor utilities to @automaker/utils (sleep, processStream)
- Add git commit utilities to @automaker/git-utils (commitAll, hasUncommittedChanges)
- Create ProjectAnalyzer service for project analysis
- Create FeatureVerificationService for verify/commit operations
- Extend FeatureLoader with updateStatus, updatePlanSpec, getPending methods
- Expand FeatureStatus type to include all used statuses
- Add PlanSpec and ParsedTask types to @automaker/types

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-22 23:25:22 +01:00
parent c4a2f2c2a8
commit 79ef8c8510
25 changed files with 3048 additions and 2306 deletions

View File

@@ -54,3 +54,15 @@ export {
type ContextFilesResult,
type LoadContextFilesOptions,
} from './context-loader.js';
// Stream processing
export {
processStream,
collectStreamText,
processStreamWithProgress,
hasMarker,
extractBeforeMarker,
sleep,
type StreamHandlers,
type StreamResult,
} from './stream-processor.js';

View File

@@ -0,0 +1,173 @@
/**
* Stream Processor - Unified stream handling for provider messages
*
* Eliminates duplication of the stream processing pattern for handling
* async generators from AI providers.
*/
import type { ProviderMessage, ContentBlock } from '@automaker/types';
/**
* Callbacks for handling different stream events
*/
export interface StreamHandlers {
/** Called for each text block in the stream */
onText?: (text: string) => void | Promise<void>;
/** Called for each tool use in the stream */
onToolUse?: (name: string, input: unknown) => void | Promise<void>;
/** Called when an error occurs in the stream */
onError?: (error: string) => void | Promise<void>;
/** Called when the stream completes successfully */
onComplete?: (result: string) => void | Promise<void>;
/** Called for thinking blocks (if present) */
onThinking?: (thinking: string) => void | Promise<void>;
}
/**
* Result from processing a stream
*/
export interface StreamResult {
/** All accumulated text from the stream */
text: string;
/** Whether the stream completed successfully */
success: boolean;
/** Error message if stream failed */
error?: string;
/** Final result message if stream completed */
result?: string;
}
/**
* Process a provider message stream with unified handling
*
* @param stream - The async generator from provider.executeQuery()
* @param handlers - Callbacks for different event types
* @returns Accumulated result with text and status
*/
export async function processStream(
stream: AsyncGenerator<ProviderMessage>,
handlers: StreamHandlers
): Promise<StreamResult> {
let accumulatedText = '';
let success = true;
let errorMessage: string | undefined;
let resultMessage: string | undefined;
try {
for await (const msg of stream) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
await processContentBlock(block, handlers, (text) => {
accumulatedText += text;
});
}
} else if (msg.type === 'error') {
success = false;
errorMessage = msg.error || 'Unknown error';
if (handlers.onError) {
await handlers.onError(errorMessage);
}
throw new Error(errorMessage);
} else if (msg.type === 'result' && msg.subtype === 'success') {
resultMessage = msg.result || '';
if (handlers.onComplete) {
await handlers.onComplete(resultMessage);
}
}
}
} catch (error) {
if (!errorMessage) {
success = false;
errorMessage = error instanceof Error ? error.message : String(error);
}
throw error;
}
return {
text: accumulatedText,
success,
error: errorMessage,
result: resultMessage,
};
}
/**
* Process a single content block
*/
async function processContentBlock(
block: ContentBlock,
handlers: StreamHandlers,
appendText: (text: string) => void
): Promise<void> {
switch (block.type) {
case 'text':
if (block.text) {
appendText(block.text);
if (handlers.onText) {
await handlers.onText(block.text);
}
}
break;
case 'tool_use':
if (block.name && handlers.onToolUse) {
await handlers.onToolUse(block.name, block.input);
}
break;
case 'thinking':
if (block.thinking && handlers.onThinking) {
await handlers.onThinking(block.thinking);
}
break;
// tool_result blocks are handled internally by the SDK
case 'tool_result':
break;
}
}
/**
* Create a simple stream processor that just collects text
*/
export async function collectStreamText(stream: AsyncGenerator<ProviderMessage>): Promise<string> {
const result = await processStream(stream, {});
return result.text;
}
/**
* Process stream with progress callback
*/
export async function processStreamWithProgress(
stream: AsyncGenerator<ProviderMessage>,
onProgress: (text: string) => void
): Promise<StreamResult> {
return processStream(stream, {
onText: onProgress,
});
}
/**
* Check if a stream result contains a specific marker
*/
export function hasMarker(result: StreamResult, marker: string): boolean {
return result.text.includes(marker);
}
/**
* Extract content before a marker
*/
export function extractBeforeMarker(text: string, marker: string): string | null {
const index = text.indexOf(marker);
if (index === -1) {
return null;
}
return text.substring(0, index).trim();
}
/**
* Sleep utility - delay execution for specified milliseconds
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}