mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
merge: integrate v0.13.0rc with React Query refactor
Resolved conflict in use-project-settings-loader.ts: - Keep React Query approach from upstream - Add phaseModelOverrides loading for provider model persistence - Update both currentProject and projects array to keep in sync
This commit is contained in:
@@ -249,7 +249,8 @@ interface AutoModeConfig {
|
||||
* @param branchName - The branch name, or null for main worktree
|
||||
*/
|
||||
function getWorktreeAutoLoopKey(projectPath: string, branchName: string | null): string {
|
||||
return `${projectPath}::${branchName ?? '__main__'}`;
|
||||
const normalizedBranch = branchName === 'main' ? null : branchName;
|
||||
return `${projectPath}::${normalizedBranch ?? '__main__'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -515,14 +516,11 @@ export class AutoModeService {
|
||||
? settings.maxConcurrency
|
||||
: DEFAULT_MAX_CONCURRENCY;
|
||||
const projectId = settings.projects?.find((project) => project.path === projectPath)?.id;
|
||||
const autoModeByWorktree = (settings as unknown as Record<string, unknown>)
|
||||
.autoModeByWorktree;
|
||||
const autoModeByWorktree = settings.autoModeByWorktree;
|
||||
|
||||
if (projectId && autoModeByWorktree && typeof autoModeByWorktree === 'object') {
|
||||
const key = `${projectId}::${branchName ?? '__main__'}`;
|
||||
const entry = (autoModeByWorktree as Record<string, unknown>)[key] as
|
||||
| { maxConcurrency?: number }
|
||||
| undefined;
|
||||
const entry = autoModeByWorktree[key];
|
||||
if (entry && typeof entry.maxConcurrency === 'number') {
|
||||
return entry.maxConcurrency;
|
||||
}
|
||||
@@ -593,6 +591,7 @@ export class AutoModeService {
|
||||
message: `Auto mode started with max ${resolvedMaxConcurrency} concurrent features`,
|
||||
projectPath,
|
||||
branchName,
|
||||
maxConcurrency: resolvedMaxConcurrency,
|
||||
});
|
||||
|
||||
// Save execution state for recovery after restart
|
||||
@@ -678,8 +677,10 @@ export class AutoModeService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a feature not currently running
|
||||
const nextFeature = pendingFeatures.find((f) => !this.runningFeatures.has(f.id));
|
||||
// Find a feature not currently running and not yet finished
|
||||
const nextFeature = pendingFeatures.find(
|
||||
(f) => !this.runningFeatures.has(f.id) && !this.isFeatureFinished(f)
|
||||
);
|
||||
|
||||
if (nextFeature) {
|
||||
logger.info(`[AutoLoop] Starting feature ${nextFeature.id}: ${nextFeature.title}`);
|
||||
@@ -731,11 +732,12 @@ export class AutoModeService {
|
||||
* @param branchName - The branch name, or null for main worktree (features without branchName or with "main")
|
||||
*/
|
||||
private getRunningCountForWorktree(projectPath: string, branchName: string | null): number {
|
||||
const normalizedBranch = branchName === 'main' ? null : branchName;
|
||||
let count = 0;
|
||||
for (const [, feature] of this.runningFeatures) {
|
||||
// Filter by project path AND branchName to get accurate worktree-specific count
|
||||
const featureBranch = feature.branchName ?? null;
|
||||
if (branchName === null) {
|
||||
if (normalizedBranch === null) {
|
||||
// Main worktree: match features with branchName === null OR branchName === "main"
|
||||
if (
|
||||
feature.projectPath === projectPath &&
|
||||
@@ -999,6 +1001,41 @@ export class AutoModeService {
|
||||
return this.runningFeatures.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's capacity to start a feature on a worktree.
|
||||
* This respects per-worktree agent limits from autoModeByWorktree settings.
|
||||
*
|
||||
* @param projectPath - The main project path
|
||||
* @param featureId - The feature ID to check capacity for
|
||||
* @returns Object with hasCapacity boolean and details about current/max agents
|
||||
*/
|
||||
async checkWorktreeCapacity(
|
||||
projectPath: string,
|
||||
featureId: string
|
||||
): Promise<{
|
||||
hasCapacity: boolean;
|
||||
currentAgents: number;
|
||||
maxAgents: number;
|
||||
branchName: string | null;
|
||||
}> {
|
||||
// Load feature to get branchName
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
const branchName = feature?.branchName ?? null;
|
||||
|
||||
// Get per-worktree limit
|
||||
const maxAgents = await this.resolveMaxConcurrency(projectPath, branchName);
|
||||
|
||||
// Get current running count for this worktree
|
||||
const currentAgents = this.getRunningCountForWorktree(projectPath, branchName);
|
||||
|
||||
return {
|
||||
hasCapacity: currentAgents < maxAgents,
|
||||
currentAgents,
|
||||
maxAgents,
|
||||
branchName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single feature
|
||||
* @param projectPath - The main project path
|
||||
@@ -1037,7 +1074,6 @@ export class AutoModeService {
|
||||
if (isAutoMode) {
|
||||
await this.saveExecutionState(projectPath);
|
||||
}
|
||||
|
||||
// Declare feature outside try block so it's available in catch for error reporting
|
||||
let feature: Awaited<ReturnType<typeof this.loadFeature>> | null = null;
|
||||
|
||||
@@ -1045,9 +1081,44 @@ export class AutoModeService {
|
||||
// Validate that project path is allowed using centralized validation
|
||||
validateWorkingDirectory(projectPath);
|
||||
|
||||
// Load feature details FIRST to get status and plan info
|
||||
feature = await this.loadFeature(projectPath, featureId);
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${featureId} not found`);
|
||||
}
|
||||
|
||||
// Check if feature has existing context - if so, resume instead of starting fresh
|
||||
// Skip this check if we're already being called with a continuation prompt (from resumeFeature)
|
||||
if (!options?.continuationPrompt) {
|
||||
// If feature has an approved plan but we don't have a continuation prompt yet,
|
||||
// we should build one to ensure it proceeds with multi-agent execution
|
||||
if (feature.planSpec?.status === 'approved') {
|
||||
logger.info(`Feature ${featureId} has approved plan, building continuation prompt`);
|
||||
|
||||
// Get customized prompts from settings
|
||||
const prompts = await getPromptCustomization(this.settingsService, '[AutoMode]');
|
||||
const planContent = feature.planSpec.content || '';
|
||||
|
||||
// Build continuation prompt using centralized template
|
||||
let continuationPrompt = prompts.taskExecution.continuationAfterApprovalTemplate;
|
||||
continuationPrompt = continuationPrompt.replace(/\{\{userFeedback\}\}/g, '');
|
||||
continuationPrompt = continuationPrompt.replace(/\{\{approvedPlan\}\}/g, planContent);
|
||||
|
||||
// Recursively call executeFeature with the continuation prompt
|
||||
// Remove from running features temporarily, it will be added back
|
||||
this.runningFeatures.delete(featureId);
|
||||
return this.executeFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
useWorktrees,
|
||||
isAutoMode,
|
||||
providedWorktreePath,
|
||||
{
|
||||
continuationPrompt,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const hasExistingContext = await this.contextExists(projectPath, featureId);
|
||||
if (hasExistingContext) {
|
||||
logger.info(
|
||||
@@ -1059,12 +1130,6 @@ export class AutoModeService {
|
||||
}
|
||||
}
|
||||
|
||||
// Load feature details FIRST to get branchName
|
||||
feature = await this.loadFeature(projectPath, featureId);
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${featureId} not found`);
|
||||
}
|
||||
|
||||
// Derive workDir from feature.branchName
|
||||
// Worktrees should already be created when the feature is added/edited
|
||||
let worktreePath: string | null = null;
|
||||
@@ -1191,6 +1256,7 @@ export class AutoModeService {
|
||||
systemPrompt: combinedSystemPrompt || undefined,
|
||||
autoLoadClaudeMd,
|
||||
thinkingLevel: feature.thinkingLevel,
|
||||
branchName: feature.branchName ?? null,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1362,6 +1428,7 @@ export class AutoModeService {
|
||||
|
||||
this.emitAutoModeEvent('auto_mode_progress', {
|
||||
featureId,
|
||||
branchName: feature.branchName ?? null,
|
||||
content: `Starting pipeline step ${i + 1}/${steps.length}: ${step.name}`,
|
||||
projectPath,
|
||||
});
|
||||
@@ -2816,6 +2883,21 @@ Format your response as a structured markdown document.`;
|
||||
}
|
||||
}
|
||||
|
||||
private isFeatureFinished(feature: Feature): boolean {
|
||||
const isCompleted = feature.status === 'completed' || feature.status === 'verified';
|
||||
|
||||
// Even if marked as completed, if it has an approved plan with pending tasks, it's not finished
|
||||
if (feature.planSpec?.status === 'approved') {
|
||||
const tasksCompleted = feature.planSpec.tasksCompleted ?? 0;
|
||||
const tasksTotal = feature.planSpec.tasksTotal ?? 0;
|
||||
if (tasksCompleted < tasksTotal) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isCompleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the planSpec of a feature
|
||||
*/
|
||||
@@ -2910,10 +2992,14 @@ Format your response as a structured markdown document.`;
|
||||
allFeatures.push(feature);
|
||||
|
||||
// Track pending features separately, filtered by worktree/branch
|
||||
// Note: waiting_approval is NOT included - those features have completed execution
|
||||
// and are waiting for user review, they should not be picked up again
|
||||
if (
|
||||
feature.status === 'pending' ||
|
||||
feature.status === 'ready' ||
|
||||
feature.status === 'backlog'
|
||||
feature.status === 'backlog' ||
|
||||
(feature.planSpec?.status === 'approved' &&
|
||||
(feature.planSpec.tasksCompleted ?? 0) < (feature.planSpec.tasksTotal ?? 0))
|
||||
) {
|
||||
// Filter by branchName:
|
||||
// - If branchName is null (main worktree), include features with branchName === null OR branchName === "main"
|
||||
@@ -2945,7 +3031,7 @@ Format your response as a structured markdown document.`;
|
||||
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
logger.info(
|
||||
`[loadPendingFeatures] Found ${allFeatures.length} total features, ${pendingFeatures.length} with backlog/pending/ready status for ${worktreeDesc}`
|
||||
`[loadPendingFeatures] Found ${allFeatures.length} total features, ${pendingFeatures.length} candidates (pending/ready/backlog/approved_with_pending_tasks) for ${worktreeDesc}`
|
||||
);
|
||||
|
||||
if (pendingFeatures.length === 0) {
|
||||
@@ -2954,7 +3040,12 @@ Format your response as a structured markdown document.`;
|
||||
);
|
||||
// Log all backlog features to help debug branchName matching
|
||||
const allBacklogFeatures = allFeatures.filter(
|
||||
(f) => f.status === 'backlog' || f.status === 'pending' || f.status === 'ready'
|
||||
(f) =>
|
||||
f.status === 'backlog' ||
|
||||
f.status === 'pending' ||
|
||||
f.status === 'ready' ||
|
||||
(f.planSpec?.status === 'approved' &&
|
||||
(f.planSpec.tasksCompleted ?? 0) < (f.planSpec.tasksTotal ?? 0))
|
||||
);
|
||||
if (allBacklogFeatures.length > 0) {
|
||||
logger.info(
|
||||
@@ -2964,7 +3055,43 @@ Format your response as a structured markdown document.`;
|
||||
}
|
||||
|
||||
// Apply dependency-aware ordering
|
||||
const { orderedFeatures } = resolveDependencies(pendingFeatures);
|
||||
const { orderedFeatures, missingDependencies } = resolveDependencies(pendingFeatures);
|
||||
|
||||
// Remove missing dependencies from features and save them
|
||||
// This allows features to proceed when their dependencies have been deleted or don't exist
|
||||
if (missingDependencies.size > 0) {
|
||||
for (const [featureId, missingDepIds] of missingDependencies) {
|
||||
const feature = pendingFeatures.find((f) => f.id === featureId);
|
||||
if (feature && feature.dependencies) {
|
||||
// Filter out the missing dependency IDs
|
||||
const validDependencies = feature.dependencies.filter(
|
||||
(depId) => !missingDepIds.includes(depId)
|
||||
);
|
||||
|
||||
logger.warn(
|
||||
`[loadPendingFeatures] Feature ${featureId} has missing dependencies: ${missingDepIds.join(', ')}. Removing them automatically.`
|
||||
);
|
||||
|
||||
// Update the feature in memory
|
||||
feature.dependencies = validDependencies.length > 0 ? validDependencies : undefined;
|
||||
|
||||
// Save the updated feature to disk
|
||||
try {
|
||||
await this.featureLoader.update(projectPath, featureId, {
|
||||
dependencies: feature.dependencies,
|
||||
});
|
||||
logger.info(
|
||||
`[loadPendingFeatures] Updated feature ${featureId} - removed missing dependencies`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[loadPendingFeatures] Failed to save feature ${featureId} after removing missing dependencies:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get skipVerificationInAutoMode setting
|
||||
const settings = await this.settingsService?.getGlobalSettings();
|
||||
@@ -3140,9 +3267,11 @@ You can use the Read tool to view these images at any time during implementation
|
||||
systemPrompt?: string;
|
||||
autoLoadClaudeMd?: boolean;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
branchName?: string | null;
|
||||
}
|
||||
): Promise<void> {
|
||||
const finalProjectPath = options?.projectPath || projectPath;
|
||||
const branchName = options?.branchName ?? null;
|
||||
const planningMode = options?.planningMode || 'skip';
|
||||
const previousContent = options?.previousContent;
|
||||
|
||||
@@ -3528,6 +3657,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
this.emitAutoModeEvent('plan_approval_required', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
planContent: currentPlanContent,
|
||||
planningMode,
|
||||
planVersion,
|
||||
@@ -3559,6 +3689,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
this.emitAutoModeEvent('plan_approved', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
hasEdits: !!approvalResult.editedPlan,
|
||||
planVersion,
|
||||
});
|
||||
@@ -3587,6 +3718,7 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
this.emitAutoModeEvent('plan_revision_requested', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
feedback: approvalResult.feedback,
|
||||
hasEdits: !!hasEdits,
|
||||
planVersion,
|
||||
@@ -3690,6 +3822,7 @@ After generating the revised spec, output:
|
||||
this.emitAutoModeEvent('plan_auto_approved', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
planContent,
|
||||
planningMode,
|
||||
});
|
||||
@@ -3740,6 +3873,7 @@ After generating the revised spec, output:
|
||||
this.emitAutoModeEvent('auto_mode_task_started', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
taskId: task.id,
|
||||
taskDescription: task.description,
|
||||
taskIndex,
|
||||
@@ -3785,11 +3919,13 @@ After generating the revised spec, output:
|
||||
responseText += block.text || '';
|
||||
this.emitAutoModeEvent('auto_mode_progress', {
|
||||
featureId,
|
||||
branchName,
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === 'tool_use') {
|
||||
this.emitAutoModeEvent('auto_mode_tool', {
|
||||
featureId,
|
||||
branchName,
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
@@ -3808,6 +3944,7 @@ After generating the revised spec, output:
|
||||
this.emitAutoModeEvent('auto_mode_task_complete', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
taskId: task.id,
|
||||
tasksCompleted: taskIndex + 1,
|
||||
tasksTotal: parsedTasks.length,
|
||||
@@ -3828,6 +3965,7 @@ After generating the revised spec, output:
|
||||
this.emitAutoModeEvent('auto_mode_phase_complete', {
|
||||
featureId,
|
||||
projectPath,
|
||||
branchName,
|
||||
phaseNumber: parseInt(phaseMatch[1], 10),
|
||||
});
|
||||
}
|
||||
@@ -3877,11 +4015,13 @@ After generating the revised spec, output:
|
||||
responseText += block.text || '';
|
||||
this.emitAutoModeEvent('auto_mode_progress', {
|
||||
featureId,
|
||||
branchName,
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === 'tool_use') {
|
||||
this.emitAutoModeEvent('auto_mode_tool', {
|
||||
featureId,
|
||||
branchName,
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
@@ -3907,6 +4047,7 @@ After generating the revised spec, output:
|
||||
);
|
||||
this.emitAutoModeEvent('auto_mode_progress', {
|
||||
featureId,
|
||||
branchName,
|
||||
content: block.text,
|
||||
});
|
||||
}
|
||||
@@ -3914,6 +4055,7 @@ After generating the revised spec, output:
|
||||
// Emit event for real-time UI
|
||||
this.emitAutoModeEvent('auto_mode_tool', {
|
||||
featureId,
|
||||
branchName,
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
@@ -4319,6 +4461,7 @@ After generating the revised spec, output:
|
||||
id: f.id,
|
||||
title: f.title,
|
||||
status: f.status,
|
||||
branchName: f.branchName ?? null,
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user