mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
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:
112
libs/git-utils/src/commit.ts
Normal file
112
libs/git-utils/src/commit.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Git Commit Utilities - Commit operations for git repositories
|
||||
*
|
||||
* Provides utilities for staging and committing changes.
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Check if there are uncommitted changes in the working directory
|
||||
*
|
||||
* @param workDir - The working directory to check
|
||||
* @returns True if there are uncommitted changes
|
||||
*/
|
||||
export async function hasUncommittedChanges(workDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: workDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stage all changes and commit with a message
|
||||
*
|
||||
* @param workDir - The working directory
|
||||
* @param message - The commit message
|
||||
* @returns The commit hash if successful, null otherwise
|
||||
*/
|
||||
export async function commitAll(workDir: string, message: string): Promise<string | null> {
|
||||
try {
|
||||
// Check for changes first
|
||||
const hasChanges = await hasUncommittedChanges(workDir);
|
||||
if (!hasChanges) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Stage all changes
|
||||
await execAsync('git add -A', { cwd: workDir });
|
||||
|
||||
// Commit with message (escape double quotes)
|
||||
const escapedMessage = message.replace(/"/g, '\\"');
|
||||
await execAsync(`git commit -m "${escapedMessage}"`, { cwd: workDir });
|
||||
|
||||
// Get the commit hash
|
||||
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: workDir });
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current HEAD commit hash
|
||||
*
|
||||
* @param workDir - The working directory
|
||||
* @returns The commit hash or null if not a git repo
|
||||
*/
|
||||
export async function getHeadHash(workDir: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: workDir });
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short version of a commit hash
|
||||
*
|
||||
* @param hash - The full commit hash
|
||||
* @param length - Length of short hash (default 8)
|
||||
* @returns The shortened hash
|
||||
*/
|
||||
export function shortHash(hash: string, length = 8): string {
|
||||
return hash.substring(0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run verification commands (lint, typecheck, test, build)
|
||||
*
|
||||
* @param workDir - The working directory
|
||||
* @param checks - Optional custom checks (defaults to npm scripts)
|
||||
* @returns Object with success status and failed check name if any
|
||||
*/
|
||||
export async function runVerificationChecks(
|
||||
workDir: string,
|
||||
checks?: Array<{ cmd: string; name: string }>
|
||||
): Promise<{ success: boolean; failedCheck?: string }> {
|
||||
const defaultChecks = [
|
||||
{ cmd: 'npm run lint', name: 'Lint' },
|
||||
{ cmd: 'npm run typecheck', name: 'Type check' },
|
||||
{ cmd: 'npm test', name: 'Tests' },
|
||||
{ cmd: 'npm run build', name: 'Build' },
|
||||
];
|
||||
|
||||
const checksToRun = checks || defaultChecks;
|
||||
|
||||
for (const check of checksToRun) {
|
||||
try {
|
||||
await execAsync(check.cmd, { cwd: workDir, timeout: 120000 });
|
||||
} catch {
|
||||
return { success: false, failedCheck: check.name };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
@@ -17,3 +17,12 @@ export {
|
||||
generateDiffsForNonGitDirectory,
|
||||
getGitRepositoryDiffs,
|
||||
} from './diff.js';
|
||||
|
||||
// Export commit utilities
|
||||
export {
|
||||
hasUncommittedChanges,
|
||||
commitAll,
|
||||
getHeadHash,
|
||||
shortHash,
|
||||
runVerificationChecks,
|
||||
} from './commit.js';
|
||||
|
||||
138
libs/prompts/src/feature-prompt.ts
Normal file
138
libs/prompts/src/feature-prompt.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Feature Prompt - Prompt building for feature implementation
|
||||
*
|
||||
* Contains utilities for building prompts from Feature objects.
|
||||
*/
|
||||
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Extract a title from feature description
|
||||
*
|
||||
* Takes the first line of the description and truncates if needed.
|
||||
*
|
||||
* @param description - The feature description
|
||||
* @returns A title string (max 60 chars)
|
||||
*/
|
||||
export function extractTitleFromDescription(description: string): string {
|
||||
if (!description?.trim()) {
|
||||
return 'Untitled Feature';
|
||||
}
|
||||
|
||||
const firstLine = description.split('\n')[0].trim();
|
||||
return firstLine.length <= 60 ? firstLine : firstLine.substring(0, 57) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a feature implementation prompt
|
||||
*
|
||||
* Creates a structured prompt for the AI agent to implement a feature.
|
||||
*
|
||||
* @param feature - The feature to build a prompt for
|
||||
* @returns The formatted prompt string
|
||||
*/
|
||||
export function buildFeaturePrompt(feature: Feature): string {
|
||||
const title = extractTitleFromDescription(feature.description);
|
||||
|
||||
let prompt = `## Feature Implementation Task
|
||||
|
||||
**Feature ID:** ${feature.id}
|
||||
**Title:** ${title}
|
||||
**Description:** ${feature.description}
|
||||
`;
|
||||
|
||||
if (feature.spec) {
|
||||
prompt += `\n**Specification:**\n${feature.spec}\n`;
|
||||
}
|
||||
|
||||
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
||||
const imagesList = feature.imagePaths
|
||||
.map((img, idx) => {
|
||||
const imgPath = typeof img === 'string' ? img : img.path;
|
||||
const filename =
|
||||
typeof img === 'string'
|
||||
? imgPath.split('/').pop()
|
||||
: (img as { filename?: string }).filename || imgPath.split('/').pop();
|
||||
return ` ${idx + 1}. ${filename}\n Path: ${imgPath}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
prompt += `\n**Context Images Attached:**\n${imagesList}\n`;
|
||||
}
|
||||
|
||||
if (feature.skipTests) {
|
||||
prompt += `
|
||||
## Instructions
|
||||
|
||||
Implement this feature by:
|
||||
1. 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
|
||||
|
||||
When done, wrap your final summary in <summary> tags.`;
|
||||
} else {
|
||||
prompt += `
|
||||
## Instructions
|
||||
|
||||
Implement and verify this feature:
|
||||
1. Explore the codebase
|
||||
2. Plan your approach
|
||||
3. Write the code changes
|
||||
4. Verify with Playwright tests
|
||||
|
||||
When done, wrap your final summary in <summary> tags.`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a follow-up prompt for continuing work on a feature
|
||||
*
|
||||
* @param feature - The feature being followed up on
|
||||
* @param previousContext - Previous agent work context
|
||||
* @param followUpInstructions - New instructions from user
|
||||
* @returns The formatted follow-up prompt
|
||||
*/
|
||||
export function buildFollowUpPrompt(
|
||||
feature: Feature | null,
|
||||
featureId: string,
|
||||
previousContext: string,
|
||||
followUpInstructions: string
|
||||
): string {
|
||||
let prompt = `## Follow-up on Feature Implementation\n\n`;
|
||||
|
||||
if (feature) {
|
||||
prompt += buildFeaturePrompt(feature) + '\n';
|
||||
} else {
|
||||
prompt += `**Feature ID:** ${featureId}\n`;
|
||||
}
|
||||
|
||||
if (previousContext) {
|
||||
prompt += `\n## Previous Agent Work\n${previousContext}\n`;
|
||||
}
|
||||
|
||||
prompt += `\n## Follow-up Instructions\n${followUpInstructions}\n\n## Task\nAddress the follow-up instructions above.`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a continuation prompt for resuming work
|
||||
*
|
||||
* @param feature - The feature to continue
|
||||
* @param context - Previous work context
|
||||
* @returns The continuation prompt
|
||||
*/
|
||||
export function buildContinuationPrompt(feature: Feature, context: string): string {
|
||||
return `## Continuing Feature Implementation
|
||||
|
||||
${buildFeaturePrompt(feature)}
|
||||
|
||||
## Previous Context
|
||||
${context}
|
||||
|
||||
## Instructions
|
||||
Review the previous work and continue the implementation.`;
|
||||
}
|
||||
@@ -21,5 +21,31 @@ export {
|
||||
getAvailableEnhancementModes,
|
||||
} from './enhancement.js';
|
||||
|
||||
// Planning prompts (spec-driven development)
|
||||
export {
|
||||
PLANNING_PROMPTS,
|
||||
getPlanningPrompt,
|
||||
getPlanningPromptPrefix,
|
||||
parseTasksFromSpec,
|
||||
parseTaskLine,
|
||||
buildTaskPrompt,
|
||||
isSpecGeneratingMode,
|
||||
canRequireApproval,
|
||||
getPlanningModeDisplayName,
|
||||
} from './planning.js';
|
||||
|
||||
// Feature prompts (implementation)
|
||||
export {
|
||||
buildFeaturePrompt,
|
||||
buildFollowUpPrompt,
|
||||
buildContinuationPrompt,
|
||||
extractTitleFromDescription,
|
||||
} from './feature-prompt.js';
|
||||
|
||||
// Re-export types from @automaker/types
|
||||
export type { EnhancementMode, EnhancementExample } from '@automaker/types';
|
||||
export type {
|
||||
EnhancementMode,
|
||||
EnhancementExample,
|
||||
PlanningMode,
|
||||
ParsedTask,
|
||||
} from '@automaker/types';
|
||||
|
||||
411
libs/prompts/src/planning.ts
Normal file
411
libs/prompts/src/planning.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* Planning Prompts - AI prompt templates for spec-driven development
|
||||
*
|
||||
* Contains planning mode prompts, task parsing utilities, and prompt builders
|
||||
* for the multi-agent task execution workflow.
|
||||
*/
|
||||
|
||||
import type { PlanningMode, ParsedTask } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Planning mode prompt templates
|
||||
*
|
||||
* Each mode has a specific prompt format that instructs the AI to generate
|
||||
* a planning document with task breakdowns in a parseable format.
|
||||
*/
|
||||
export 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.`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the planning prompt for a given mode
|
||||
*
|
||||
* @param mode - The planning mode (skip, lite, spec, full)
|
||||
* @param requireApproval - Whether to use approval variant for lite mode
|
||||
* @returns The prompt string, or empty string for 'skip' mode
|
||||
*/
|
||||
export function getPlanningPrompt(mode: PlanningMode, requireApproval?: boolean): string {
|
||||
if (mode === 'skip') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// For lite mode, use approval variant if required
|
||||
if (mode === 'lite' && requireApproval) {
|
||||
return PLANNING_PROMPTS.lite_with_approval;
|
||||
}
|
||||
|
||||
return PLANNING_PROMPTS[mode] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the planning prompt prefix for a feature prompt
|
||||
*
|
||||
* Used to prepend planning instructions before the feature description.
|
||||
*
|
||||
* @param mode - The planning mode
|
||||
* @param requireApproval - Whether approval is required
|
||||
* @returns Formatted prompt prefix with separator, or empty string
|
||||
*/
|
||||
export function getPlanningPromptPrefix(mode: PlanningMode, requireApproval?: boolean): string {
|
||||
const prompt = getPlanningPrompt(mode, requireApproval);
|
||||
if (!prompt) {
|
||||
return '';
|
||||
}
|
||||
return prompt + '\n\n---\n\n## Feature Request\n\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tasks from generated spec content
|
||||
*
|
||||
* Looks for the ```tasks code block and extracts task lines.
|
||||
* Falls back to finding task lines anywhere in content if no block found.
|
||||
*
|
||||
* @param specContent - The full spec content string
|
||||
* @returns Array of parsed tasks
|
||||
*/
|
||||
export function parseTasksFromSpec(specContent: string): ParsedTask[] {
|
||||
const tasks: ParsedTask[] = [];
|
||||
|
||||
// Extract content within ```tasks ... ``` block
|
||||
const tasksBlockMatch = specContent.match(/```tasks\s*([\s\S]*?)```/);
|
||||
if (!tasksBlockMatch) {
|
||||
// Try fallback: look for task lines anywhere in content
|
||||
const taskLines = specContent.match(/- \[ \] T\d{3}:.*$/gm);
|
||||
if (!taskLines) {
|
||||
return tasks;
|
||||
}
|
||||
// Parse fallback task lines
|
||||
let currentPhase: string | undefined;
|
||||
for (const line of taskLines) {
|
||||
const parsed = parseTaskLine(line, currentPhase);
|
||||
if (parsed) {
|
||||
tasks.push(parsed);
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
const tasksContent = tasksBlockMatch[1];
|
||||
const lines = tasksContent.split('\n');
|
||||
|
||||
let currentPhase: string | undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Check for phase header (e.g., "## Phase 1: Foundation")
|
||||
const phaseMatch = trimmedLine.match(/^##\s*(.+)$/);
|
||||
if (phaseMatch) {
|
||||
currentPhase = phaseMatch[1].trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for task line
|
||||
if (trimmedLine.startsWith('- [ ]')) {
|
||||
const parsed = parseTaskLine(trimmedLine, currentPhase);
|
||||
if (parsed) {
|
||||
tasks.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single task line
|
||||
*
|
||||
* Format: - [ ] T###: Description | File: path/to/file
|
||||
*
|
||||
* @param line - The task line to parse
|
||||
* @param currentPhase - Optional phase context
|
||||
* @returns Parsed task or null if line doesn't match format
|
||||
*/
|
||||
export function parseTaskLine(line: string, currentPhase?: string): ParsedTask | null {
|
||||
// Match pattern: - [ ] T###: Description | File: path
|
||||
const taskMatch = line.match(/- \[ \] (T\d{3}):\s*([^|]+)(?:\|\s*File:\s*(.+))?$/);
|
||||
if (!taskMatch) {
|
||||
// Try simpler pattern without file
|
||||
const simpleMatch = line.match(/- \[ \] (T\d{3}):\s*(.+)$/);
|
||||
if (simpleMatch) {
|
||||
return {
|
||||
id: simpleMatch[1],
|
||||
description: simpleMatch[2].trim(),
|
||||
phase: currentPhase,
|
||||
status: 'pending',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: taskMatch[1],
|
||||
description: taskMatch[2].trim(),
|
||||
filePath: taskMatch[3]?.trim(),
|
||||
phase: currentPhase,
|
||||
status: 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a focused prompt for executing a single task
|
||||
*
|
||||
* Creates a prompt that shows the current task, completed tasks,
|
||||
* and remaining tasks to give the agent context while keeping focus.
|
||||
*
|
||||
* @param task - The current task to execute
|
||||
* @param allTasks - All tasks in the spec
|
||||
* @param taskIndex - Index of current task in allTasks
|
||||
* @param planContent - The full approved plan content
|
||||
* @param userFeedback - Optional user feedback to incorporate
|
||||
* @returns Formatted prompt for task execution
|
||||
*/
|
||||
export function buildTaskPrompt(
|
||||
task: ParsedTask,
|
||||
allTasks: ParsedTask[],
|
||||
taskIndex: number,
|
||||
planContent: string,
|
||||
userFeedback?: string
|
||||
): string {
|
||||
const completedTasks = allTasks.slice(0, taskIndex);
|
||||
const remainingTasks = allTasks.slice(taskIndex + 1);
|
||||
|
||||
let prompt = `# Task Execution: ${task.id}
|
||||
|
||||
You are executing a specific task as part of a larger feature implementation.
|
||||
|
||||
## Your Current Task
|
||||
|
||||
**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.`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a planning mode requires spec generation
|
||||
*/
|
||||
export function isSpecGeneratingMode(mode: PlanningMode): boolean {
|
||||
return mode === 'spec' || mode === 'full' || mode === 'lite';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a planning mode can require approval
|
||||
*/
|
||||
export function canRequireApproval(mode: PlanningMode): boolean {
|
||||
return mode !== 'skip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for a planning mode
|
||||
*/
|
||||
export function getPlanningModeDisplayName(mode: PlanningMode): string {
|
||||
const names: Record<PlanningMode, string> = {
|
||||
skip: 'Skip Planning',
|
||||
lite: 'Lite Planning',
|
||||
spec: 'Specification',
|
||||
full: 'Full SDD',
|
||||
};
|
||||
return names[mode] || mode;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { PlanningMode } from './settings.js';
|
||||
import type { PlanSpec } from './planning.js';
|
||||
|
||||
export interface FeatureImagePath {
|
||||
id: string;
|
||||
@@ -41,20 +42,21 @@ export interface Feature {
|
||||
thinkingLevel?: string;
|
||||
planningMode?: PlanningMode;
|
||||
requirePlanApproval?: boolean;
|
||||
planSpec?: {
|
||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||
content?: string;
|
||||
version: number;
|
||||
generatedAt?: string;
|
||||
approvedAt?: string;
|
||||
reviewedByUser: boolean;
|
||||
tasksCompleted?: number;
|
||||
tasksTotal?: number;
|
||||
};
|
||||
/** Specification state for spec-driven development modes */
|
||||
planSpec?: PlanSpec;
|
||||
error?: string;
|
||||
summary?: string;
|
||||
startedAt?: string;
|
||||
[key: string]: unknown; // Keep catch-all for extensibility
|
||||
}
|
||||
|
||||
export type FeatureStatus = 'pending' | 'running' | 'completed' | 'failed' | 'verified';
|
||||
export type FeatureStatus =
|
||||
| 'pending'
|
||||
| 'ready'
|
||||
| 'backlog'
|
||||
| 'in_progress'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'verified'
|
||||
| 'waiting_approval';
|
||||
|
||||
@@ -81,3 +81,15 @@ export {
|
||||
THINKING_LEVEL_LABELS,
|
||||
getModelDisplayName,
|
||||
} from './model-display.js';
|
||||
|
||||
// Planning types (spec-driven development)
|
||||
export type {
|
||||
TaskStatus,
|
||||
PlanSpecStatus,
|
||||
ParsedTask,
|
||||
PlanSpec,
|
||||
AutoModeEventType,
|
||||
AutoModeEventPayload,
|
||||
TaskProgressPayload,
|
||||
PlanApprovalPayload,
|
||||
} from './planning.js';
|
||||
|
||||
141
libs/types/src/planning.ts
Normal file
141
libs/types/src/planning.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Planning Types - Types for spec-driven development and task execution
|
||||
*
|
||||
* These types support the planning/specification workflow in auto-mode:
|
||||
* - PlanningMode: skip, lite, spec, full
|
||||
* - ParsedTask: Individual tasks extracted from specs
|
||||
* - PlanSpec: Specification state and content
|
||||
* - AutoModeEventType: Type-safe event names for auto-mode
|
||||
*/
|
||||
|
||||
import type { PlanningMode } from './settings.js';
|
||||
|
||||
// Re-export PlanningMode for convenience
|
||||
export type { PlanningMode };
|
||||
|
||||
/**
|
||||
* TaskStatus - Status of an individual task within a spec
|
||||
*/
|
||||
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* PlanSpecStatus - Status of a plan/specification document
|
||||
*/
|
||||
export type PlanSpecStatus = 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||
|
||||
/**
|
||||
* ParsedTask - A single task extracted from a generated specification
|
||||
*
|
||||
* Tasks are identified by ID (e.g., "T001") and may belong to a phase.
|
||||
* Format in spec: `- [ ] T###: Description | File: path/to/file`
|
||||
*/
|
||||
export interface ParsedTask {
|
||||
/** Task identifier, e.g., "T001", "T002" */
|
||||
id: string;
|
||||
/** Human-readable description of what the task accomplishes */
|
||||
description: string;
|
||||
/** Primary file affected by this task (optional) */
|
||||
filePath?: string;
|
||||
/** Phase this task belongs to, e.g., "Phase 1: Foundation" (for full mode) */
|
||||
phase?: string;
|
||||
/** Current execution status of the task */
|
||||
status: TaskStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanSpec - Specification/plan state for a feature
|
||||
*
|
||||
* Tracks the generated spec content, approval status, and task progress.
|
||||
* Stored in feature.json as `planSpec` property.
|
||||
*/
|
||||
export interface PlanSpec {
|
||||
/** Current status of the spec */
|
||||
status: PlanSpecStatus;
|
||||
/** The spec/plan content (markdown) */
|
||||
content?: string;
|
||||
/** Version number, incremented on each revision */
|
||||
version: number;
|
||||
/** ISO timestamp when spec was first generated */
|
||||
generatedAt?: string;
|
||||
/** ISO timestamp when spec was approved */
|
||||
approvedAt?: string;
|
||||
/** Whether user has reviewed (approved/rejected) the spec */
|
||||
reviewedByUser: boolean;
|
||||
/** Number of tasks completed during execution */
|
||||
tasksCompleted?: number;
|
||||
/** Total number of tasks parsed from spec */
|
||||
tasksTotal?: number;
|
||||
/** ID of the task currently being executed */
|
||||
currentTaskId?: string;
|
||||
/** All parsed tasks from the spec */
|
||||
tasks?: ParsedTask[];
|
||||
}
|
||||
|
||||
/**
|
||||
* AutoModeEventType - Type-safe event names emitted by auto-mode service
|
||||
*
|
||||
* All events are wrapped as `auto-mode:event` with `type` field containing
|
||||
* one of these values.
|
||||
*/
|
||||
export type AutoModeEventType =
|
||||
// Auto-loop lifecycle events
|
||||
| 'auto_mode_started'
|
||||
| 'auto_mode_stopped'
|
||||
| 'auto_mode_idle'
|
||||
// Feature execution events
|
||||
| 'auto_mode_feature_start'
|
||||
| 'auto_mode_feature_complete'
|
||||
| 'auto_mode_progress'
|
||||
| 'auto_mode_tool'
|
||||
| 'auto_mode_error'
|
||||
// Task execution events (multi-agent)
|
||||
| 'auto_mode_task_started'
|
||||
| 'auto_mode_task_complete'
|
||||
| 'auto_mode_phase_complete'
|
||||
// Planning/spec events
|
||||
| 'planning_started'
|
||||
| 'plan_approval_required'
|
||||
| 'plan_approved'
|
||||
| 'plan_rejected'
|
||||
| 'plan_auto_approved'
|
||||
| 'plan_revision_requested';
|
||||
|
||||
/**
|
||||
* AutoModeEvent - Base event payload structure
|
||||
*/
|
||||
export interface AutoModeEventPayload {
|
||||
/** The specific event type */
|
||||
type: AutoModeEventType;
|
||||
/** Feature ID this event relates to */
|
||||
featureId?: string;
|
||||
/** Project path */
|
||||
projectPath?: string;
|
||||
/** Additional event-specific data */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* TaskProgressPayload - Event payload for task progress events
|
||||
*/
|
||||
export interface TaskProgressPayload {
|
||||
type: 'auto_mode_task_started' | 'auto_mode_task_complete';
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
taskId: string;
|
||||
taskDescription?: string;
|
||||
taskIndex: number;
|
||||
tasksTotal: number;
|
||||
tasksCompleted?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* PlanApprovalPayload - Event payload for plan approval events
|
||||
*/
|
||||
export interface PlanApprovalPayload {
|
||||
type: 'plan_approval_required';
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
planContent: string;
|
||||
planningMode: PlanningMode;
|
||||
planVersion: number;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
173
libs/utils/src/stream-processor.ts
Normal file
173
libs/utils/src/stream-processor.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user