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

@@ -4,8 +4,9 @@
*/
import path from 'path';
import type { Feature } from '@automaker/types';
import type { Feature, PlanSpec, FeatureStatus } from '@automaker/types';
import { createLogger } from '@automaker/utils';
import { resolveDependencies, areDependenciesSatisfied } from '@automaker/dependency-resolver';
import * as secureFs from '../lib/secure-fs.js';
import {
getFeaturesDir,
@@ -381,4 +382,115 @@ export class FeatureLoader {
}
}
}
/**
* Check if agent output exists for a feature
*/
async hasAgentOutput(projectPath: string, featureId: string): Promise<boolean> {
try {
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
await secureFs.access(agentOutputPath);
return true;
} catch {
return false;
}
}
/**
* Update feature status with proper timestamp handling
* Used by auto-mode to update feature status during execution
*/
async updateStatus(
projectPath: string,
featureId: string,
status: FeatureStatus
): Promise<Feature | null> {
try {
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
const content = (await secureFs.readFile(featureJsonPath, 'utf-8')) as string;
const feature = JSON.parse(content) as Feature;
feature.status = status;
feature.updatedAt = new Date().toISOString();
// Handle justFinishedAt for waiting_approval status
if (status === 'waiting_approval') {
feature.justFinishedAt = new Date().toISOString();
} else {
feature.justFinishedAt = undefined;
}
await secureFs.writeFile(featureJsonPath, JSON.stringify(feature, null, 2));
return feature;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error(`[FeatureLoader] Failed to update status for ${featureId}:`, error);
return null;
}
}
/**
* Update feature plan specification
* Handles version incrementing and timestamp management
*/
async updatePlanSpec(
projectPath: string,
featureId: string,
updates: Partial<PlanSpec>
): Promise<Feature | null> {
try {
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
const content = (await secureFs.readFile(featureJsonPath, 'utf-8')) as string;
const feature = JSON.parse(content) as Feature;
// Initialize planSpec if not present
if (!feature.planSpec) {
feature.planSpec = { status: 'pending', version: 1, reviewedByUser: false };
}
// Increment version if content changed
if (updates.content && updates.content !== feature.planSpec.content) {
feature.planSpec.version = (feature.planSpec.version || 0) + 1;
}
// Merge updates
Object.assign(feature.planSpec, updates);
feature.updatedAt = new Date().toISOString();
await secureFs.writeFile(featureJsonPath, JSON.stringify(feature, null, 2));
return feature;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
logger.error(`[FeatureLoader] Failed to update planSpec for ${featureId}:`, error);
return null;
}
}
/**
* Get features that are pending and ready to execute
* Filters by status and resolves dependencies
*/
async getPending(projectPath: string): Promise<Feature[]> {
try {
const allFeatures = await this.getAll(projectPath);
const pendingFeatures = allFeatures.filter((f) =>
['pending', 'ready', 'backlog'].includes(f.status)
);
// Resolve dependencies and order features
const { orderedFeatures } = resolveDependencies(pendingFeatures);
// Filter to features whose dependencies are satisfied
return orderedFeatures.filter((feature: Feature) =>
areDependenciesSatisfied(feature, allFeatures)
);
} catch (error) {
logger.error('[FeatureLoader] Failed to get pending features:', error);
return [];
}
}
}