Tm start (#1200)
Co-authored-by: Max Tuzzolino <maxtuzz@Maxs-MacBook-Pro.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Max Tuzzolino <max.tuzsmith@gmail.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
@@ -117,7 +117,7 @@ export class TaskService {
|
||||
tasks,
|
||||
total: rawTasks.length,
|
||||
filtered: filteredEntities.length,
|
||||
tag: options.tag, // Only include tag if explicitly provided
|
||||
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
|
||||
storageType: this.getStorageType()
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -219,43 +219,127 @@ export class TaskService {
|
||||
|
||||
/**
|
||||
* Get next available task to work on
|
||||
* Prioritizes eligible subtasks from in-progress parent tasks before falling back to top-level tasks
|
||||
*/
|
||||
async getNextTask(tag?: string): Promise<Task | null> {
|
||||
const result = await this.getTaskList({
|
||||
tag,
|
||||
filter: {
|
||||
status: ['pending', 'in-progress']
|
||||
status: ['pending', 'in-progress', 'done']
|
||||
}
|
||||
});
|
||||
|
||||
// Find tasks with no dependencies or all dependencies satisfied
|
||||
const completedIds = new Set(
|
||||
result.tasks.filter((t) => t.status === 'done').map((t) => t.id)
|
||||
);
|
||||
const allTasks = result.tasks;
|
||||
const priorityValues = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
|
||||
const availableTasks = result.tasks.filter((task) => {
|
||||
if (task.status === 'done' || task.status === 'blocked') {
|
||||
return false;
|
||||
// Helper to convert subtask dependencies to full dotted notation
|
||||
const toFullSubId = (
|
||||
parentId: string,
|
||||
maybeDotId: string | number
|
||||
): string => {
|
||||
if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
|
||||
return maybeDotId;
|
||||
}
|
||||
return `${parentId}.${maybeDotId}`;
|
||||
};
|
||||
|
||||
if (!task.dependencies || task.dependencies.length === 0) {
|
||||
return true;
|
||||
// Build completed IDs set (both tasks and subtasks)
|
||||
const completedIds = new Set<string>();
|
||||
allTasks.forEach((t) => {
|
||||
if (t.status === 'done') {
|
||||
completedIds.add(String(t.id));
|
||||
}
|
||||
if (Array.isArray(t.subtasks)) {
|
||||
t.subtasks.forEach((st) => {
|
||||
if (st.status === 'done') {
|
||||
completedIds.add(`${t.id}.${st.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return task.dependencies.every((depId) =>
|
||||
completedIds.has(depId.toString())
|
||||
);
|
||||
});
|
||||
|
||||
// Sort by priority
|
||||
availableTasks.sort((a, b) => {
|
||||
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
const aPriority = priorityOrder[a.priority || 'medium'];
|
||||
const bPriority = priorityOrder[b.priority || 'medium'];
|
||||
return aPriority - bPriority;
|
||||
// 1) Look for eligible subtasks from in-progress parent tasks
|
||||
const candidateSubtasks: Array<Task & { parentId?: string }> = [];
|
||||
|
||||
allTasks
|
||||
.filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
|
||||
.forEach((parent) => {
|
||||
parent.subtasks!.forEach((st) => {
|
||||
const stStatus = (st.status || 'pending').toLowerCase();
|
||||
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
|
||||
|
||||
const fullDeps =
|
||||
st.dependencies?.map((d) => toFullSubId(String(parent.id), d)) ??
|
||||
[];
|
||||
const depsSatisfied =
|
||||
fullDeps.length === 0 ||
|
||||
fullDeps.every((depId) => completedIds.has(String(depId)));
|
||||
|
||||
if (depsSatisfied) {
|
||||
candidateSubtasks.push({
|
||||
id: `${parent.id}.${st.id}`,
|
||||
title: st.title || `Subtask ${st.id}`,
|
||||
status: st.status || 'pending',
|
||||
priority: st.priority || parent.priority || 'medium',
|
||||
dependencies: fullDeps,
|
||||
parentId: String(parent.id),
|
||||
description: st.description,
|
||||
details: st.details,
|
||||
testStrategy: st.testStrategy,
|
||||
subtasks: []
|
||||
} as Task & { parentId: string });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (candidateSubtasks.length > 0) {
|
||||
// Sort by priority → dependency count → parent ID → subtask ID
|
||||
candidateSubtasks.sort((a, b) => {
|
||||
const pa =
|
||||
priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
|
||||
const pb =
|
||||
priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
if (a.dependencies!.length !== b.dependencies!.length) {
|
||||
return a.dependencies!.length - b.dependencies!.length;
|
||||
}
|
||||
|
||||
// Compare parent then subtask ID numerically
|
||||
const [aPar, aSub] = String(a.id).split('.').map(Number);
|
||||
const [bPar, bSub] = String(b.id).split('.').map(Number);
|
||||
if (aPar !== bPar) return aPar - bPar;
|
||||
return aSub - bSub;
|
||||
});
|
||||
|
||||
return candidateSubtasks[0];
|
||||
}
|
||||
|
||||
// 2) Fall back to top-level tasks (original logic)
|
||||
const eligibleTasks = allTasks.filter((task) => {
|
||||
const status = (task.status || 'pending').toLowerCase();
|
||||
if (status !== 'pending' && status !== 'in-progress') return false;
|
||||
|
||||
const deps = task.dependencies ?? [];
|
||||
return deps.every((depId) => completedIds.has(String(depId)));
|
||||
});
|
||||
|
||||
return availableTasks[0] || null;
|
||||
if (eligibleTasks.length === 0) return null;
|
||||
|
||||
// Sort by priority → dependency count → task ID
|
||||
const nextTask = eligibleTasks.sort((a, b) => {
|
||||
const pa = priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
|
||||
const pb = priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
const da = (a.dependencies ?? []).length;
|
||||
const db = (b.dependencies ?? []).length;
|
||||
if (da !== db) return da - db;
|
||||
|
||||
return Number(a.id) - Number(b.id);
|
||||
})[0];
|
||||
|
||||
return nextTask;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user