Merge main into massive-terminal-upgrade

Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:27:44 -05:00
393 changed files with 32473 additions and 17974 deletions

View File

@@ -0,0 +1,188 @@
# @automaker/dependency-resolver
Feature dependency resolution using topological sorting.
## Overview
This package provides dependency resolution for AutoMaker features using Kahn's algorithm with priority-aware ordering. It ensures features are executed in the correct order based on their dependencies.
## Installation
```bash
npm install @automaker/dependency-resolver
```
## Exports
### Resolve Dependencies
Order features based on dependencies and priorities.
```typescript
import { resolveDependencies } from '@automaker/dependency-resolver';
import type { Feature } from '@automaker/types';
const features: Feature[] = [
{
id: 'database',
category: 'backend',
description: 'Setup database',
priority: 1,
},
{
id: 'auth',
category: 'backend',
description: 'Add authentication',
dependencies: ['database'],
priority: 2,
},
{
id: 'api',
category: 'backend',
description: 'Create API endpoints',
dependencies: ['auth'],
priority: 3,
},
];
const result = resolveDependencies(features);
console.log(result.orderedFeatures);
// [database, auth, api]
if (result.hasCycle) {
console.error('Circular dependency detected!');
console.error('Features in cycle:', result.cyclicFeatures);
}
```
### Check Dependencies Satisfied
Check if a feature's dependencies are satisfied.
```typescript
import { areDependenciesSatisfied } from '@automaker/dependency-resolver';
const allFeatures: Feature[] = [
{ id: 'database', status: 'completed', ... },
{ id: 'auth', status: 'pending', dependencies: ['database'], ... }
];
const authFeature = allFeatures.find(f => f.id === 'auth');
if (areDependenciesSatisfied(authFeature, allFeatures)) {
console.log('Auth feature is ready to execute');
} else {
console.log('Waiting for dependencies');
}
```
### Get Blocking Dependencies
Get list of incomplete dependencies blocking a feature.
```typescript
import { getBlockingDependencies } from '@automaker/dependency-resolver';
const blocking = getBlockingDependencies(feature, allFeatures);
if (blocking.length > 0) {
console.log(`Feature blocked by: ${blocking.join(', ')}`);
} else {
console.log('No blocking dependencies');
}
```
## Usage Example
```typescript
import {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
} from '@automaker/dependency-resolver';
import type { Feature } from '@automaker/types';
async function executeFeatures(features: Feature[]) {
// Resolve dependency order
const { orderedFeatures, hasCycle, cyclicFeatures } = resolveDependencies(features);
if (hasCycle) {
throw new Error(`Circular dependency: ${cyclicFeatures.join(' → ')}`);
}
// Execute in order
for (const feature of orderedFeatures) {
// Check if dependencies are satisfied
if (!areDependenciesSatisfied(feature, features)) {
const blocking = getBlockingDependencies(feature, features);
console.log(`Skipping ${feature.id}, blocked by: ${blocking.join(', ')}`);
continue;
}
// Execute feature
console.log(`Executing: ${feature.id}`);
await executeFeature(feature);
// Mark as completed
feature.status = 'completed';
}
}
```
## Algorithm
### Topological Sort (Kahn's Algorithm)
1. Calculate in-degree for each feature (number of dependencies)
2. Start with features that have no dependencies (in-degree = 0)
3. Process features in priority order
4. Remove processed features from dependency graph
5. Repeat until all features processed or cycle detected
### Priority Handling
- Features with lower priority numbers execute first
- When multiple features have same in-degree, priority determines order
- Features without explicit priority default to lowest priority
### Cycle Detection
- Detects circular dependencies
- Returns affected features in cycle
- Prevents infinite loops in execution
## Return Types
### DependencyResolutionResult
```typescript
interface DependencyResolutionResult {
orderedFeatures: Feature[]; // Features in execution order
hasCycle: boolean; // True if circular dependency detected
cyclicFeatures: string[]; // Feature IDs involved in cycle
}
```
## Edge Cases
### Missing Dependencies
Features with dependencies on non-existent features are treated as if the dependency is satisfied (allows flexibility).
### Self-Dependencies
Features depending on themselves are detected as cycles.
### Empty Dependencies Array
Treated same as no dependencies - feature is ready immediately.
## Dependencies
- `@automaker/types` - Feature type definition
## Used By
- `@automaker/server` - Auto-mode feature execution
- `@automaker/ui` - Board view feature ordering

View File

@@ -0,0 +1,36 @@
{
"name": "@automaker/dependency-resolver",
"version": "1.0.0",
"description": "Feature dependency resolution for AutoMaker",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": [
"automaker",
"dependency",
"resolver"
],
"author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@automaker/types": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3",
"vitest": "^4.0.16"
}
}

View File

@@ -0,0 +1,11 @@
/**
* @automaker/dependency-resolver
* Feature dependency resolution for AutoMaker
*/
export {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
type DependencyResolutionResult,
} from './resolver.js';

View File

@@ -0,0 +1,211 @@
/**
* Dependency Resolution Utility
*
* Provides topological sorting and dependency analysis for features.
* Uses a modified Kahn's algorithm that respects both dependencies and priorities.
*/
import type { Feature } from '@automaker/types';
export interface DependencyResolutionResult {
orderedFeatures: Feature[]; // Features in dependency-aware order
circularDependencies: string[][]; // Groups of IDs forming cycles
missingDependencies: Map<string, string[]>; // featureId -> missing dep IDs
blockedFeatures: Map<string, string[]>; // 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<string, Feature>(features.map((f) => [f.id, f]));
const inDegree = new Map<string, number>();
const adjacencyList = new Map<string, string[]>(); // dependencyId -> [dependentIds]
const missingDependencies = new Map<string, string[]>();
const blockedFeatures = new Map<string, string[]>();
// 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 currentDegree = inDegree.get(dependentId);
if (currentDegree === undefined) {
throw new Error(`In-degree not initialized for feature ${dependentId}`);
}
const newDegree = currentDegree - 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, Feature>): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
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: string) => {
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: string) => {
const dep = allFeatures.find((f) => f.id === depId);
return dep && dep.status !== 'completed' && dep.status !== 'verified';
});
}

View File

@@ -0,0 +1,351 @@
import { describe, it, expect } from 'vitest';
import {
resolveDependencies,
areDependenciesSatisfied,
getBlockingDependencies,
} from '../src/resolver';
import type { Feature } from '@automaker/types';
// Helper to create test features
function createFeature(
id: string,
options: {
dependencies?: string[];
status?: string;
priority?: number;
} = {}
): Feature {
return {
id,
category: 'test',
description: `Feature ${id}`,
dependencies: options.dependencies,
status: options.status || 'pending',
priority: options.priority,
};
}
describe('resolver.ts', () => {
describe('resolveDependencies', () => {
it('should handle features with no dependencies', () => {
const features = [createFeature('A'), createFeature('B'), createFeature('C')];
const result = resolveDependencies(features);
expect(result.orderedFeatures).toHaveLength(3);
expect(result.circularDependencies).toEqual([]);
expect(result.missingDependencies.size).toBe(0);
expect(result.blockedFeatures.size).toBe(0);
});
it('should order features with linear dependencies', () => {
const features = [
createFeature('C', { dependencies: ['B'] }),
createFeature('A'),
createFeature('B', { dependencies: ['A'] }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map((f) => f.id);
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C'));
expect(result.circularDependencies).toEqual([]);
});
it('should respect priority within same dependency level', () => {
const features = [
createFeature('Low', { priority: 3 }),
createFeature('High', { priority: 1 }),
createFeature('Medium', { priority: 2 }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map((f) => f.id);
expect(ids).toEqual(['High', 'Medium', 'Low']);
});
it('should use default priority 2 when not specified', () => {
const features = [
createFeature('NoPriority'),
createFeature('HighPriority', { priority: 1 }),
createFeature('LowPriority', { priority: 3 }),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map((f) => f.id);
expect(ids.indexOf('HighPriority')).toBeLessThan(ids.indexOf('NoPriority'));
expect(ids.indexOf('NoPriority')).toBeLessThan(ids.indexOf('LowPriority'));
});
it('should respect dependencies over priority', () => {
const features = [
createFeature('B', { dependencies: ['A'], priority: 1 }), // High priority but depends on A
createFeature('A', { priority: 3 }), // Low priority but no dependencies
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map((f) => f.id);
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
});
it('should detect circular dependencies (simple cycle)', () => {
const features = [
createFeature('A', { dependencies: ['B'] }),
createFeature('B', { dependencies: ['A'] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies).toHaveLength(1);
expect(result.circularDependencies[0]).toContain('A');
expect(result.circularDependencies[0]).toContain('B');
expect(result.orderedFeatures).toHaveLength(2); // All features still included
});
it('should detect circular dependencies (3-way cycle)', () => {
const features = [
createFeature('A', { dependencies: ['C'] }),
createFeature('B', { dependencies: ['A'] }),
createFeature('C', { dependencies: ['B'] }),
];
const result = resolveDependencies(features);
expect(result.circularDependencies.length).toBeGreaterThan(0);
const allCycleIds = result.circularDependencies.flat();
expect(allCycleIds).toContain('A');
expect(allCycleIds).toContain('B');
expect(allCycleIds).toContain('C');
});
it('should detect missing dependencies', () => {
const features = [createFeature('A', { dependencies: ['NonExistent'] }), createFeature('B')];
const result = resolveDependencies(features);
expect(result.missingDependencies.has('A')).toBe(true);
expect(result.missingDependencies.get('A')).toContain('NonExistent');
});
it('should detect blocked features (incomplete dependencies)', () => {
const features = [
createFeature('A', { status: 'pending' }),
createFeature('B', { dependencies: ['A'], status: 'pending' }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has('B')).toBe(true);
expect(result.blockedFeatures.get('B')).toContain('A');
});
it('should not mark features as blocked if dependencies are completed', () => {
const features = [
createFeature('A', { status: 'completed' }),
createFeature('B', { dependencies: ['A'], status: 'pending' }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has('B')).toBe(false);
});
it('should not mark features as blocked if dependencies are verified', () => {
const features = [
createFeature('A', { status: 'verified' }),
createFeature('B', { dependencies: ['A'], status: 'pending' }),
];
const result = resolveDependencies(features);
expect(result.blockedFeatures.has('B')).toBe(false);
});
it('should handle complex dependency graph', () => {
const features = [
createFeature('E', { dependencies: ['C', 'D'] }),
createFeature('D', { dependencies: ['B'] }),
createFeature('C', { dependencies: ['A', 'B'] }),
createFeature('B'),
createFeature('A'),
];
const result = resolveDependencies(features);
const ids = result.orderedFeatures.map((f) => f.id);
// A and B have no dependencies - can be first or second
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('C'));
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('C'));
expect(ids.indexOf('B')).toBeLessThan(ids.indexOf('D'));
// C depends on A and B
expect(ids.indexOf('C')).toBeLessThan(ids.indexOf('E'));
// D depends on B
expect(ids.indexOf('D')).toBeLessThan(ids.indexOf('E'));
expect(result.circularDependencies).toEqual([]);
});
it('should handle multiple missing dependencies', () => {
const features = [createFeature('A', { dependencies: ['X', 'Y', 'Z'] })];
const result = resolveDependencies(features);
expect(result.missingDependencies.get('A')).toEqual(['X', 'Y', 'Z']);
});
it('should handle empty feature list', () => {
const result = resolveDependencies([]);
expect(result.orderedFeatures).toEqual([]);
expect(result.circularDependencies).toEqual([]);
expect(result.missingDependencies.size).toBe(0);
expect(result.blockedFeatures.size).toBe(0);
});
it('should handle features with both missing and existing dependencies', () => {
const features = [
createFeature('A'),
createFeature('B', { dependencies: ['A', 'NonExistent'] }),
];
const result = resolveDependencies(features);
expect(result.missingDependencies.get('B')).toContain('NonExistent');
const ids = result.orderedFeatures.map((f) => f.id);
expect(ids.indexOf('A')).toBeLessThan(ids.indexOf('B'));
});
});
describe('areDependenciesSatisfied', () => {
it('should return true for feature with no dependencies', () => {
const feature = createFeature('A');
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it('should return true for feature with empty dependencies array', () => {
const feature = createFeature('A', { dependencies: [] });
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it('should return true when all dependencies are completed', () => {
const dep = createFeature('Dep', { status: 'completed' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it('should return true when all dependencies are verified', () => {
const dep = createFeature('Dep', { status: 'verified' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(true);
});
it('should return false when any dependency is pending', () => {
const dep = createFeature('Dep', { status: 'pending' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it('should return false when any dependency is running', () => {
const dep = createFeature('Dep', { status: 'running' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it('should return false when dependency is missing', () => {
const feature = createFeature('A', { dependencies: ['NonExistent'] });
const allFeatures = [feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
it('should check all dependencies', () => {
const dep1 = createFeature('Dep1', { status: 'completed' });
const dep2 = createFeature('Dep2', { status: 'pending' });
const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2'] });
const allFeatures = [dep1, dep2, feature];
expect(areDependenciesSatisfied(feature, allFeatures)).toBe(false);
});
});
describe('getBlockingDependencies', () => {
it('should return empty array for feature with no dependencies', () => {
const feature = createFeature('A');
const allFeatures = [feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it('should return empty array when all dependencies are completed', () => {
const dep = createFeature('Dep', { status: 'completed' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it('should return empty array when all dependencies are verified', () => {
const dep = createFeature('Dep', { status: 'verified' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual([]);
});
it('should return pending dependencies', () => {
const dep = createFeature('Dep', { status: 'pending' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
});
it('should return running dependencies', () => {
const dep = createFeature('Dep', { status: 'running' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
});
it('should return failed dependencies', () => {
const dep = createFeature('Dep', { status: 'failed' });
const feature = createFeature('A', { dependencies: ['Dep'] });
const allFeatures = [dep, feature];
expect(getBlockingDependencies(feature, allFeatures)).toEqual(['Dep']);
});
it('should return all incomplete dependencies', () => {
const dep1 = createFeature('Dep1', { status: 'pending' });
const dep2 = createFeature('Dep2', { status: 'completed' });
const dep3 = createFeature('Dep3', { status: 'running' });
const feature = createFeature('A', { dependencies: ['Dep1', 'Dep2', 'Dep3'] });
const allFeatures = [dep1, dep2, dep3, feature];
const blocking = getBlockingDependencies(feature, allFeatures);
expect(blocking).toContain('Dep1');
expect(blocking).toContain('Dep3');
expect(blocking).not.toContain('Dep2');
});
});
});

View File

@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.d.ts', 'src/index.ts'],
thresholds: {
lines: 90,
functions: 100,
branches: 85,
statements: 90,
},
},
},
});