-
Notifications
You must be signed in to change notification settings - Fork 526
feat: improve worktree and spec generation workflows #387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7cbc45c
03ce309
fcc4bec
4d4799a
ced8aa9
7e7abcc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ import { generateFeaturesFromSpec } from './generate-features-from-spec.js'; | |
| import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform'; | ||
| import type { SettingsService } from '../../services/settings-service.js'; | ||
| import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; | ||
| import { validateBranchName } from '@automaker/git-utils'; | ||
|
|
||
| const logger = createLogger('SpecRegeneration'); | ||
|
|
||
|
|
@@ -37,7 +38,8 @@ export async function generateSpec( | |
| generateFeatures?: boolean, | ||
| analyzeProject?: boolean, | ||
| maxFeatures?: number, | ||
| settingsService?: SettingsService | ||
| settingsService?: SettingsService, | ||
| targetBranch?: string | ||
| ): Promise<void> { | ||
| logger.info('========== generateSpec() started =========='); | ||
| logger.info('projectPath:', projectPath); | ||
|
|
@@ -46,6 +48,7 @@ export async function generateSpec( | |
| logger.info('generateFeatures:', generateFeatures); | ||
| logger.info('analyzeProject:', analyzeProject); | ||
| logger.info('maxFeatures:', maxFeatures); | ||
| logger.info('targetBranch:', targetBranch); | ||
|
|
||
| // Build the prompt based on whether we should analyze the project | ||
| let analysisInstructions = ''; | ||
|
|
@@ -360,6 +363,81 @@ Your entire response should be valid JSON starting with { and ending with }. No | |
| }); | ||
| } | ||
|
|
||
| // Create worktree for target branch if specified and doesn't exist | ||
| if (targetBranch && targetBranch !== 'main' && targetBranch !== 'master') { | ||
| try { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The To fix this, you must validate the const isValidGitBranchName = (branchName: string): boolean => {
// Prevents command injection and path traversal.
if (branchName.includes('..')) {
return false;
}
// A simplified regex for valid git branch names. It disallows characters
// often used in command injection attacks (;, &, |, $, `, etc.).
return /^[a-zA-Z0-9_./-]+$/.test(branchName);
};
if (!isValidGitBranchName(targetBranch)) {
const errorMessage = `Invalid target branch name provided: ${targetBranch}`;
logger.error(errorMessage);
// Throwing an error is important to stop execution and alert the caller.
throw new Error(errorMessage);
}
try {
Comment on lines
+366
to
+368
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's look at the file in question to understand the context better
cat -n apps/server/src/routes/app-spec/generate-spec.ts | sed -n '355,375p'Repository: AutoMaker-Org/automaker Length of output: 999 🏁 Script executed: # Search for how the codebase determines or references primary/main branches
rg -n "primary.*branch|mainBranch|develop.*branch|master.*branch" apps/server/src --type tsRepository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Look for branch-related constants or configurations
rg -n "const.*branch|main.*master|develop" apps/server/src --type ts -B 1 -A 1 | head -n 100Repository: AutoMaker-Org/automaker Length of output: 7742 🏁 Script executed: # Look for function documentation and more context around the worktree creation
cat -n apps/server/src/routes/app-spec/generate-spec.ts | sed -n '300,400p'Repository: AutoMaker-Org/automaker Length of output: 5011 🏁 Script executed: # Search for any configuration or documentation about primary branches
rg -n "primary" apps/server/src --type ts -B 2 -A 2Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Check if there's documentation about the function or branch handling strategy
rg -n "develop" apps/server/src/routes/app-spec --type ts -B 1 -A 1Repository: AutoMaker-Org/automaker Length of output: 49 🏁 Script executed: # Look for how baseBranch or similar concepts are handled
rg -n "baseBranch|base.*branch" apps/server/src --type ts -B 1 -A 1 | head -50Repository: AutoMaker-Org/automaker Length of output: 2541 🏁 Script executed: # Get function signature and documentation
cat -n apps/server/src/routes/app-spec/generate-spec.ts | head -100Repository: AutoMaker-Org/automaker Length of output: 4632 🏁 Script executed: # Search for where generateSpec function is called and how targetBranch is passed
rg -n "generateSpec|targetBranch" apps/server/src/routes --type ts -B 2 -A 2 | head -100Repository: AutoMaker-Org/automaker Length of output: 8523 🏁 Script executed: # Look for any configuration or environment variables related to default/primary branches
rg -n "default.*branch|main.*master" apps/server/src --type ts | head -50Repository: AutoMaker-Org/automaker Length of output: 486 Add support for additional primary branches beyond just The worktree creation logic currently skips only This pattern appears in multiple places (generate-spec.ts:366, parse-and-create-features.ts:123, and delete.ts:60). The worktree logic should detect or accept a configurable list of primary branches rather than hardcoding only two branch names. 🤖 Prompt for AI Agents |
||
| // Validate branch name to prevent command injection | ||
| validateBranchName(targetBranch); | ||
|
|
||
| logger.info(`Checking if worktree for branch '${targetBranch}' exists...`); | ||
| const { execFile } = await import('child_process'); | ||
| const { promisify } = await import('util'); | ||
| const fs = await import('fs'); | ||
| const execFileAsync = promisify(execFile); | ||
|
|
||
| // Check if worktree already exists | ||
| const worktreePath = path.join(projectPath, '.worktrees', targetBranch); | ||
| const worktreeExists = fs.existsSync(worktreePath); | ||
|
|
||
| if (worktreeExists) { | ||
| logger.info(`Worktree for branch '${targetBranch}' already exists at ${worktreePath}`); | ||
| } else { | ||
| // Check if branch exists | ||
| let branchExists = false; | ||
| try { | ||
| await execFileAsync('git', ['rev-parse', '--verify', targetBranch], { cwd: projectPath }); | ||
| branchExists = true; | ||
| logger.info(`Branch '${targetBranch}' already exists`); | ||
| } catch { | ||
| logger.info(`Branch '${targetBranch}' does not exist, will create with worktree`); | ||
| } | ||
|
|
||
| // Create .worktrees directory if it doesn't exist | ||
| const worktreesDir = path.join(projectPath, '.worktrees'); | ||
| if (!fs.existsSync(worktreesDir)) { | ||
| fs.mkdirSync(worktreesDir, { recursive: true }); | ||
| } | ||
|
|
||
| // Create worktree | ||
| logger.info(`Creating worktree for branch '${targetBranch}' at ${worktreePath}`); | ||
| if (branchExists) { | ||
| // Use existing branch | ||
| await execFileAsync('git', ['worktree', 'add', worktreePath, targetBranch], { | ||
| cwd: projectPath, | ||
| }); | ||
| } else { | ||
| // Create new branch from HEAD | ||
| await execFileAsync( | ||
| 'git', | ||
| ['worktree', 'add', '-b', targetBranch, worktreePath, 'HEAD'], | ||
| { | ||
| cwd: projectPath, | ||
| } | ||
| ); | ||
| } | ||
| logger.info(`✓ Created worktree for branch '${targetBranch}'`); | ||
|
|
||
| // Track the branch so it persists in the UI | ||
| try { | ||
| await execFileAsync('git', ['config', `branch.${targetBranch}.remote`, 'origin'], { | ||
| cwd: projectPath, | ||
| }); | ||
| await execFileAsync( | ||
| 'git', | ||
| ['config', `branch.${targetBranch}.merge`, `refs/heads/${targetBranch}`], | ||
| { cwd: projectPath } | ||
| ); | ||
| logger.info(`✓ Configured tracking for branch '${targetBranch}'`); | ||
| } catch (trackError) { | ||
| logger.warn(`Failed to configure tracking for branch '${targetBranch}':`, trackError); | ||
| } | ||
| } | ||
| } catch (worktreeError) { | ||
| logger.warn(`Failed to create worktree for branch '${targetBranch}':`, worktreeError); | ||
| // Don't throw - this is not critical, continue with feature generation | ||
| } | ||
| } | ||
|
|
||
| // If generate features was requested, generate them from the spec | ||
| if (generateFeatures) { | ||
| logger.info('Starting feature generation from spec...'); | ||
|
|
@@ -371,7 +449,8 @@ Your entire response should be valid JSON starting with { and ending with }. No | |
| events, | ||
| featureAbortController, | ||
| maxFeatures, | ||
| settingsService | ||
| settingsService, | ||
| targetBranch | ||
| ); | ||
| // Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures | ||
| } catch (featureError) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,10 +14,12 @@ const logger = createLogger('SpecRegeneration'); | |
| export async function parseAndCreateFeatures( | ||
| projectPath: string, | ||
| content: string, | ||
| events: EventEmitter | ||
| events: EventEmitter, | ||
| targetBranch?: string | ||
| ): Promise<void> { | ||
| logger.info('========== parseAndCreateFeatures() started =========='); | ||
| logger.info(`Content length: ${content.length} chars`); | ||
| logger.info('targetBranch:', targetBranch); | ||
| logger.info('========== CONTENT RECEIVED FOR PARSING =========='); | ||
| logger.info(content); | ||
| logger.info('========== END CONTENT =========='); | ||
|
Comment on lines
14
to
25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging raw Adding 🤖 Prompt for AI Agents |
||
|
|
@@ -53,6 +55,38 @@ export async function parseAndCreateFeatures( | |
| const featuresDir = getFeaturesDir(projectPath); | ||
| await secureFs.mkdir(featuresDir, { recursive: true }); | ||
|
|
||
| // Delete existing features for this branch before creating new ones | ||
| if (targetBranch) { | ||
| logger.info(`Deleting existing features for branch '${targetBranch}'...`); | ||
| try { | ||
| const existingFeatures = await secureFs.readdir(featuresDir); | ||
| let deletedCount = 0; | ||
|
|
||
| for (const featureId of existingFeatures) { | ||
| const featureJsonPath = path.join(featuresDir, featureId, 'feature.json'); | ||
| try { | ||
| const featureContent = (await secureFs.readFile(featureJsonPath, 'utf-8')) as string; | ||
| const featureData = JSON.parse(featureContent); | ||
|
|
||
| // Delete if it matches the target branch | ||
| if (featureData.branchName === targetBranch) { | ||
| logger.info(`Deleting feature ${featureId} from branch ${targetBranch}`); | ||
| await secureFs.rm(path.join(featuresDir, featureId), { recursive: true }); | ||
| deletedCount++; | ||
| } | ||
| } catch (err) { | ||
| // Skip if feature.json doesn't exist or can't be read | ||
| logger.debug(`Skipping ${featureId}: ${err}`); | ||
| } | ||
| } | ||
|
|
||
| logger.info(`✓ Deleted ${deletedCount} existing features for branch '${targetBranch}'`); | ||
| } catch (err) { | ||
| logger.warn('Failed to delete existing features:', err); | ||
| // Continue anyway - not critical | ||
| } | ||
| } | ||
|
|
||
| const createdFeatures: Array<{ id: string; title: string }> = []; | ||
|
|
||
| for (const feature of parsed.features) { | ||
|
|
@@ -69,6 +103,7 @@ export async function parseAndCreateFeatures( | |
| priority: feature.priority || 2, | ||
| complexity: feature.complexity || 'moderate', | ||
| dependencies: feature.dependencies || [], | ||
| branchName: targetBranch || 'main', | ||
| createdAt: new Date().toISOString(), | ||
| updatedAt: new Date().toISOString(), | ||
| }; | ||
|
|
@@ -83,10 +118,18 @@ export async function parseAndCreateFeatures( | |
|
|
||
| logger.info(`✓ Created ${createdFeatures.length} features successfully`); | ||
|
|
||
| // Calculate worktree path for the target branch | ||
| let worktreePath: string | undefined; | ||
| if (targetBranch && targetBranch !== 'main' && targetBranch !== 'master') { | ||
| worktreePath = path.join(projectPath, '.worktrees', targetBranch); | ||
| } | ||
|
|
||
| events.emit('spec-regeneration:event', { | ||
| type: 'spec_regeneration_complete', | ||
| message: `Spec regeneration complete! Created ${createdFeatures.length} features.`, | ||
| projectPath: projectPath, | ||
| targetBranch: targetBranch, | ||
| worktreePath: worktreePath, | ||
| }); | ||
| } catch (error) { | ||
| logger.error('❌ parseAndCreateFeatures() failed:'); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.