mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge main into refactor/frontend
Merge latest features from main including: - PR #161 (worktree-confusion): Clarified branch handling in dialogs - PR #160 (speckits-rebase): Planning mode functionality Resolved conflicts: - add-feature-dialog.tsx: Combined TanStack Router navigation with branch selection state - worktree-integration.spec.ts: Updated tests for new worktree behavior (created at execution time) - package-lock.json: Regenerated after merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,22 @@ export function isAbortError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a user-initiated cancellation
|
||||
*
|
||||
* @param errorMessage - The error message to check
|
||||
* @returns True if the error is a user-initiated cancellation
|
||||
*/
|
||||
export function isCancellationError(errorMessage: string): boolean {
|
||||
const lowerMessage = errorMessage.toLowerCase();
|
||||
return (
|
||||
lowerMessage.includes("cancelled") ||
|
||||
lowerMessage.includes("canceled") ||
|
||||
lowerMessage.includes("stopped") ||
|
||||
lowerMessage.includes("aborted")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is an authentication/API key error
|
||||
*
|
||||
@@ -39,7 +55,7 @@ export function isAuthenticationError(errorMessage: string): boolean {
|
||||
/**
|
||||
* Error type classification
|
||||
*/
|
||||
export type ErrorType = "authentication" | "abort" | "execution" | "unknown";
|
||||
export type ErrorType = "authentication" | "cancellation" | "abort" | "execution" | "unknown";
|
||||
|
||||
/**
|
||||
* Classified error information
|
||||
@@ -49,6 +65,7 @@ export interface ErrorInfo {
|
||||
message: string;
|
||||
isAbort: boolean;
|
||||
isAuth: boolean;
|
||||
isCancellation: boolean;
|
||||
originalError: unknown;
|
||||
}
|
||||
|
||||
@@ -62,12 +79,15 @@ export function classifyError(error: unknown): ErrorInfo {
|
||||
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
||||
const isAbort = isAbortError(error);
|
||||
const isAuth = isAuthenticationError(message);
|
||||
const isCancellation = isCancellationError(message);
|
||||
|
||||
let type: ErrorType;
|
||||
if (isAuth) {
|
||||
type = "authentication";
|
||||
} else if (isAbort) {
|
||||
type = "abort";
|
||||
} else if (isCancellation) {
|
||||
type = "cancellation";
|
||||
} else if (error instanceof Error) {
|
||||
type = "execution";
|
||||
} else {
|
||||
@@ -79,6 +99,7 @@ export function classifyError(error: unknown): ErrorInfo {
|
||||
message,
|
||||
isAbort,
|
||||
isAuth,
|
||||
isCancellation,
|
||||
originalError: error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,3 +23,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
import { Router } from "express";
|
||||
import type { AutoModeService } from "../../services/auto-mode-service.js";
|
||||
import { createStartHandler } from "./routes/start.js";
|
||||
import { createStopHandler } from "./routes/stop.js";
|
||||
import { createStopFeatureHandler } from "./routes/stop-feature.js";
|
||||
import { createStatusHandler } from "./routes/status.js";
|
||||
import { createRunFeatureHandler } from "./routes/run-feature.js";
|
||||
@@ -17,12 +15,11 @@ import { createContextExistsHandler } from "./routes/context-exists.js";
|
||||
import { createAnalyzeProjectHandler } from "./routes/analyze-project.js";
|
||||
import { createFollowUpFeatureHandler } from "./routes/follow-up-feature.js";
|
||||
import { createCommitFeatureHandler } from "./routes/commit-feature.js";
|
||||
import { createApprovePlanHandler } from "./routes/approve-plan.js";
|
||||
|
||||
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/start", createStartHandler(autoModeService));
|
||||
router.post("/stop", createStopHandler(autoModeService));
|
||||
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
|
||||
router.post("/status", createStatusHandler(autoModeService));
|
||||
router.post("/run-feature", createRunFeatureHandler(autoModeService));
|
||||
@@ -35,6 +32,7 @@ export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
||||
createFollowUpFeatureHandler(autoModeService)
|
||||
);
|
||||
router.post("/commit-feature", createCommitFeatureHandler(autoModeService));
|
||||
router.post("/approve-plan", createApprovePlanHandler(autoModeService));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
78
apps/server/src/routes/auto-mode/routes/approve-plan.ts
Normal file
78
apps/server/src/routes/auto-mode/routes/approve-plan.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* POST /approve-plan endpoint - Approve or reject a generated plan/spec
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
const logger = createLogger("AutoMode");
|
||||
|
||||
export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { featureId, approved, editedPlan, feedback, projectPath } = req.body as {
|
||||
featureId: string;
|
||||
approved: boolean;
|
||||
editedPlan?: string;
|
||||
feedback?: string;
|
||||
projectPath?: string;
|
||||
};
|
||||
|
||||
if (!featureId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "featureId is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof approved !== "boolean") {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "approved must be a boolean",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: We no longer check hasPendingApproval here because resolvePlanApproval
|
||||
// can handle recovery when pending approval is not in Map but feature has planSpec.status='generated'
|
||||
// This supports cases where the server restarted while waiting for approval
|
||||
|
||||
logger.info(
|
||||
`[AutoMode] Plan ${approved ? "approved" : "rejected"} for feature ${featureId}${
|
||||
editedPlan ? " (with edits)" : ""
|
||||
}${feedback ? ` - Feedback: ${feedback}` : ""}`
|
||||
);
|
||||
|
||||
// Resolve the pending approval (with recovery support)
|
||||
const result = await autoModeService.resolvePlanApproval(
|
||||
featureId,
|
||||
approved,
|
||||
editedPlan,
|
||||
feedback,
|
||||
projectPath
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
approved,
|
||||
message: approved
|
||||
? "Plan approved - implementation will continue"
|
||||
: "Plan rejected - feature execution stopped",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Approve plan failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -12,13 +12,14 @@ const logger = createLogger("AutoMode");
|
||||
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, featureId, prompt, imagePaths, worktreePath } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
worktreePath?: string;
|
||||
};
|
||||
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
|
||||
req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
useWorktrees?: boolean;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId || !prompt) {
|
||||
res.status(400).json({
|
||||
@@ -28,14 +29,25 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start follow-up in background, using the feature's worktreePath for correct branch
|
||||
// Start follow-up in background
|
||||
// followUpFeature derives workDir from feature.branchName
|
||||
autoModeService
|
||||
.followUpFeature(projectPath, featureId, prompt, imagePaths, worktreePath)
|
||||
.followUpFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
useWorktrees ?? true
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`[AutoMode] Follow up feature ${featureId} error:`,
|
||||
error
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
// Release the starting slot when follow-up completes (success or error)
|
||||
// Note: The feature should be in runningFeatures by this point
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -19,12 +19,10 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,30 +12,30 @@ const logger = createLogger("AutoMode");
|
||||
export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, featureId, useWorktrees, worktreePath } = req.body as {
|
||||
const { projectPath, featureId, useWorktrees } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
worktreePath?: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start execution in background
|
||||
// If worktreePath is provided, use it directly; otherwise let the service decide
|
||||
// Default to false - worktrees should only be used when explicitly enabled
|
||||
// executeFeature derives workDir from feature.branchName
|
||||
autoModeService
|
||||
.executeFeature(projectPath, featureId, useWorktrees ?? false, false, worktreePath)
|
||||
.executeFeature(projectPath, featureId, useWorktrees ?? false, false)
|
||||
.catch((error) => {
|
||||
logger.error(`[AutoMode] Feature ${featureId} error:`, error);
|
||||
})
|
||||
.finally(() => {
|
||||
// Release the starting slot when execution completes (success or error)
|
||||
// Note: The feature should be in runningFeatures by this point
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* POST /start endpoint - Start auto mode loop
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createStartHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, maxConcurrency } = req.body as {
|
||||
projectPath: string;
|
||||
maxConcurrency?: number;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Start auto loop failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* POST /stop endpoint - Stop auto mode loop
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createStopHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const runningCount = await autoModeService.stopAutoLoop();
|
||||
res.json({ success: true, runningFeatures: runningCount });
|
||||
} catch (error) {
|
||||
logError(error, "Stop auto loop failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export function createIndexHandler(autoModeService: AutoModeService) {
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: runningAgents.length,
|
||||
autoLoopRunning: status.autoLoopRunning,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get running agents failed");
|
||||
|
||||
@@ -103,3 +103,4 @@ export function createDeleteApiKeyHandler() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -58,10 +59,12 @@ export function createListHandler() {
|
||||
});
|
||||
|
||||
const worktrees: WorktreeInfo[] = [];
|
||||
const removedWorktrees: Array<{ path: string; branch: string }> = [];
|
||||
const lines = stdout.split("\n");
|
||||
let current: { path?: string; branch?: string } = {};
|
||||
let isFirst = true;
|
||||
|
||||
// First pass: detect removed worktrees
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
current.path = normalizePath(line.slice(9));
|
||||
@@ -69,19 +72,40 @@ export function createListHandler() {
|
||||
current.branch = line.slice(7).replace("refs/heads/", "");
|
||||
} else if (line === "") {
|
||||
if (current.path && current.branch) {
|
||||
worktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
isMain: isFirst,
|
||||
isCurrent: current.branch === currentBranch,
|
||||
hasWorktree: true,
|
||||
});
|
||||
isFirst = false;
|
||||
const isMainWorktree = isFirst;
|
||||
// Check if the worktree directory actually exists
|
||||
// Skip checking/pruning the main worktree (projectPath itself)
|
||||
if (!isMainWorktree && !existsSync(current.path)) {
|
||||
// Worktree directory doesn't exist - it was manually deleted
|
||||
removedWorktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
});
|
||||
} else {
|
||||
// Worktree exists (or is main worktree), add it to the list
|
||||
worktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
isMain: isMainWorktree,
|
||||
isCurrent: current.branch === currentBranch,
|
||||
hasWorktree: true,
|
||||
});
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
current = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Prune removed worktrees from git (only if any were detected)
|
||||
if (removedWorktrees.length > 0) {
|
||||
try {
|
||||
await execAsync("git worktree prune", { cwd: projectPath });
|
||||
} catch {
|
||||
// Prune failed, but we'll still report the removed worktrees
|
||||
}
|
||||
}
|
||||
|
||||
// If includeDetails is requested, fetch change status for each worktree
|
||||
if (includeDetails) {
|
||||
for (const worktree of worktrees) {
|
||||
@@ -103,7 +127,11 @@ export function createListHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, worktrees });
|
||||
res.json({
|
||||
success: true,
|
||||
worktrees,
|
||||
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "List worktrees failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,26 @@ export interface Feature {
|
||||
spec?: string;
|
||||
model?: string;
|
||||
imagePaths?: Array<string | { path: string; [key: string]: unknown }>;
|
||||
[key: string]: unknown;
|
||||
// Branch info - worktree path is derived at runtime from branchName
|
||||
branchName?: string; // Name of the feature branch (undefined = use current worktree)
|
||||
skipTests?: boolean;
|
||||
thinkingLevel?: string;
|
||||
planningMode?: 'skip' | 'lite' | 'spec' | 'full';
|
||||
requirePlanApproval?: boolean;
|
||||
planSpec?: {
|
||||
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||
content?: string;
|
||||
version: number;
|
||||
generatedAt?: string;
|
||||
approvedAt?: string;
|
||||
reviewedByUser: boolean;
|
||||
tasksCompleted?: number;
|
||||
tasksTotal?: number;
|
||||
};
|
||||
error?: string;
|
||||
summary?: string;
|
||||
startedAt?: string;
|
||||
[key: string]: unknown; // Keep catch-all for extensibility
|
||||
}
|
||||
|
||||
export class FeatureLoader {
|
||||
|
||||
Reference in New Issue
Block a user