Tasks and ActionItems represent the executable work breakdown structure within a Feature.
export class Task {
readonly id: string;
readonly createdAt: Date;
readonly updatedAt: Date;
title?: string;
description?: string;
state: TaskState;
baseBranch: string;
branch: string;
dependsOn: Task[];
actionItems: ActionItem[];
}
| Property | Type | Description |
|---|---|---|
id |
string |
Unique identifier (UUID) |
title |
string? |
Concise task title (optional) |
description |
string? |
Detailed task description (optional) |
state |
TaskState |
Current execution state |
baseBranch |
string |
Base branch for the task |
branch |
string |
Working branch for the task |
dependsOn |
Task[] |
Tasks that must complete first |
actionItems |
ActionItem[] |
Granular steps within task |
createdAt |
Date |
Creation timestamp |
updatedAt |
Date |
Last update timestamp |
export class ActionItem {
readonly id: string;
readonly createdAt: Date;
readonly updatedAt: Date;
name: string;
description: string;
branch: string;
dependsOn: ActionItem[];
acceptanceCriteria: AcceptanceCriteria[];
}
| Property | Type | Description |
|---|---|---|
id |
string |
Unique identifier (UUID) |
name |
string |
Action item name |
description |
string |
Detailed description |
branch |
string |
Working branch for the action item |
dependsOn |
ActionItem[] |
Action items that must complete first |
acceptanceCriteria |
AcceptanceCriteria[] |
Criteria for completion |
createdAt |
Date |
Creation timestamp |
updatedAt |
Date |
Last update timestamp |
export type AcceptanceCriteria = BaseEntity & {
description: string;
verified: boolean;
};
| Property | Type | Description |
|---|---|---|
id |
string |
Unique identifier (UUID) |
description |
string |
Criterion description |
verified |
boolean |
Whether the criterion is verified |
export enum TaskState {
Todo = 'Todo',
WIP = 'Work in Progress',
Done = 'Done',
Review = 'Review',
}
┌──────┐
│ Todo │
└──┬───┘
│ start
▼
┌──────┐
│ WIP │
└──┬───┘
│ submit for review
▼
┌────────┐
│ Review │
└──┬─────┘
│ approve
▼
┌──────┐
│ Done │
└──────┘
Tasks can depend on other Tasks within the same Feature:
┌─────────────────────────────────────────────────────────────┐
│ Feature │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Task A │───►│ Task B │───►│ Task C │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ ▲ │
│ │ │ │
│ └───────────────────────────────┘ │
│ │
│ Task C depends on Task A AND Task B │
└─────────────────────────────────────────────────────────────┘
ActionItems can depend on other ActionItems within the same Task:
┌─────────────────────────────────────────────────────────────┐
│ Task │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ActionItem 1 │───►│ ActionItem 2 │───►│ ActionItem 3 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
// src/domain/services/dependency-validator.ts
export class DependencyValidator {
static validateTasks(tasks: Task[]): ValidationResult {
const errors: ValidationError[] = [];
// Check all dependencies exist
const taskIds = new Set(tasks.map((t) => t.id));
for (const task of tasks) {
for (const dep of task.dependsOn) {
if (!taskIds.has(dep.id)) {
errors.push({
type: 'missing_dependency',
taskId: task.id,
dependencyId: dep.id,
});
}
}
}
// Check for cycles
if (this.hasCycles(tasks)) {
errors.push({ type: 'circular_dependency' });
}
return { valid: errors.length === 0, errors };
}
static hasCycles(tasks: Task[]): boolean {
const graph = this.buildGraph(tasks);
return this.detectCycle(graph);
}
private static detectCycle(graph: Map<string, string[]>): boolean {
const visited = new Set<string>();
const recursionStack = new Set<string>();
const dfs = (nodeId: string): boolean => {
visited.add(nodeId);
recursionStack.add(nodeId);
for (const neighbor of graph.get(nodeId) ?? []) {
if (!visited.has(neighbor)) {
if (dfs(neighbor)) return true;
} else if (recursionStack.has(neighbor)) {
return true;
}
}
recursionStack.delete(nodeId);
return false;
};
for (const nodeId of graph.keys()) {
if (!visited.has(nodeId) && dfs(nodeId)) {
return true;
}
}
return false;
}
}
The execution engine determines order based on dependencies:
export class ExecutionGraph {
private tasks: Map<string, Task>;
private completed: Set<string>;
getExecutable(): Task[] {
return Array.from(this.tasks.values()).filter(
(task) =>
task.state === TaskState.Todo && task.dependsOn.every((dep) => this.completed.has(dep.id))
);
}
markCompleted(taskId: string): void {
this.completed.add(taskId);
this.unblockDependents(taskId);
}
}
class Task {
get progress(): TaskProgress {
if (this.actionItems.length === 0) {
return {
completed: this.state === TaskState.Done ? 1 : 0,
total: 1,
percentage: this.state === TaskState.Done ? 100 : 0,
};
}
const completed = this.actionItems.filter((ai) =>
ai.acceptanceCriteria.every((c) => c.verified)
).length;
return {
completed,
total: this.actionItems.length,
percentage: Math.round((completed / this.actionItems.length) * 100),
};
}
}
From the inspiration screenshot, tasks are displayed hierarchically:
┌─────────────────────────────────────────────────────────────┐
│ STORY Develop Backend Service 5 tasks │
├─────────────────────────────────────────────────────────────┤
│ □ Design and implement Event Listener Module │
│ □ Implement Event Accuracy Verification Service │
│ □ Define and implement Notification data models │
│ □ Develop Notification Dispatcher Service │
│ □ Integrate with a Message Queue │
└─────────────────────────────────────────────────────────────┘
// Create tasks during planning
const setupTask = new Task({
title: 'Setup project structure',
description: 'Initialize directories and configs',
state: TaskState.Todo,
baseBranch: 'main',
branch: 'feat/setup',
dependsOn: [],
});
const implementTask = new Task({
title: 'Implement core logic',
description: 'Build the main functionality',
state: TaskState.Todo,
baseBranch: 'main',
branch: 'feat/core',
dependsOn: [setupTask],
});
// Add action items
setupTask.actionItems = [
new ActionItem({
name: 'Create src directory',
description: 'Initialize source directory',
branch: 'feat/setup',
}),
new ActionItem({
name: 'Add tsconfig.json',
description: 'Configure TypeScript',
branch: 'feat/setup',
}),
new ActionItem({
name: 'Configure ESLint',
description: 'Setup linting rules',
branch: 'feat/setup',
}),
];
// Check executability
const graph = new ExecutionGraph([setupTask, implementTask]);
console.log(graph.getExecutable()); // [setupTask] - implementTask blocked
// Mark complete and check again
graph.markCompleted(setupTask.id);
console.log(graph.getExecutable()); // [implementTask] - now executable
Update when:
Related docs: