Merge main into worktree-confusion: resolve conflicts maintaining both sets of changes

This commit is contained in:
Cody Seibert
2025-12-17 23:01:48 -05:00
41 changed files with 4831 additions and 717 deletions

View File

@@ -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,
};
}

View File

@@ -15,6 +15,7 @@ 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();
@@ -31,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;
}

View 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) });
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,24 @@ export interface Feature {
imagePaths?: Array<string | { path: string; [key: string]: unknown }>;
// Branch info - worktree path is derived at runtime from branchName
branchName?: string; // Name of the feature branch (undefined = use current worktree)
[key: string]: unknown;
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 {