Skip to content

Run loops in sequence with dependency chains #29

@alexh

Description

@alexh

Summary

Enable "Run loop B after loop A completes" for staged workflows like implement → test → review.

Current State

Loop relationships exist for reviews:

  • parentLoopId - Links child to parent (src/core/types.ts:61)
  • reviewLoopId - Links parent to review (src/core/types.ts:60)
  • isReviewLoop - Flags review loops (src/core/types.ts:62)

Review flow: src/core/loops.ts (lines 1212-1344)

  • createReviewLoop() creates linked review
  • createFollowUpFromReview() creates follow-up from review feedback

No dependency chain system exists.

Implementation

Phase 1: Extend Loop Type

File: src/core/types.ts

Add dependency fields:

export interface Loop {
  // ... existing fields
  
  // Dependency chain fields
  dependsOn?: string;           // ID of loop that must complete first
  dependentLoops?: string[];    // IDs of loops waiting on this one
  chainId?: string;             // Group ID for chain visualization
  autoStartOnComplete?: boolean; // Auto-start when dependency completes
}

export type DependencyStatus = 'waiting' | 'ready' | 'blocked';

Phase 2: Dependency Management

File: src/core/loops.ts

Add dependency functions:

export function addDependency(
  loopId: string, 
  dependsOnId: string,
  autoStart: boolean = true
): void {
  let state = loadState();
  
  const loop = state.loops.find(l => l.id === loopId);
  const dependsOn = state.loops.find(l => l.id === dependsOnId);
  
  if (!loop || !dependsOn) throw new Error('Loop not found');
  if (dependsOn.status === 'completed') throw new Error('Cannot depend on completed loop');
  
  // Check for circular dependency
  if (wouldCreateCycle(loopId, dependsOnId, state)) {
    throw new Error('Circular dependency detected');
  }
  
  // Update dependent loop
  state = updateLoop(state, loopId, {
    dependsOn: dependsOnId,
    autoStartOnComplete: autoStart,
    status: 'queued',  // Can't run until dependency completes
  });
  
  // Update parent's dependentLoops array
  const dependents = dependsOn.dependentLoops || [];
  state = updateLoop(state, dependsOnId, {
    dependentLoops: [...dependents, loopId],
  });
  
  saveState(state);
  emit({ type: 'dependency-added', loopId, dependsOnId });
}

export function removeDependency(loopId: string): void {
  let state = loadState();
  const loop = state.loops.find(l => l.id === loopId);
  if (!loop?.dependsOn) return;
  
  const parentId = loop.dependsOn;
  const parent = state.loops.find(l => l.id === parentId);
  
  // Remove from parent's dependentLoops
  if (parent?.dependentLoops) {
    state = updateLoop(state, parentId, {
      dependentLoops: parent.dependentLoops.filter(id => id !== loopId),
    });
  }
  
  // Clear dependency on loop
  state = updateLoop(state, loopId, {
    dependsOn: undefined,
    autoStartOnComplete: undefined,
  });
  
  saveState(state);
}

function wouldCreateCycle(loopId: string, newDepId: string, state: AppState): boolean {
  // BFS to detect if newDepId eventually depends on loopId
  const visited = new Set<string>();
  const queue = [newDepId];
  
  while (queue.length > 0) {
    const current = queue.shift()!;
    if (current === loopId) return true;
    if (visited.has(current)) continue;
    visited.add(current);
    
    const loop = state.loops.find(l => l.id === current);
    if (loop?.dependsOn) queue.push(loop.dependsOn);
  }
  return false;
}

export function getDependencyStatus(loop: Loop, state: AppState): DependencyStatus {
  if (!loop.dependsOn) return 'ready';
  
  const dependency = state.loops.find(l => l.id === loop.dependsOn);
  if (!dependency) return 'ready';  // Dependency deleted
  
  if (dependency.status === 'completed') return 'ready';
  if (dependency.status === 'error' || dependency.status === 'stopped') return 'blocked';
  return 'waiting';
}

Phase 3: Auto-Start on Completion

File: src/core/loops.ts

Update finalizeLoop() to trigger dependents:

// In finalizeLoop() after setting status to 'completed':
if (loop.dependentLoops?.length) {
  for (const depId of loop.dependentLoops) {
    const depLoop = state.loops.find(l => l.id === depId);
    if (depLoop?.autoStartOnComplete && depLoop.status === 'queued') {
      // Queue auto-start (don't start synchronously)
      setTimeout(() => startLoop(depId), 100);
    }
  }
}

Phase 4: UI - Link Dependencies

File: src/index.ts

Add keybind to link loops:

// 'D' key to add dependency
loopListWindow.key('d', () => {
  if (!selectedLoopId) return;
  showDependencyModal(selectedLoopId);
});

function showDependencyModal(loopId: string): void {
  const loop = state.loops.find(l => l.id === loopId);
  if (!loop) return;
  
  // Show list of other loops to depend on
  const candidates = state.loops.filter(l => 
    l.id !== loopId && 
    l.status !== 'completed' &&
    l.dependsOn !== loopId  // Avoid immediate cycles
  );
  
  // Modal with selectable list
  // On select: addDependency(loopId, selectedDep.id)
}

Phase 5: Visual Indicators

File: src/index.ts

Update loop list formatting:

function formatLoopListItem(loop: Loop): string {
  // ... existing formatting
  
  // Add dependency indicator
  if (loop.dependsOn) {
    const depStatus = getDependencyStatus(loop, state);
    const depIcon = depStatus === 'waiting' ? '⏳' : depStatus === 'blocked' ? '🚫' : '→';
    // Prepend: "⏳ " or "→ " to show dependency status
  }
  
  // Show dependent count
  if (loop.dependentLoops?.length) {
    // Append: " +2" to show 2 loops depend on this
  }
}

Update detail pane to show chain:

// In detail pane, show dependency info:
// "Depends on: #42 Implement auth (running)"
// "Dependents: #43 Add tests, #44 Review code"

Files to Modify

  1. src/core/types.ts - Add dependency fields to Loop interface
  2. src/core/loops.ts - Add dependency functions, update finalizeLoop()
  3. src/index.ts - Add D keybind, dependency modal, visual indicators
  4. src/ui/theme.ts - Add dependency status colors

Acceptance Criteria

  • Add dependency via D key → shows modal to select parent loop
  • Dependent loop shows "waiting" status until parent completes
  • Auto-start dependent loop when parent completes successfully
  • Visual indicator showing chain status (⏳ waiting, → ready, 🚫 blocked)
  • Show "+N" badge for loops with dependents
  • Cancel chain option if parent fails/errors (don't auto-start, mark blocked)
  • Circular dependency detection prevents invalid chains
  • Detail pane shows full dependency info

Testing

  1. Create loop A, create loop B, press D on B → select A
  2. Start loop A → B should show "waiting" status
  3. Complete A → B should auto-start
  4. Create A → B → C chain, verify cascade works
  5. Error A → B should show "blocked", not auto-start
  6. Try to create B → A when A → B exists → should prevent cycle

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions