diff --git a/apps/app/src/components/views/board-view/components/kanban-card.tsx b/apps/app/src/components/views/board-view/components/kanban-card.tsx index 4d23a31f..1501273e 100644 --- a/apps/app/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/app/src/components/views/board-view/components/kanban-card.tsx @@ -59,9 +59,11 @@ import { Brain, Wand2, Archive, + Lock, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; import { getElectronAPI } from "@/lib/electron"; +import { getBlockingDependencies } from "@/lib/dependency-resolver"; import { parseAgentContext, AgentTaskInfo, @@ -150,10 +152,25 @@ export const KanbanCard = memo(function KanbanCard({ const [agentInfo, setAgentInfo] = useState(null); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const [currentTime, setCurrentTime] = useState(() => Date.now()); - const { kanbanCardDetailLevel } = useAppStore(); + const { kanbanCardDetailLevel, enableDependencyBlocking, features } = useAppStore(); const hasWorktree = !!feature.branchName; + // Calculate blocking dependencies (if feature is in backlog and has incomplete dependencies) + const blockingDependencies = useMemo(() => { + if (!enableDependencyBlocking || feature.status !== "backlog") { + return []; + } + return getBlockingDependencies(feature, features); + }, [enableDependencyBlocking, feature, features]); + + // Determine if a badge occupies the top-right position + const hasBadgeAtTopRight = useMemo(() => { + return feature.status === "backlog" && + !feature.error && + (feature.skipTests || blockingDependencies.length > 0); + }, [feature.status, feature.error, feature.skipTests, blockingDependencies.length]); + const showSteps = kanbanCardDetailLevel === "standard" || kanbanCardDetailLevel === "detailed"; @@ -337,7 +354,7 @@ export const KanbanCard = memo(function KanbanCard({
)} - {/* Skip Tests (Manual) indicator badge */} - {feature.skipTests && !feature.error && ( + {/* Skip Tests (Manual) indicator badge - positioned at top right */} + {feature.skipTests && !feature.error && feature.status === "backlog" && (
- +
- +

Manual verification required

@@ -403,13 +421,14 @@ export const KanbanCard = memo(function KanbanCard({
- +
@@ -419,16 +438,42 @@ export const KanbanCard = memo(function KanbanCard({
)} + {/* Blocked by dependencies badge - positioned at top right */} + {blockingDependencies.length > 0 && !feature.error && !feature.skipTests && feature.status === "backlog" && ( + + + +
+ +
+
+ +

Blocked by {blockingDependencies.length} incomplete {blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}

+

+ {blockingDependencies.map(depId => { + const dep = features.find(f => f.id === depId); + return dep?.description || depId; + }).join(', ')} +

+
+
+
+ )} + {/* Just Finished indicator badge */} {isJustFinished && (
@@ -495,7 +540,7 @@ export const KanbanCard = memo(function KanbanCard({
)} {!isCurrentAutoTask && feature.status === "backlog" && ( -
+
- {onViewOutput && ( + <> +
- )} + {onViewOutput && ( + + )} +
+
+ +
+ + )} + {!isCurrentAutoTask && feature.status === "in_progress" && ( + <> +
+ + + + + + { + e.stopPropagation(); + onEdit(); + }} + data-testid={`edit-feature-${feature.id}`} + className="text-xs" + > + + Edit + + {onViewOutput && ( + { + e.stopPropagation(); + onViewOutput(); + }} + data-testid={`view-logs-${feature.id}`} + className="text-xs" + > + + View Logs + + )} + + +
+
- )} - {!isCurrentAutoTask && feature.status === "in_progress" && ( -
- - - - - - { - e.stopPropagation(); - onEdit(); - }} - data-testid={`edit-feature-${feature.id}`} - className="text-xs" - > - - Edit - - {onViewOutput && ( - { - e.stopPropagation(); - onViewOutput(); - }} - data-testid={`view-logs-${feature.id}`} - className="text-xs" - > - - View Logs - - )} - { - e.stopPropagation(); - handleDeleteClick(e as unknown as React.MouseEvent); - }} - data-testid={`delete-feature-${feature.id}`} - > - - Delete - - - -
+ )}
{isDraggable && ( diff --git a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts index ccae297a..6d2a0852 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts @@ -5,6 +5,7 @@ import { getElectronAPI } from "@/lib/electron"; import { toast } from "sonner"; import { useAutoMode } from "@/hooks/use-auto-mode"; import { truncateDescription } from "@/lib/utils"; +import { getBlockingDependencies } from "@/lib/dependency-resolver"; interface UseBoardActionsProps { currentProject: { path: string; id: string } | null; @@ -53,7 +54,7 @@ export function useBoardActions({ inProgressFeaturesForShortcuts, outputFeature, }: UseBoardActionsProps) { - const { addFeature, updateFeature, removeFeature, moveFeature, useWorktrees } = useAppStore(); + const { addFeature, updateFeature, removeFeature, moveFeature, useWorktrees, enableDependencyBlocking } = useAppStore(); const autoMode = useAutoMode(); const handleAddFeature = useCallback( @@ -188,6 +189,21 @@ export function useBoardActions({ return false; } + // Check for blocking dependencies and show warning if enabled + if (enableDependencyBlocking) { + const blockingDeps = getBlockingDependencies(feature, features); + if (blockingDeps.length > 0) { + const depDescriptions = blockingDeps.map(depId => { + const dep = features.find(f => f.id === depId); + return dep ? truncateDescription(dep.description, 40) : depId; + }).join(", "); + + toast.warning("Starting feature with incomplete dependencies", { + description: `This feature depends on: ${depDescriptions}`, + }); + } + } + const updates = { status: "in_progress" as const, startedAt: new Date().toISOString(), @@ -198,7 +214,7 @@ export function useBoardActions({ await handleRunFeature(feature); return true; }, - [autoMode, updateFeature, persistFeatureUpdate, handleRunFeature] + [autoMode, enableDependencyBlocking, features, updateFeature, persistFeatureUpdate, handleRunFeature] ); const handleVerifyFeature = useCallback( diff --git a/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts b/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts index c3944b5d..b1356bf6 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-column-features.ts @@ -1,5 +1,6 @@ import { useMemo, useCallback } from "react"; import { Feature } from "@/store/app-store"; +import { resolveDependencies } from "@/lib/dependency-resolver"; type ColumnId = Feature["status"]; @@ -51,12 +52,13 @@ export function useBoardColumnFeatures({ } }); - // Sort backlog by priority: 1 (high) -> 2 (medium) -> 3 (low) -> no priority - map.backlog.sort((a, b) => { - const aPriority = a.priority ?? 999; // Features without priority go last - const bPriority = b.priority ?? 999; - return aPriority - bPriority; - }); + // Apply dependency-aware sorting to backlog + // This ensures features appear in dependency order (dependencies before dependents) + // Within the same dependency level, features are sorted by priority + if (map.backlog.length > 0) { + const { orderedFeatures } = resolveDependencies(map.backlog); + map.backlog = orderedFeatures; + } return map; }, [features, runningAutoTasks, searchQuery]); diff --git a/apps/app/src/components/views/settings-view.tsx b/apps/app/src/components/views/settings-view.tsx index bb56262d..a127ba0f 100644 --- a/apps/app/src/components/views/settings-view.tsx +++ b/apps/app/src/components/views/settings-view.tsx @@ -30,6 +30,8 @@ export function SettingsView() { setProjectTheme, defaultSkipTests, setDefaultSkipTests, + enableDependencyBlocking, + setEnableDependencyBlocking, useWorktrees, setUseWorktrees, showProfilesOnly, @@ -118,9 +120,11 @@ export function SettingsView() { ); diff --git a/apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx b/apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx index cd060466..e253c28c 100644 --- a/apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx +++ b/apps/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx @@ -1,23 +1,27 @@ import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; -import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react"; +import { FlaskConical, Settings2, TestTube, GitBranch, AlertCircle } from "lucide-react"; import { cn } from "@/lib/utils"; interface FeatureDefaultsSectionProps { showProfilesOnly: boolean; defaultSkipTests: boolean; + enableDependencyBlocking: boolean; useWorktrees: boolean; onShowProfilesOnlyChange: (value: boolean) => void; onDefaultSkipTestsChange: (value: boolean) => void; + onEnableDependencyBlockingChange: (value: boolean) => void; onUseWorktreesChange: (value: boolean) => void; } export function FeatureDefaultsSection({ showProfilesOnly, defaultSkipTests, + enableDependencyBlocking, useWorktrees, onShowProfilesOnlyChange, onDefaultSkipTestsChange, + onEnableDependencyBlockingChange, onUseWorktreesChange, }: FeatureDefaultsSectionProps) { return ( @@ -102,6 +106,36 @@ export function FeatureDefaultsSection({ {/* Separator */}
+ {/* Dependency Blocking Setting */} +
+ + onEnableDependencyBlockingChange(checked === true) + } + className="mt-1" + data-testid="enable-dependency-blocking-checkbox" + /> +
+ +

+ When enabled, features with incomplete dependencies will show blocked badges + and warnings. Auto mode and backlog ordering always respect dependencies + regardless of this setting. +

+
+
+ + {/* Separator */} +
+ {/* Worktree Isolation Setting */}
; // featureId -> missing dep IDs + blockedFeatures: Map; // featureId -> blocking dep IDs (incomplete dependencies) +} + +/** + * Resolves feature dependencies using topological sort with priority-aware ordering. + * + * Algorithm: + * 1. Build dependency graph and detect missing/blocked dependencies + * 2. Apply Kahn's algorithm for topological sort + * 3. Within same dependency level, sort by priority (1=high, 2=medium, 3=low) + * 4. Detect circular dependencies for features that can't be ordered + * + * @param features - Array of features to order + * @returns Resolution result with ordered features and dependency metadata + */ +export function resolveDependencies(features: Feature[]): DependencyResolutionResult { + const featureMap = new Map(features.map(f => [f.id, f])); + const inDegree = new Map(); + const adjacencyList = new Map(); // dependencyId -> [dependentIds] + const missingDependencies = new Map(); + const blockedFeatures = new Map(); + + // Initialize graph structures + for (const feature of features) { + inDegree.set(feature.id, 0); + adjacencyList.set(feature.id, []); + } + + // Build dependency graph and detect missing/blocked dependencies + for (const feature of features) { + const deps = feature.dependencies || []; + for (const depId of deps) { + if (!featureMap.has(depId)) { + // Missing dependency - track it + if (!missingDependencies.has(feature.id)) { + missingDependencies.set(feature.id, []); + } + missingDependencies.get(feature.id)!.push(depId); + } else { + // Valid dependency - add edge to graph + adjacencyList.get(depId)!.push(feature.id); + inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1); + + // Check if dependency is incomplete (blocking) + const depFeature = featureMap.get(depId)!; + if (depFeature.status !== 'completed' && depFeature.status !== 'verified') { + if (!blockedFeatures.has(feature.id)) { + blockedFeatures.set(feature.id, []); + } + blockedFeatures.get(feature.id)!.push(depId); + } + } + } + } + + // Kahn's algorithm with priority-aware selection + const queue: Feature[] = []; + const orderedFeatures: Feature[] = []; + + // Helper to sort features by priority (lower number = higher priority) + const sortByPriority = (a: Feature, b: Feature) => + (a.priority ?? 2) - (b.priority ?? 2); + + // Start with features that have no dependencies (in-degree 0) + for (const [id, degree] of inDegree) { + if (degree === 0) { + queue.push(featureMap.get(id)!); + } + } + + // Sort initial queue by priority + queue.sort(sortByPriority); + + // Process features in topological order + while (queue.length > 0) { + // Take highest priority feature from queue + const current = queue.shift()!; + orderedFeatures.push(current); + + // Process features that depend on this one + for (const dependentId of adjacencyList.get(current.id) || []) { + const newDegree = (inDegree.get(dependentId) || 1) - 1; + inDegree.set(dependentId, newDegree); + + if (newDegree === 0) { + queue.push(featureMap.get(dependentId)!); + // Re-sort queue to maintain priority order + queue.sort(sortByPriority); + } + } + } + + // Detect circular dependencies (features not in output = part of cycle) + const circularDependencies: string[][] = []; + const processedIds = new Set(orderedFeatures.map(f => f.id)); + + if (orderedFeatures.length < features.length) { + // Find cycles using DFS + const remaining = features.filter(f => !processedIds.has(f.id)); + const cycles = detectCycles(remaining, featureMap); + circularDependencies.push(...cycles); + + // Add remaining features at end (part of cycles) + orderedFeatures.push(...remaining); + } + + return { + orderedFeatures, + circularDependencies, + missingDependencies, + blockedFeatures + }; +} + +/** + * Detects circular dependencies using depth-first search + * + * @param features - Features that couldn't be topologically sorted (potential cycles) + * @param featureMap - Map of all features by ID + * @returns Array of cycles, where each cycle is an array of feature IDs + */ +function detectCycles( + features: Feature[], + featureMap: Map +): string[][] { + const cycles: string[][] = []; + const visited = new Set(); + const recursionStack = new Set(); + const currentPath: string[] = []; + + function dfs(featureId: string): boolean { + visited.add(featureId); + recursionStack.add(featureId); + currentPath.push(featureId); + + const feature = featureMap.get(featureId); + if (feature) { + for (const depId of feature.dependencies || []) { + if (!visited.has(depId)) { + if (dfs(depId)) return true; + } else if (recursionStack.has(depId)) { + // Found cycle - extract it + const cycleStart = currentPath.indexOf(depId); + cycles.push(currentPath.slice(cycleStart)); + return true; + } + } + } + + currentPath.pop(); + recursionStack.delete(featureId); + return false; + } + + for (const feature of features) { + if (!visited.has(feature.id)) { + dfs(feature.id); + } + } + + return cycles; +} + +/** + * Checks if a feature's dependencies are satisfied (all complete or verified) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns true if all dependencies are satisfied, false otherwise + */ +export function areDependenciesSatisfied( + feature: Feature, + allFeatures: Feature[] +): boolean { + if (!feature.dependencies || feature.dependencies.length === 0) { + return true; // No dependencies = always ready + } + + return feature.dependencies.every(depId => { + const dep = allFeatures.find(f => f.id === depId); + return dep && (dep.status === 'completed' || dep.status === 'verified'); + }); +} + +/** + * Gets the blocking dependencies for a feature (dependencies that are incomplete) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns Array of feature IDs that are blocking this feature + */ +export function getBlockingDependencies( + feature: Feature, + allFeatures: Feature[] +): string[] { + if (!feature.dependencies || feature.dependencies.length === 0) { + return []; + } + + return feature.dependencies.filter(depId => { + const dep = allFeatures.find(f => f.id === depId); + return dep && dep.status !== 'completed' && dep.status !== 'verified'; + }); +} diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index d4a0397a..01311526 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -394,6 +394,7 @@ export interface AppState { // Feature Default Settings defaultSkipTests: boolean; // Default value for skip tests when creating new features + enableDependencyBlocking: boolean; // When true, show blocked badges and warnings for features with incomplete dependencies (default: true) // Worktree Settings useWorktrees: boolean; // Whether to use git worktree isolation for features (default: false) @@ -566,6 +567,7 @@ export interface AppActions { // Feature Default Settings actions setDefaultSkipTests: (skip: boolean) => void; + setEnableDependencyBlocking: (enabled: boolean) => void; // Worktree Settings actions setUseWorktrees: (enabled: boolean) => void; @@ -717,6 +719,7 @@ const initialState: AppState = { maxConcurrency: 3, // Default to 3 concurrent agents kanbanCardDetailLevel: "standard", // Default to standard detail level defaultSkipTests: true, // Default to manual verification (tests disabled) + enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI) useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts @@ -1306,6 +1309,7 @@ export const useAppStore = create()( // Feature Default Settings actions setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }), + setEnableDependencyBlocking: (enabled) => set({ enableDependencyBlocking: enabled }), // Worktree Settings actions setUseWorktrees: (enabled) => set({ useWorktrees: enabled }), @@ -2169,6 +2173,7 @@ export const useAppStore = create()( maxConcurrency: state.maxConcurrency, autoModeByProject: state.autoModeByProject, defaultSkipTests: state.defaultSkipTests, + enableDependencyBlocking: state.enableDependencyBlocking, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, keyboardShortcuts: state.keyboardShortcuts, diff --git a/apps/server/src/lib/dependency-resolver.ts b/apps/server/src/lib/dependency-resolver.ts new file mode 100644 index 00000000..619d62e4 --- /dev/null +++ b/apps/server/src/lib/dependency-resolver.ts @@ -0,0 +1,226 @@ +/** + * Dependency Resolution Utility (Server-side) + * + * Provides topological sorting and dependency analysis for features. + * Uses a modified Kahn's algorithm that respects both dependencies and priorities. + */ + +interface Feature { + id: string; + category: string; + description: string; + steps?: string[]; + status: string; + priority?: number; + dependencies?: string[]; + [key: string]: unknown; +} + +export interface DependencyResolutionResult { + orderedFeatures: Feature[]; // Features in dependency-aware order + circularDependencies: string[][]; // Groups of IDs forming cycles + missingDependencies: Map; // featureId -> missing dep IDs + blockedFeatures: Map; // featureId -> blocking dep IDs (incomplete dependencies) +} + +/** + * Resolves feature dependencies using topological sort with priority-aware ordering. + * + * Algorithm: + * 1. Build dependency graph and detect missing/blocked dependencies + * 2. Apply Kahn's algorithm for topological sort + * 3. Within same dependency level, sort by priority (1=high, 2=medium, 3=low) + * 4. Detect circular dependencies for features that can't be ordered + * + * @param features - Array of features to order + * @returns Resolution result with ordered features and dependency metadata + */ +export function resolveDependencies(features: Feature[]): DependencyResolutionResult { + const featureMap = new Map(features.map(f => [f.id, f])); + const inDegree = new Map(); + const adjacencyList = new Map(); // dependencyId -> [dependentIds] + const missingDependencies = new Map(); + const blockedFeatures = new Map(); + + // Initialize graph structures + for (const feature of features) { + inDegree.set(feature.id, 0); + adjacencyList.set(feature.id, []); + } + + // Build dependency graph and detect missing/blocked dependencies + for (const feature of features) { + const deps = feature.dependencies || []; + for (const depId of deps) { + if (!featureMap.has(depId)) { + // Missing dependency - track it + if (!missingDependencies.has(feature.id)) { + missingDependencies.set(feature.id, []); + } + missingDependencies.get(feature.id)!.push(depId); + } else { + // Valid dependency - add edge to graph + adjacencyList.get(depId)!.push(feature.id); + inDegree.set(feature.id, (inDegree.get(feature.id) || 0) + 1); + + // Check if dependency is incomplete (blocking) + const depFeature = featureMap.get(depId)!; + if (depFeature.status !== 'completed' && depFeature.status !== 'verified') { + if (!blockedFeatures.has(feature.id)) { + blockedFeatures.set(feature.id, []); + } + blockedFeatures.get(feature.id)!.push(depId); + } + } + } + } + + // Kahn's algorithm with priority-aware selection + const queue: Feature[] = []; + const orderedFeatures: Feature[] = []; + + // Helper to sort features by priority (lower number = higher priority) + const sortByPriority = (a: Feature, b: Feature) => + (a.priority ?? 2) - (b.priority ?? 2); + + // Start with features that have no dependencies (in-degree 0) + for (const [id, degree] of inDegree) { + if (degree === 0) { + queue.push(featureMap.get(id)!); + } + } + + // Sort initial queue by priority + queue.sort(sortByPriority); + + // Process features in topological order + while (queue.length > 0) { + // Take highest priority feature from queue + const current = queue.shift()!; + orderedFeatures.push(current); + + // Process features that depend on this one + for (const dependentId of adjacencyList.get(current.id) || []) { + const newDegree = (inDegree.get(dependentId) || 1) - 1; + inDegree.set(dependentId, newDegree); + + if (newDegree === 0) { + queue.push(featureMap.get(dependentId)!); + // Re-sort queue to maintain priority order + queue.sort(sortByPriority); + } + } + } + + // Detect circular dependencies (features not in output = part of cycle) + const circularDependencies: string[][] = []; + const processedIds = new Set(orderedFeatures.map(f => f.id)); + + if (orderedFeatures.length < features.length) { + // Find cycles using DFS + const remaining = features.filter(f => !processedIds.has(f.id)); + const cycles = detectCycles(remaining, featureMap); + circularDependencies.push(...cycles); + + // Add remaining features at end (part of cycles) + orderedFeatures.push(...remaining); + } + + return { + orderedFeatures, + circularDependencies, + missingDependencies, + blockedFeatures + }; +} + +/** + * Detects circular dependencies using depth-first search + * + * @param features - Features that couldn't be topologically sorted (potential cycles) + * @param featureMap - Map of all features by ID + * @returns Array of cycles, where each cycle is an array of feature IDs + */ +function detectCycles( + features: Feature[], + featureMap: Map +): string[][] { + const cycles: string[][] = []; + const visited = new Set(); + const recursionStack = new Set(); + const currentPath: string[] = []; + + function dfs(featureId: string): boolean { + visited.add(featureId); + recursionStack.add(featureId); + currentPath.push(featureId); + + const feature = featureMap.get(featureId); + if (feature) { + for (const depId of feature.dependencies || []) { + if (!visited.has(depId)) { + if (dfs(depId)) return true; + } else if (recursionStack.has(depId)) { + // Found cycle - extract it + const cycleStart = currentPath.indexOf(depId); + cycles.push(currentPath.slice(cycleStart)); + return true; + } + } + } + + currentPath.pop(); + recursionStack.delete(featureId); + return false; + } + + for (const feature of features) { + if (!visited.has(feature.id)) { + dfs(feature.id); + } + } + + return cycles; +} + +/** + * Checks if a feature's dependencies are satisfied (all complete or verified) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns true if all dependencies are satisfied, false otherwise + */ +export function areDependenciesSatisfied( + feature: Feature, + allFeatures: Feature[] +): boolean { + if (!feature.dependencies || feature.dependencies.length === 0) { + return true; // No dependencies = always ready + } + + return feature.dependencies.every(depId => { + const dep = allFeatures.find(f => f.id === depId); + return dep && (dep.status === 'completed' || dep.status === 'verified'); + }); +} + +/** + * Gets the blocking dependencies for a feature (dependencies that are incomplete) + * + * @param feature - Feature to check + * @param allFeatures - All features in the project + * @returns Array of feature IDs that are blocking this feature + */ +export function getBlockingDependencies( + feature: Feature, + allFeatures: Feature[] +): string[] { + if (!feature.dependencies || feature.dependencies.length === 0) { + return []; + } + + return feature.dependencies.filter(depId => { + const dep = allFeatures.find(f => f.id === depId); + return dep && dep.status !== 'completed' && dep.status !== 'verified'; + }); +} diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 00bfd0f7..9f79ea8b 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -20,6 +20,7 @@ import { buildPromptWithImages } from "../lib/prompt-builder.js"; import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js"; import { createAutoModeOptions } from "../lib/sdk-options.js"; import { isAbortError, classifyError } from "../lib/error-handler.js"; +import { resolveDependencies, areDependenciesSatisfied } from "../lib/dependency-resolver.js"; const execAsync = promisify(exec); @@ -30,6 +31,7 @@ interface Feature { steps?: string[]; status: string; priority?: number; + dependencies?: string[]; // Feature dependencies spec?: string; model?: string; // Model to use for this feature imagePaths?: Array< @@ -943,8 +945,10 @@ Format your response as a structured markdown document.`; try { const entries = await fs.readdir(featuresDir, { withFileTypes: true }); - const features: Feature[] = []; + const allFeatures: Feature[] = []; + const pendingFeatures: Feature[] = []; + // Load all features (for dependency checking) for (const entry of entries) { if (entry.isDirectory()) { const featurePath = path.join( @@ -955,12 +959,15 @@ Format your response as a structured markdown document.`; try { const data = await fs.readFile(featurePath, "utf-8"); const feature = JSON.parse(data); + allFeatures.push(feature); + + // Track pending features separately if ( feature.status === "pending" || feature.status === "ready" || feature.status === "backlog" ) { - features.push(feature); + pendingFeatures.push(feature); } } catch { // Skip invalid features @@ -968,8 +975,15 @@ Format your response as a structured markdown document.`; } } - // Sort by priority - return features.sort((a, b) => (a.priority || 999) - (b.priority || 999)); + // Apply dependency-aware ordering + const { orderedFeatures } = resolveDependencies(pendingFeatures); + + // Filter to only features with satisfied dependencies + const readyFeatures = orderedFeatures.filter(feature => + areDependenciesSatisfied(feature, allFeatures) + ); + + return readyFeatures; } catch { return []; }