# Task Dependencies

Autobeat supports task dependencies through a DAG (Directed Acyclic Graph) based dependency system. Tasks can depend on other tasks, and Autobeat ensures that dependent tasks only execute after their dependencies have completed.

## Architecture

### Overview

The task dependency system is built on these core components:

1. **DependencyRepository** - Persists and queries task dependencies
2. **DependencyGraph** - Validates dependency DAG and detects cycles
3. **DependencyHandler** - Event handler that manages dependency lifecycle
4. **QueueHandler** - Modified to be dependency-aware (blocks tasks with unresolved dependencies)

### Session Continuation (`continueFrom`)

Tasks can use the `continueFrom` field to receive checkpoint context from a completed dependency. When set, the dependent task's prompt is automatically enriched with the dependency's output summary, git state, and error information before execution.

```typescript
// Build task runs first
const build = await taskManager.delegate({
  prompt: 'npm run build',
  priority: Priority.P1
});

// Test task receives build's checkpoint context in its prompt
const test = await taskManager.delegate({
  prompt: 'npm test',
  dependsOn: [build.value.id],
  continueFrom: build.value.id
});
```

**Key behaviors:**
- `continueFrom` must reference a task in the `dependsOn` list (auto-added if missing)
- Uses subscribe-first pattern with 5-second timeout for race-safe checkpoint retrieval
- Supports chains: A→B→C where B receives A's context, and C receives B's (which includes A's)

**CLI:**
```bash
beat run "npm test" --depends-on task-abc123 --continue-from task-abc123
```

### Dependency-Aware Queueing

**Key Principle**: Tasks are only enqueued when all dependencies are resolved.

```
┌──────────────┐
│  Task Created│
└──────┬───────┘
       │
       ▼
┌──────────────┐
│Has Dependencies?│──No──▶┌─────────┐
└──────┬───────┘          │ Enqueue │
       │Yes               └─────────┘
       ▼
┌──────────────┐
│Check if Blocked│
└──────┬───────┘
       │
       ├─Blocked──▶┌──────────────┐
       │           │Wait for Event│
       │           └──────────────┘
       │
       └─Not Blocked──▶┌─────────┐
                       │ Enqueue │
                       └─────────┘
```

When a dependency completes:
1. `DependencyHandler` resolves the dependency
2. Checks if dependent task is now unblocked
3. Emits `TaskUnblocked` event
4. `QueueHandler` receives event and enqueues the task

### Event Flow

```
TaskDelegated
  │
  ├─▶ DependencyHandler.handleTaskDelegated()
  │     └─▶ Validates DAG (cycle detection)
  │     └─▶ Persists dependencies
  │
  └─▶ PersistenceHandler.handleTaskDelegated()
        └─▶ Persists task
        └─▶ QueueHandler.enqueueIfReady() (direct call)
              └─▶ Checks if task is blocked
                    ├─ Blocked: Skip enqueueing
                    └─ Not Blocked: Enqueue task

TaskCompleted
  │
  └─▶ DependencyHandler.handleTaskCompleted()
        └─▶ Resolves dependencies as 'completed'
        └─▶ Checks dependent tasks
              └─▶ If unblocked (all deps completed): Emits TaskUnblocked
                    │
                    └─▶ QueueHandler.handleTaskUnblocked()
                          └─▶ Enqueues unblocked task

TaskFailed/Cancelled/Timeout
  │
  └─▶ DependencyHandler.handleTaskFailed/Cancelled/Timeout()
        └─▶ Resolves dependencies as 'failed' or 'cancelled'
        └─▶ Checks dependent tasks
              └─▶ If unblocked but has failed/cancelled dep:
                    Emits TaskCancellationRequested (cascade cancellation)
```

### DAG Validation

The system uses Depth-First Search (DFS) to detect cycles before adding dependencies:

```typescript
// Pseudo-code for cycle detection
wouldCreateCycle(taskId, dependsOnTaskId):
  graph = buildGraph(existingDependencies)

  // Check if adding this edge would create a cycle
  if canReach(dependsOnTaskId, taskId, graph):
    return true  // Cycle detected!

  return false
```

**Examples:**
- ✅ `A → B → C` (valid chain)
- ✅ `A → B, A → C, B → D, C → D` (valid diamond pattern)
- ❌ `A → B → C → A` (cycle - rejected)
- ❌ `A → A` (self-dependency - rejected)

## API Reference

### Task Manager

#### `delegate(options)`

Delegate a new task with optional dependencies.

```typescript
interface TaskOptions {
  prompt: string;
  priority?: Priority;
  dependsOn?: TaskId[];  // Array of task IDs this task depends on
  // ... other options
}

// Example
await taskManager.delegate({
  prompt: 'Run tests',
  priority: Priority.P1,
  dependsOn: ['build-task-id']
});
```

### Dependency Repository

#### `addDependency(taskId, dependsOnTaskId)`

Add a dependency relationship between two tasks.

```typescript
const result = await dependencyRepo.addDependency(
  'task-b' as TaskId,
  'task-a' as TaskId
);

if (result.ok) {
  console.log('Dependency added:', result.value);
} else {
  console.error('Failed:', result.error.message);
}
```

**Returns:**
- `Result<TaskDependency>` with created dependency
- Error if cycle would be created or dependency already exists

#### `getDependencies(taskId)`

Get all tasks that a given task depends on.

```typescript
const result = await dependencyRepo.getDependencies('task-b' as TaskId);

if (result.ok) {
  for (const dep of result.value) {
    console.log(`Task ${dep.taskId} depends on ${dep.dependsOnTaskId}`);
    console.log(`Resolution: ${dep.resolution}`); // 'pending' | 'completed' | 'failed' | 'cancelled'
  }
}
```

#### `getDependents(taskId)`

Get all tasks that depend on a given task.

```typescript
const result = await dependencyRepo.getDependents('task-a' as TaskId);

if (result.ok) {
  console.log(`${result.value.length} tasks depend on task-a`);
}
```

#### `isBlocked(taskId)`

Check if a task is blocked by unresolved dependencies.

```typescript
const result = await dependencyRepo.isBlocked('task-b' as TaskId);

if (result.ok) {
  if (result.value) {
    console.log('Task is blocked - waiting for dependencies');
  } else {
    console.log('Task is ready to execute');
  }
}
```

**Note:** A task is considered blocked if it has ANY dependencies with `resolution = 'pending'`.

#### `resolveDependency(taskId, dependsOnTaskId, resolution)`

Resolve a dependency with a specific resolution state.

```typescript
await dependencyRepo.resolveDependency(
  'task-b' as TaskId,
  'task-a' as TaskId,
  'completed'  // or 'failed' | 'cancelled'
);
```

**Note:** This is typically called automatically by `DependencyHandler` when tasks complete/fail/are cancelled.

#### `getUnresolvedDependencies(taskId)`

Get only the unresolved (pending) dependencies for a task.

```typescript
const result = await dependencyRepo.getUnresolvedDependencies('task-c' as TaskId);

if (result.ok) {
  console.log(`Task has ${result.value.length} unresolved dependencies`);
}
```

#### `deleteDependencies(taskId)`

Delete all dependencies involving a task (both as dependent and dependency).

```typescript
await dependencyRepo.deleteDependencies('task-a' as TaskId);
```

**Use Cases:**
- Task cleanup
- Dependency graph modifications
- Task cancellation cascades

## Usage Examples

### Basic Dependency Chain

```typescript
// Create a build -> test -> deploy pipeline

const buildResult = await taskManager.delegate({
  prompt: 'npm run build',
  priority: Priority.P1
});

const testResult = await taskManager.delegate({
  prompt: 'npm test',
  priority: Priority.P1,
  dependsOn: [buildResult.value.id]
});

const deployResult = await taskManager.delegate({
  prompt: 'npm run deploy',
  priority: Priority.P0,
  dependsOn: [testResult.value.id]
});

// Execution order: build → test → deploy
```

### Multiple Dependencies

```typescript
// Task depends on multiple prerequisites

const lintResult = await taskManager.delegate({
  prompt: 'npm run lint',
  priority: Priority.P2
});

const formatResult = await taskManager.delegate({
  prompt: 'npm run format',
  priority: Priority.P2
});

const commitResult = await taskManager.delegate({
  prompt: 'git commit -m "Formatted and linted code"',
  priority: Priority.P1,
  dependsOn: [lintResult.value.id, formatResult.value.id]
});

// lint and format run in parallel
// commit waits for both to complete
```

### Diamond Pattern

```typescript
// Complex dependency graph:
//       A
//      / \
//     B   C
//      \ /
//       D

const taskA = await taskManager.delegate({
  prompt: 'Task A - base'
});

const taskB = await taskManager.delegate({
  prompt: 'Task B',
  dependsOn: [taskA.value.id]
});

const taskC = await taskManager.delegate({
  prompt: 'Task C',
  dependsOn: [taskA.value.id]
});

const taskD = await taskManager.delegate({
  prompt: 'Task D - final',
  dependsOn: [taskB.value.id, taskC.value.id]
});

// Execution:
// 1. A completes
// 2. B and C run in parallel
// 3. D waits for both B and C to complete
```

### Parallel Task Groups with Final Task

```typescript
// Run tests in parallel, then aggregate results

const unitTests = await taskManager.delegate({
  prompt: 'Run unit tests'
});

const integrationTests = await taskManager.delegate({
  prompt: 'Run integration tests'
});

const e2eTests = await taskManager.delegate({
  prompt: 'Run E2E tests'
});

const aggregateResults = await taskManager.delegate({
  prompt: 'Aggregate test results and generate report',
  dependsOn: [
    unitTests.value.id,
    integrationTests.value.id,
    e2eTests.value.id
  ]
});

// All tests run in parallel
// Report generation waits for all tests
```

### Error Handling

```typescript
// Dependency failures cascade automatically (v0.6.0+)

const dbMigration = await taskManager.delegate({
  prompt: 'Run database migrations'
});

const seedData = await taskManager.delegate({
  prompt: 'Seed database',
  dependsOn: [dbMigration.value.id]
});

// If dbMigration fails:
// 1. Dependency is resolved as 'failed'
// 2. seedData has no more pending dependencies
// 3. DependencyHandler detects the failed dependency
// 4. seedData is automatically cancelled (cascade cancellation)
// 5. seedData never executes

// To inspect dependency resolution after the fact:
const deps = await dependencyRepo.getDependencies(seedData.value.id);
if (deps.ok) {
  for (const dep of deps.value) {
    console.log(`Dependency ${dep.dependsOnTaskId}: ${dep.resolution}`);
    // 'completed' | 'failed' | 'cancelled'
  }
}
```

## Database Schema

```sql
CREATE TABLE task_dependencies (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  task_id TEXT NOT NULL,
  depends_on_task_id TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  resolved_at INTEGER,
  resolution TEXT NOT NULL DEFAULT 'pending',
  FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
  FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id) ON DELETE CASCADE,
  UNIQUE(task_id, depends_on_task_id)
);

-- Indexes for performance
CREATE INDEX idx_task_dependencies_task_id ON task_dependencies(task_id);
CREATE INDEX idx_task_dependencies_depends_on ON task_dependencies(depends_on_task_id);
CREATE INDEX idx_task_dependencies_resolution ON task_dependencies(resolution);
```

### TaskDependency Type

```typescript
interface TaskDependency {
  id: number;
  taskId: TaskId;
  dependsOnTaskId: TaskId;
  createdAt: number;
  resolvedAt: number | null;
  resolution: 'pending' | 'completed' | 'failed' | 'cancelled';
}
```

## Best Practices

### 1. Keep Dependency Chains Shallow

❌ **Avoid:**
```typescript
// 10-level deep chain
A → B → C → D → E → F → G → H → I → J
```

✅ **Prefer:**
```typescript
// Parallel execution with final aggregation
A, B, C, D run in parallel → Aggregate results
```

### 2. Use Priority Wisely

```typescript
// High-priority tasks should have high-priority dependencies
await taskManager.delegate({
  prompt: 'Critical deployment',
  priority: Priority.P0,
  dependsOn: [buildTaskId]  // Make sure build is also P0 or P1
});
```

### 3. Handle Dependency Failures

> **Breaking Change (v0.6.0):** Dependency failure behavior changed from "unblock and execute" to "cascade cancellation". Failed or cancelled upstream tasks now automatically cancel all downstream dependents. This replaced the v0.3.0 behavior where dependents were unblocked and expected to check resolution states manually.

#### Current Behavior (v0.6.0+)

**Important:** When a dependency fails, is cancelled, or times out, all dependent tasks are **automatically cancelled** via cascade cancellation. Dependents do not execute.

**Resolution Flow:**

```
Task A fails/is cancelled/times out
  ↓
Dependency resolved as 'failed' or 'cancelled'
  ↓
Task B (depends on A) becomes unblocked (no pending deps remain)
  ↓
DependencyHandler detects a failed/cancelled dependency in Task B's resolved deps
  ↓
Emits TaskCancellationRequested for Task B
  ↓
Task B is cancelled with reason "Dependency <taskA.id> failed"
  ↓
If Task B has its own dependents, the cascade continues recursively
```

**Example:**

```typescript
// Task A is the dependency
const taskA = await taskManager.delegate({
  prompt: 'Database migration'
});

// Task B depends on Task A
const taskB = await taskManager.delegate({
  prompt: 'Seed data',
  dependsOn: [taskA.id]
});

// If Task A fails:
// 1. Dependency is marked as resolution='failed'
// 2. Task B has no more pending dependencies (isBlocked = false)
// 3. DependencyHandler finds a failed dependency in Task B's resolved deps
// 4. TaskCancellationRequested is emitted for Task B
// 5. Task B is cancelled — it never executes
// 6. If Task C depends on Task B, it will also be cascade-cancelled
```

#### Design Rationale

Cascade cancellation (auto-cancel on upstream failure) was chosen for safety and pipeline reliability:

**Advantages:**
- Prevents executing tasks when prerequisites failed (e.g., deploying after a failed build)
- No manual resolution checking required in task code
- Recursive propagation handles deep dependency chains automatically
- Essential for scheduled pipelines where unattended execution must fail-safe

**Implementation:**
- `DependencyHandler.resolveDependencies()` checks resolved deps after unblocking
- If any dependency has `resolution === 'failed'` or `resolution === 'cancelled'`, emits `TaskCancellationRequested`
- The cancellation itself triggers further cascade for downstream dependents

#### Cascade Cancellation Propagation

**Current Behavior:** Cancelling or failing a task automatically cascades to all dependents.

```typescript
// Pipeline: A → B → C
// If Task A fails:
// 1. Task B is cascade-cancelled (reason: "Dependency <A.id> failed")
// 2. Task C is cascade-cancelled (reason: "Dependency <B.id> cancelled")
```

This is handled automatically by the `DependencyHandler` — no manual cancellation code is needed.

### 4. Avoid Circular Dependencies

The system will reject cycles, but design your workflows to avoid them:

❌ **Invalid:**
```
A depends on B
B depends on C
C depends on A  // Cycle!
```

✅ **Valid:**
```
A depends on nothing (base task)
B depends on A
C depends on B
```

### 5. Clean Up Dependencies

When tasks are no longer needed, clean up their dependencies:

```typescript
await dependencyRepo.deleteDependencies(obsoleteTaskId);
```

## Performance Considerations

1. **Prepared Statements**: All database operations use prepared statements for efficiency

2. **Indexes**: Dependencies are indexed on `task_id`, `depends_on_task_id`, and `resolution`

3. **Event-Driven**: All operations are event-driven to avoid blocking

4. **Batch Operations**: For bulk dependency operations, consider wrapping in a transaction:

```typescript
const sqliteDb = database.getDatabase();
const addMany = sqliteDb.transaction(async () => {
  for (const dep of dependencies) {
    await dependencyRepo.addDependency(dep.taskId, dep.dependsOnTaskId);
  }
});

addMany();
```

## Limitations

1. **No Conditional Dependencies**: Dependencies are binary - a task either depends on another or it doesn't. There's no support for conditional dependencies (e.g., "depend on A only if X is true").

2. **Static Dependency Graph**: Dependencies must be defined when the task is created. You cannot add dependencies to an already-running task.

3. **Resolution States**: Once a dependency is resolved (completed/failed/cancelled), it cannot be changed back to pending.

4. **No Dependency Timeout**: If a dependency never completes, the dependent task will wait indefinitely. Consider implementing task timeouts at the application level.

## Troubleshooting

### Task Not Executing

1. **Check if task is blocked:**
```typescript
const blocked = await dependencyRepo.isBlocked(taskId);
console.log('Task blocked:', blocked.value);
```

2. **Check unresolved dependencies:**
```typescript
const unresolved = await dependencyRepo.getUnresolvedDependencies(taskId);
console.log('Unresolved dependencies:', unresolved.value);
```

3. **Check if dependencies completed:**
```typescript
const deps = await dependencyRepo.getDependencies(taskId);
for (const dep of deps.value) {
  console.log(`Dependency ${dep.dependsOnTaskId}: ${dep.resolution}`);
}
```

### Cycle Detection False Positives

If you're getting cycle detection errors but believe your graph is acyclic:

1. **Visualize the dependency graph:**
```typescript
// Use findAllUnbounded() to see ALL dependencies (findAll() returns max 100)
const allDeps = await dependencyRepo.findAllUnbounded();
console.log('All dependencies:', allDeps.value);
```

2. **Check for transitive dependencies:**
   - If A → B and B → C, you cannot add C → A

### Performance Issues

If dependency operations are slow:

1. **Check database size:** `SELECT COUNT(*) FROM task_dependencies`

2. **Verify indexes exist:** Check that the three indexes are present

3. **Clean up old dependencies:** Periodically delete dependencies for completed tasks

## Implementation Details

### File Locations

- **Repository**: `src/implementations/dependency-repository.ts`
- **Handler**: `src/services/handlers/dependency-handler.ts`
- **Graph**: `src/core/dependency-graph.ts`
- **Queue Integration**: `src/services/handlers/queue-handler.ts`
- **Events**: `src/core/events/events.ts`
- **Integration Tests**: `tests/integration/task-dependencies.test.ts`
- **Unit Tests**: `tests/unit/implementations/dependency-repository.test.ts`
- **Graph Tests**: `tests/unit/core/dependency-graph.test.ts`

### Code References

- Cycle detection: `wouldCreateCycle()` in `src/core/dependency-graph.ts`
- Dependency-aware queueing: `enqueueIfReady()` in `src/services/handlers/queue-handler.ts`
- Dependency resolution: `resolveDependencies()` in `src/services/handlers/dependency-handler.ts`
- Task unblocking: `handleTaskUnblocked()` in `src/services/handlers/queue-handler.ts`
