@mcoda/core 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +9 -0
  4. package/dist/api/AgentsApi.d.ts +36 -0
  5. package/dist/api/AgentsApi.d.ts.map +1 -0
  6. package/dist/api/AgentsApi.js +176 -0
  7. package/dist/api/QaTasksApi.d.ts +8 -0
  8. package/dist/api/QaTasksApi.d.ts.map +1 -0
  9. package/dist/api/QaTasksApi.js +36 -0
  10. package/dist/api/TasksApi.d.ts +7 -0
  11. package/dist/api/TasksApi.d.ts.map +1 -0
  12. package/dist/api/TasksApi.js +34 -0
  13. package/dist/config/ConfigService.d.ts +3 -0
  14. package/dist/config/ConfigService.d.ts.map +1 -0
  15. package/dist/config/ConfigService.js +2 -0
  16. package/dist/domain/dependencies/Dependency.d.ts +3 -0
  17. package/dist/domain/dependencies/Dependency.d.ts.map +1 -0
  18. package/dist/domain/dependencies/Dependency.js +2 -0
  19. package/dist/domain/epics/Epic.d.ts +3 -0
  20. package/dist/domain/epics/Epic.d.ts.map +1 -0
  21. package/dist/domain/epics/Epic.js +2 -0
  22. package/dist/domain/projects/Project.d.ts +3 -0
  23. package/dist/domain/projects/Project.d.ts.map +1 -0
  24. package/dist/domain/projects/Project.js +2 -0
  25. package/dist/domain/tasks/Task.d.ts +3 -0
  26. package/dist/domain/tasks/Task.d.ts.map +1 -0
  27. package/dist/domain/tasks/Task.js +2 -0
  28. package/dist/domain/userStories/UserStory.d.ts +3 -0
  29. package/dist/domain/userStories/UserStory.d.ts.map +1 -0
  30. package/dist/domain/userStories/UserStory.js +2 -0
  31. package/dist/index.d.ts +28 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +27 -0
  34. package/dist/prompts/PdrPrompts.d.ts +4 -0
  35. package/dist/prompts/PdrPrompts.d.ts.map +1 -0
  36. package/dist/prompts/PdrPrompts.js +21 -0
  37. package/dist/prompts/PromptLoader.d.ts +3 -0
  38. package/dist/prompts/PromptLoader.d.ts.map +1 -0
  39. package/dist/prompts/PromptLoader.js +2 -0
  40. package/dist/prompts/SdsPrompts.d.ts +5 -0
  41. package/dist/prompts/SdsPrompts.d.ts.map +1 -0
  42. package/dist/prompts/SdsPrompts.js +44 -0
  43. package/dist/services/agents/AgentManagementService.d.ts +3 -0
  44. package/dist/services/agents/AgentManagementService.d.ts.map +1 -0
  45. package/dist/services/agents/AgentManagementService.js +2 -0
  46. package/dist/services/agents/GatewayAgentService.d.ts +92 -0
  47. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -0
  48. package/dist/services/agents/GatewayAgentService.js +870 -0
  49. package/dist/services/agents/RoutingApiClient.d.ts +23 -0
  50. package/dist/services/agents/RoutingApiClient.d.ts.map +1 -0
  51. package/dist/services/agents/RoutingApiClient.js +62 -0
  52. package/dist/services/agents/RoutingService.d.ts +50 -0
  53. package/dist/services/agents/RoutingService.d.ts.map +1 -0
  54. package/dist/services/agents/RoutingService.js +386 -0
  55. package/dist/services/agents/generated/RoutingApiClient.d.ts +21 -0
  56. package/dist/services/agents/generated/RoutingApiClient.d.ts.map +1 -0
  57. package/dist/services/agents/generated/RoutingApiClient.js +68 -0
  58. package/dist/services/backlog/BacklogService.d.ts +98 -0
  59. package/dist/services/backlog/BacklogService.d.ts.map +1 -0
  60. package/dist/services/backlog/BacklogService.js +453 -0
  61. package/dist/services/backlog/TaskOrderingService.d.ts +88 -0
  62. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -0
  63. package/dist/services/backlog/TaskOrderingService.js +675 -0
  64. package/dist/services/docs/DocsService.d.ts +82 -0
  65. package/dist/services/docs/DocsService.d.ts.map +1 -0
  66. package/dist/services/docs/DocsService.js +1631 -0
  67. package/dist/services/estimate/EstimateService.d.ts +12 -0
  68. package/dist/services/estimate/EstimateService.d.ts.map +1 -0
  69. package/dist/services/estimate/EstimateService.js +103 -0
  70. package/dist/services/estimate/VelocityService.d.ts +19 -0
  71. package/dist/services/estimate/VelocityService.d.ts.map +1 -0
  72. package/dist/services/estimate/VelocityService.js +237 -0
  73. package/dist/services/estimate/types.d.ts +30 -0
  74. package/dist/services/estimate/types.d.ts.map +1 -0
  75. package/dist/services/estimate/types.js +1 -0
  76. package/dist/services/execution/ExecutionService.d.ts +3 -0
  77. package/dist/services/execution/ExecutionService.d.ts.map +1 -0
  78. package/dist/services/execution/ExecutionService.js +2 -0
  79. package/dist/services/execution/QaFollowupService.d.ts +38 -0
  80. package/dist/services/execution/QaFollowupService.d.ts.map +1 -0
  81. package/dist/services/execution/QaFollowupService.js +236 -0
  82. package/dist/services/execution/QaProfileService.d.ts +22 -0
  83. package/dist/services/execution/QaProfileService.d.ts.map +1 -0
  84. package/dist/services/execution/QaProfileService.js +142 -0
  85. package/dist/services/execution/QaTasksService.d.ts +101 -0
  86. package/dist/services/execution/QaTasksService.d.ts.map +1 -0
  87. package/dist/services/execution/QaTasksService.js +1117 -0
  88. package/dist/services/execution/TaskSelectionService.d.ts +50 -0
  89. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -0
  90. package/dist/services/execution/TaskSelectionService.js +281 -0
  91. package/dist/services/execution/TaskStateService.d.ts +19 -0
  92. package/dist/services/execution/TaskStateService.d.ts.map +1 -0
  93. package/dist/services/execution/TaskStateService.js +59 -0
  94. package/dist/services/execution/WorkOnTasksService.d.ts +80 -0
  95. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -0
  96. package/dist/services/execution/WorkOnTasksService.js +1833 -0
  97. package/dist/services/jobs/JobInsightsService.d.ts +97 -0
  98. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -0
  99. package/dist/services/jobs/JobInsightsService.js +263 -0
  100. package/dist/services/jobs/JobResumeService.d.ts +16 -0
  101. package/dist/services/jobs/JobResumeService.d.ts.map +1 -0
  102. package/dist/services/jobs/JobResumeService.js +113 -0
  103. package/dist/services/jobs/JobService.d.ts +149 -0
  104. package/dist/services/jobs/JobService.d.ts.map +1 -0
  105. package/dist/services/jobs/JobService.js +490 -0
  106. package/dist/services/jobs/JobsApiClient.d.ts +73 -0
  107. package/dist/services/jobs/JobsApiClient.d.ts.map +1 -0
  108. package/dist/services/jobs/JobsApiClient.js +67 -0
  109. package/dist/services/openapi/OpenApiService.d.ts +54 -0
  110. package/dist/services/openapi/OpenApiService.d.ts.map +1 -0
  111. package/dist/services/openapi/OpenApiService.js +503 -0
  112. package/dist/services/planning/CreateTasksService.d.ts +68 -0
  113. package/dist/services/planning/CreateTasksService.d.ts.map +1 -0
  114. package/dist/services/planning/CreateTasksService.js +989 -0
  115. package/dist/services/planning/KeyHelpers.d.ts +5 -0
  116. package/dist/services/planning/KeyHelpers.d.ts.map +1 -0
  117. package/dist/services/planning/KeyHelpers.js +62 -0
  118. package/dist/services/planning/PlanningService.d.ts +3 -0
  119. package/dist/services/planning/PlanningService.d.ts.map +1 -0
  120. package/dist/services/planning/PlanningService.js +2 -0
  121. package/dist/services/planning/RefineTasksService.d.ts +56 -0
  122. package/dist/services/planning/RefineTasksService.d.ts.map +1 -0
  123. package/dist/services/planning/RefineTasksService.js +1328 -0
  124. package/dist/services/review/CodeReviewService.d.ts +103 -0
  125. package/dist/services/review/CodeReviewService.d.ts.map +1 -0
  126. package/dist/services/review/CodeReviewService.js +1187 -0
  127. package/dist/services/system/SystemUpdateService.d.ts +55 -0
  128. package/dist/services/system/SystemUpdateService.d.ts.map +1 -0
  129. package/dist/services/system/SystemUpdateService.js +136 -0
  130. package/dist/services/tasks/TaskApiResolver.d.ts +7 -0
  131. package/dist/services/tasks/TaskApiResolver.d.ts.map +1 -0
  132. package/dist/services/tasks/TaskApiResolver.js +41 -0
  133. package/dist/services/tasks/TaskDetailService.d.ts +106 -0
  134. package/dist/services/tasks/TaskDetailService.d.ts.map +1 -0
  135. package/dist/services/tasks/TaskDetailService.js +332 -0
  136. package/dist/services/telemetry/TelemetryService.d.ts +53 -0
  137. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -0
  138. package/dist/services/telemetry/TelemetryService.js +434 -0
  139. package/dist/workspace/WorkspaceManager.d.ts +35 -0
  140. package/dist/workspace/WorkspaceManager.d.ts.map +1 -0
  141. package/dist/workspace/WorkspaceManager.js +201 -0
  142. package/package.json +45 -0
@@ -0,0 +1,675 @@
1
+ import fs from "node:fs/promises";
2
+ import { AgentService } from "@mcoda/agents";
3
+ import { DocdexClient } from "@mcoda/integrations";
4
+ import { GlobalRepository, WorkspaceRepository, Connection } from "@mcoda/db";
5
+ import { PathHelper } from "@mcoda/shared";
6
+ import { JobService } from "../jobs/JobService.js";
7
+ import { RoutingService } from "../agents/RoutingService.js";
8
+ const DEFAULT_STATUSES = ["not_started", "in_progress", "blocked", "ready_to_review", "ready_to_qa"];
9
+ const DONE_STATUSES = new Set(["completed", "cancelled"]);
10
+ const STATUS_RANK = {
11
+ in_progress: 0,
12
+ not_started: 1,
13
+ ready_to_review: 2,
14
+ ready_to_qa: 3,
15
+ blocked: 4,
16
+ completed: 5,
17
+ cancelled: 6,
18
+ };
19
+ const hasTables = async (db, required) => {
20
+ const placeholders = required.map(() => "?").join(", ");
21
+ const rows = await db.all(`SELECT name FROM sqlite_master WHERE type = 'table' AND name IN (${placeholders})`, required);
22
+ return rows.length === required.length;
23
+ };
24
+ const normalizeStatuses = (statuses) => {
25
+ if (!statuses || statuses.length === 0)
26
+ return DEFAULT_STATUSES;
27
+ return Array.from(new Set(statuses.map((s) => s.toLowerCase().trim()).filter(Boolean)));
28
+ };
29
+ const estimateTokens = (text) => Math.max(1, Math.ceil((text ?? "").length / 4));
30
+ const SDS_DEPENDENCY_GUIDE = [
31
+ "SDS hints for dependency-aware ordering:",
32
+ "- Enforce topological ordering: never place a task before any of its dependencies.",
33
+ "- Prioritize tasks that unlock the most downstream work (direct + indirect dependents).",
34
+ "- Tie-break by existing priority, then lower story points, then older tasks, then status (in_progress before not_started).",
35
+ "- Blocked tasks should remain after unblocked tasks unless explicitly requested.",
36
+ ].join("\n");
37
+ export class TaskOrderingService {
38
+ constructor(workspace, db, repo, jobService, agentService, globalRepo, routingService, docdex, recordTelemetry) {
39
+ this.workspace = workspace;
40
+ this.db = db;
41
+ this.repo = repo;
42
+ this.jobService = jobService;
43
+ this.agentService = agentService;
44
+ this.globalRepo = globalRepo;
45
+ this.routingService = routingService;
46
+ this.docdex = docdex;
47
+ this.recordTelemetry = recordTelemetry;
48
+ }
49
+ static async create(workspace, options = {}) {
50
+ const dbPath = PathHelper.getWorkspaceDbPath(workspace.workspaceRoot);
51
+ try {
52
+ await fs.access(dbPath);
53
+ }
54
+ catch {
55
+ throw new Error(`No workspace DB found at ${dbPath}. Run mcoda create-tasks first.`);
56
+ }
57
+ const connection = await Connection.open(dbPath);
58
+ const ok = await hasTables(connection.db, ["projects", "epics", "user_stories", "tasks", "task_dependencies"]);
59
+ if (!ok) {
60
+ await connection.close();
61
+ throw new Error(`Workspace DB at ${dbPath} is missing required tables. Re-run create-tasks to seed it.`);
62
+ }
63
+ const repo = new WorkspaceRepository(connection.db, connection);
64
+ const jobService = new JobService(workspace, repo);
65
+ const globalRepo = await GlobalRepository.create();
66
+ const agentService = new AgentService(globalRepo);
67
+ const routingService = await RoutingService.create();
68
+ const docdex = new DocdexClient({
69
+ workspaceRoot: workspace.workspaceRoot,
70
+ baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
71
+ });
72
+ return new TaskOrderingService(workspace, connection.db, repo, jobService, agentService, globalRepo, routingService, docdex, options.recordTelemetry !== false);
73
+ }
74
+ async buildDocContext(projectKey, warnings) {
75
+ try {
76
+ const docs = await this.docdex.search({ docType: "SDS", projectKey });
77
+ if (!docs.length)
78
+ return undefined;
79
+ const doc = docs[0];
80
+ const segments = (doc.segments ?? []).slice(0, 3);
81
+ const body = segments.length > 0
82
+ ? segments
83
+ .map((seg, idx) => {
84
+ const head = seg.heading || `Segment ${idx + 1}`;
85
+ const trimmed = seg.content.length > 800 ? `${seg.content.slice(0, 800)}...` : seg.content;
86
+ return `### ${head}\n${trimmed}`;
87
+ })
88
+ .join("\n\n")
89
+ : doc.content ?? "";
90
+ return {
91
+ content: ["[SDS context]", doc.title ?? doc.path ?? doc.id, body].filter(Boolean).join("\n\n"),
92
+ source: doc.id ?? doc.path ?? "sds",
93
+ };
94
+ }
95
+ catch (error) {
96
+ warnings.push(`Docdex context unavailable: ${error.message}`);
97
+ return undefined;
98
+ }
99
+ }
100
+ async close() {
101
+ const maybeClose = async (target) => {
102
+ try {
103
+ if (target?.close)
104
+ await target.close();
105
+ }
106
+ catch {
107
+ /* ignore */
108
+ }
109
+ };
110
+ await maybeClose(this.repo);
111
+ await maybeClose(this.jobService);
112
+ await maybeClose(this.agentService);
113
+ await maybeClose(this.globalRepo);
114
+ await maybeClose(this.docdex);
115
+ await maybeClose(this.routingService);
116
+ }
117
+ async getProject(projectKey) {
118
+ const row = await this.db.get(`SELECT id, key, name FROM projects WHERE key = ?`, projectKey);
119
+ return row ?? undefined;
120
+ }
121
+ async getEpic(epicKey, projectId) {
122
+ const row = await this.db.get(`SELECT id, key, project_id, title FROM epics WHERE key = ? AND project_id = ?`, epicKey, projectId);
123
+ return row ?? undefined;
124
+ }
125
+ async getStory(storyKey, projectId, epicId) {
126
+ const clauses = ["key = ?", "project_id = ?"];
127
+ const params = [storyKey, projectId];
128
+ if (epicId) {
129
+ clauses.push("epic_id = ?");
130
+ params.push(epicId);
131
+ }
132
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
133
+ const row = await this.db.get(`SELECT id, key, epic_id, project_id FROM user_stories ${where}`, ...params);
134
+ return row ?? undefined;
135
+ }
136
+ async fetchTasks(projectId, epicId, statuses, storyId, assignee) {
137
+ const clauses = ["t.project_id = ?"];
138
+ const params = [projectId];
139
+ if (epicId) {
140
+ clauses.push("t.epic_id = ?");
141
+ params.push(epicId);
142
+ }
143
+ if (storyId) {
144
+ clauses.push("t.user_story_id = ?");
145
+ params.push(storyId);
146
+ }
147
+ if (assignee) {
148
+ clauses.push("LOWER(t.assignee_human) = LOWER(?)");
149
+ params.push(assignee);
150
+ }
151
+ if (statuses && statuses.length > 0) {
152
+ clauses.push(`LOWER(t.status) IN (${statuses.map(() => "?").join(", ")})`);
153
+ params.push(...statuses.map((s) => s.toLowerCase()));
154
+ }
155
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
156
+ const rows = await this.db.all(`
157
+ SELECT
158
+ t.id,
159
+ t.key,
160
+ t.title,
161
+ t.description,
162
+ t.type,
163
+ t.status,
164
+ t.story_points,
165
+ t.priority,
166
+ t.assignee_human,
167
+ t.epic_id,
168
+ e.key as epic_key,
169
+ t.user_story_id as story_id,
170
+ us.key as story_key,
171
+ us.title as story_title,
172
+ t.created_at,
173
+ t.updated_at,
174
+ t.metadata_json
175
+ FROM tasks t
176
+ JOIN epics e ON e.id = t.epic_id
177
+ JOIN user_stories us ON us.id = t.user_story_id
178
+ ${where}
179
+ ORDER BY t.created_at ASC, t.key ASC
180
+ `, ...params);
181
+ return rows.map((row) => ({
182
+ ...row,
183
+ story_points: row.story_points ?? null,
184
+ priority: row.priority ?? null,
185
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : null,
186
+ }));
187
+ }
188
+ async fetchDependencies(taskIds) {
189
+ if (taskIds.length === 0)
190
+ return new Map();
191
+ const placeholders = taskIds.map(() => "?").join(", ");
192
+ const rows = await this.db.all(`
193
+ SELECT
194
+ td.task_id,
195
+ td.depends_on_task_id,
196
+ dep.key as depends_on_key,
197
+ dep.status as depends_on_status
198
+ FROM task_dependencies td
199
+ LEFT JOIN tasks dep ON dep.id = td.depends_on_task_id
200
+ WHERE td.task_id IN (${placeholders})
201
+ `, ...taskIds);
202
+ const grouped = new Map();
203
+ for (const row of rows) {
204
+ const existing = grouped.get(row.task_id) ?? [];
205
+ existing.push(row);
206
+ grouped.set(row.task_id, existing);
207
+ }
208
+ return grouped;
209
+ }
210
+ dependencyImpactMap(dependents) {
211
+ const memo = new Map();
212
+ const visit = (taskId, stack) => {
213
+ if (memo.has(taskId))
214
+ return memo.get(taskId);
215
+ if (stack.has(taskId))
216
+ return { direct: dependents.get(taskId)?.length ?? 0, total: dependents.get(taskId)?.length ?? 0 };
217
+ stack.add(taskId);
218
+ const children = dependents.get(taskId) ?? [];
219
+ const seen = new Set();
220
+ let total = 0;
221
+ for (const child of children) {
222
+ if (seen.has(child))
223
+ continue;
224
+ seen.add(child);
225
+ total += 1;
226
+ const nested = visit(child, stack);
227
+ total += nested.total;
228
+ }
229
+ const impact = { direct: children.length, total };
230
+ memo.set(taskId, impact);
231
+ stack.delete(taskId);
232
+ return impact;
233
+ };
234
+ for (const key of dependents.keys()) {
235
+ if (!memo.has(key)) {
236
+ visit(key, new Set());
237
+ }
238
+ }
239
+ return memo;
240
+ }
241
+ compareTasks(a, b, impact, agentRank) {
242
+ const rankA = agentRank?.get(a.id);
243
+ const rankB = agentRank?.get(b.id);
244
+ if (rankA !== undefined || rankB !== undefined) {
245
+ if (rankA === undefined)
246
+ return 1;
247
+ if (rankB === undefined)
248
+ return -1;
249
+ if (rankA !== rankB)
250
+ return rankA - rankB;
251
+ }
252
+ const impactA = impact.get(a.id)?.total ?? 0;
253
+ const impactB = impact.get(b.id)?.total ?? 0;
254
+ if (impactA !== impactB)
255
+ return impactB - impactA;
256
+ const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
257
+ const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
258
+ if (priorityA !== priorityB)
259
+ return priorityA - priorityB;
260
+ const spA = a.story_points ?? Number.POSITIVE_INFINITY;
261
+ const spB = b.story_points ?? Number.POSITIVE_INFINITY;
262
+ if (spA !== spB)
263
+ return spA - spB;
264
+ const createdA = Date.parse(a.created_at) || 0;
265
+ const createdB = Date.parse(b.created_at) || 0;
266
+ if (createdA !== createdB)
267
+ return createdA - createdB;
268
+ const statusA = STATUS_RANK[a.status.toLowerCase()] ?? Number.MAX_SAFE_INTEGER;
269
+ const statusB = STATUS_RANK[b.status.toLowerCase()] ?? Number.MAX_SAFE_INTEGER;
270
+ if (statusA !== statusB)
271
+ return statusA - statusB;
272
+ return a.key.localeCompare(b.key);
273
+ }
274
+ topologicalSort(tasks, edges, impact, agentRank) {
275
+ const indegree = new Map();
276
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
277
+ for (const task of tasks) {
278
+ indegree.set(task.id, 0);
279
+ }
280
+ for (const [from, toList] of edges.entries()) {
281
+ for (const to of toList) {
282
+ if (!indegree.has(to))
283
+ continue;
284
+ indegree.set(to, (indegree.get(to) ?? 0) + 1);
285
+ }
286
+ }
287
+ const queue = tasks.filter((t) => (indegree.get(t.id) ?? 0) === 0);
288
+ const sortQueue = () => queue.sort((a, b) => this.compareTasks(a, b, impact, agentRank));
289
+ sortQueue();
290
+ const ordered = [];
291
+ const visited = new Set();
292
+ while (queue.length > 0) {
293
+ const current = queue.shift();
294
+ ordered.push(current);
295
+ visited.add(current.id);
296
+ const neighbors = edges.get(current.id) ?? [];
297
+ for (const neighbor of neighbors) {
298
+ indegree.set(neighbor, (indegree.get(neighbor) ?? 0) - 1);
299
+ if ((indegree.get(neighbor) ?? 0) === 0) {
300
+ const node = taskMap.get(neighbor);
301
+ if (node)
302
+ queue.push(node);
303
+ }
304
+ }
305
+ sortQueue();
306
+ }
307
+ const cycle = ordered.length !== tasks.length;
308
+ const cycleMembers = new Set();
309
+ if (cycle) {
310
+ for (const task of tasks) {
311
+ if (!visited.has(task.id)) {
312
+ cycleMembers.add(task.id);
313
+ }
314
+ }
315
+ const remaining = tasks.filter((t) => !visited.has(t.id));
316
+ remaining.sort((a, b) => this.compareTasks(a, b, impact, agentRank));
317
+ ordered.push(...remaining);
318
+ }
319
+ return { ordered, cycle, cycleMembers };
320
+ }
321
+ buildNodes(tasks, deps) {
322
+ const taskIds = new Set(tasks.map((t) => t.id));
323
+ const dependents = new Map();
324
+ const missingRefs = new Set();
325
+ const nodes = tasks.map((task) => {
326
+ const taskDeps = deps.get(task.id) ?? [];
327
+ const blockedBy = [];
328
+ const missing = [];
329
+ for (const dep of taskDeps) {
330
+ const status = dep.depends_on_status?.toLowerCase();
331
+ if (!dep.depends_on_task_id) {
332
+ missing.push(dep.depends_on_key ?? "unknown");
333
+ missingRefs.add(dep.depends_on_key ?? "unknown");
334
+ blockedBy.push(dep.depends_on_key ?? "unknown");
335
+ continue;
336
+ }
337
+ const inScope = taskIds.has(dep.depends_on_task_id);
338
+ const isDone = DONE_STATUSES.has(status ?? "");
339
+ if (!inScope) {
340
+ if (!isDone) {
341
+ blockedBy.push(dep.depends_on_key ?? dep.depends_on_task_id);
342
+ missing.push(dep.depends_on_key ?? dep.depends_on_task_id);
343
+ missingRefs.add(dep.depends_on_key ?? dep.depends_on_task_id);
344
+ }
345
+ continue;
346
+ }
347
+ if (!isDone) {
348
+ blockedBy.push(dep.depends_on_key ?? dep.depends_on_task_id);
349
+ }
350
+ const list = dependents.get(dep.depends_on_task_id) ?? [];
351
+ list.push(task.id);
352
+ dependents.set(dep.depends_on_task_id, list);
353
+ }
354
+ return {
355
+ ...task,
356
+ dependencies: taskDeps,
357
+ blockedBy,
358
+ missingDependencies: missing,
359
+ };
360
+ });
361
+ return { nodes, dependents, missingRefs };
362
+ }
363
+ async resolveAgent(agentName) {
364
+ const resolved = await this.routingService.resolveAgentForCommand({
365
+ workspace: this.workspace,
366
+ commandName: "order-tasks",
367
+ overrideAgentSlug: agentName,
368
+ });
369
+ return resolved.agent;
370
+ }
371
+ async invokeAgent(agent, prompt, stream, metadata) {
372
+ if (stream) {
373
+ try {
374
+ const generator = await this.agentService.invokeStream(agent.id, { input: prompt, metadata });
375
+ const collected = [];
376
+ for await (const chunk of generator) {
377
+ collected.push(chunk.output);
378
+ // eslint-disable-next-line no-console
379
+ console.log(chunk.output);
380
+ }
381
+ return { output: collected.join(""), adapter: agent.adapter };
382
+ }
383
+ catch {
384
+ // fall back to non-streaming
385
+ }
386
+ }
387
+ const result = await this.agentService.invoke(agent.id, { input: prompt, metadata });
388
+ return { output: result.output, adapter: result.adapter };
389
+ }
390
+ applyAgentRanking(ordered, agentOutput, warnings) {
391
+ try {
392
+ const parsed = JSON.parse(agentOutput);
393
+ const order = parsed.order ?? [];
394
+ const ranking = new Map();
395
+ order.forEach((entry, idx) => {
396
+ const key = entry.task_key ?? entry.key;
397
+ if (typeof key === "string") {
398
+ ranking.set(key, idx);
399
+ }
400
+ });
401
+ if (ranking.size === 0)
402
+ return undefined;
403
+ const byId = new Map(ordered.map((t) => [t.key, t.id]));
404
+ const mapped = new Map();
405
+ for (const [taskKey, idx] of ranking.entries()) {
406
+ const taskId = byId.get(taskKey);
407
+ if (taskId)
408
+ mapped.set(taskId, idx);
409
+ }
410
+ return mapped.size > 0 ? mapped : undefined;
411
+ }
412
+ catch {
413
+ warnings.push("Agent output could not be parsed; using dependency-only ordering.");
414
+ return undefined;
415
+ }
416
+ }
417
+ async persistPriorities(ordered, epicMap, storyMap) {
418
+ await this.repo.withTransaction(async () => {
419
+ for (let i = 0; i < ordered.length; i += 1) {
420
+ const task = ordered[i];
421
+ await this.repo.updateTask(task.id, { priority: i + 1 });
422
+ }
423
+ const epicEntries = Array.from(epicMap.entries()).map(([epicId, tasks]) => ({
424
+ epicId,
425
+ minPriority: Math.min(...tasks.map((t) => t.priority ?? Number.MAX_SAFE_INTEGER)),
426
+ }));
427
+ epicEntries.sort((a, b) => a.minPriority - b.minPriority);
428
+ for (let i = 0; i < epicEntries.length; i += 1) {
429
+ const entry = epicEntries[i];
430
+ await this.db.run(`UPDATE epics SET priority = ?, updated_at = ? WHERE id = ?`, i + 1, new Date().toISOString(), entry.epicId);
431
+ }
432
+ const storyEntries = Array.from(storyMap.entries()).map(([storyId, tasks]) => ({
433
+ storyId,
434
+ minPriority: Math.min(...tasks.map((t) => t.priority ?? Number.MAX_SAFE_INTEGER)),
435
+ }));
436
+ storyEntries.sort((a, b) => a.minPriority - b.minPriority);
437
+ for (let i = 0; i < storyEntries.length; i += 1) {
438
+ const entry = storyEntries[i];
439
+ await this.db.run(`UPDATE user_stories SET priority = ?, updated_at = ? WHERE id = ?`, i + 1, new Date().toISOString(), entry.storyId);
440
+ }
441
+ });
442
+ }
443
+ mapResult(ordered, blockedSet, impact, cycleMembers) {
444
+ const result = ordered.map((task, idx) => ({
445
+ taskId: task.id,
446
+ taskKey: task.key,
447
+ title: task.title,
448
+ status: task.status,
449
+ storyPoints: task.story_points,
450
+ priority: idx + 1,
451
+ epicId: task.epic_id,
452
+ epicKey: task.epic_key,
453
+ storyId: task.story_id,
454
+ storyKey: task.story_key,
455
+ storyTitle: task.story_title,
456
+ blocked: blockedSet.has(task.id),
457
+ blockedBy: task.blockedBy,
458
+ dependencyKeys: (task.dependencies ?? []).map((d) => d.depends_on_key ?? d.depends_on_task_id ?? "").filter(Boolean),
459
+ dependencyImpact: impact.get(task.id) ?? { direct: 0, total: 0 },
460
+ cycleDetected: cycleMembers.has(task.id) || undefined,
461
+ metadata: task.metadata,
462
+ }));
463
+ const blocked = result.filter((t) => t.blocked);
464
+ return { ordered: result, blocked };
465
+ }
466
+ async orderTasks(request) {
467
+ if (!request.projectKey) {
468
+ throw new Error("order-tasks requires --project <PROJECT_KEY>");
469
+ }
470
+ const statuses = normalizeStatuses(request.statusFilter);
471
+ const warnings = [];
472
+ const commandRun = this.recordTelemetry
473
+ ? await this.jobService.startCommandRun("order-tasks", request.projectKey, {
474
+ taskIds: undefined,
475
+ jobId: undefined,
476
+ gitBranch: undefined,
477
+ gitBaseBranch: undefined,
478
+ })
479
+ : undefined;
480
+ const job = this.recordTelemetry
481
+ ? await this.jobService.startJob("task_ordering", commandRun?.id, request.projectKey, {
482
+ commandName: "order-tasks",
483
+ payload: {
484
+ projectKey: request.projectKey,
485
+ epicKey: request.epicKey,
486
+ storyKey: request.storyKey,
487
+ assignee: request.assignee,
488
+ statuses,
489
+ includeBlocked: request.includeBlocked === true,
490
+ agent: request.agentName,
491
+ },
492
+ })
493
+ : undefined;
494
+ try {
495
+ const project = await this.getProject(request.projectKey);
496
+ if (!project) {
497
+ throw new Error(`Unknown project key: ${request.projectKey}`);
498
+ }
499
+ const epic = request.epicKey ? await this.getEpic(request.epicKey, project.id) : undefined;
500
+ if (request.epicKey && !epic) {
501
+ throw new Error(`Unknown epic key: ${request.epicKey} for project ${request.projectKey}`);
502
+ }
503
+ const story = request.storyKey ? await this.getStory(request.storyKey, project.id, epic?.id) : undefined;
504
+ if (request.storyKey && !story) {
505
+ throw new Error(`Unknown user story key: ${request.storyKey} for project ${request.projectKey}`);
506
+ }
507
+ const tasks = await this.fetchTasks(project.id, epic?.id, statuses, story?.id, request.assignee);
508
+ const deps = await this.fetchDependencies(tasks.map((t) => t.id));
509
+ const { nodes, dependents, missingRefs } = this.buildNodes(tasks, deps);
510
+ if (missingRefs.size > 0) {
511
+ warnings.push(`Missing dependencies referenced: ${Array.from(missingRefs).join(", ")}`);
512
+ }
513
+ const blockedSet = new Set();
514
+ for (const node of nodes) {
515
+ if (node.blockedBy.length > 0 || node.status.toLowerCase() === "blocked") {
516
+ blockedSet.add(node.id);
517
+ }
518
+ }
519
+ const impact = this.dependencyImpactMap(dependents);
520
+ const { ordered: initialOrder, cycle, cycleMembers } = this.topologicalSort(nodes, dependents, impact);
521
+ if (cycle) {
522
+ warnings.push("Dependency cycle detected; ordering may be partial.");
523
+ }
524
+ let agentRank;
525
+ const docContext = await this.buildDocContext(project.key, warnings);
526
+ if (docContext && commandRun && this.recordTelemetry) {
527
+ const contextTokens = estimateTokens(docContext.content);
528
+ await this.jobService.recordTokenUsage({
529
+ workspaceId: this.workspace.workspaceId,
530
+ projectId: project.id,
531
+ commandRunId: commandRun.id,
532
+ jobId: job?.id,
533
+ timestamp: new Date().toISOString(),
534
+ commandName: "order-tasks",
535
+ action: "docdex_context",
536
+ tokensPrompt: contextTokens,
537
+ tokensTotal: contextTokens,
538
+ metadata: { source: docContext.source },
539
+ });
540
+ }
541
+ const enableAgent = request.agentName !== undefined || this.recordTelemetry;
542
+ if (enableAgent) {
543
+ try {
544
+ const agent = await this.resolveAgent(request.agentName);
545
+ const summary = {
546
+ project: project.key,
547
+ epic: epic?.key,
548
+ statuses,
549
+ tasks: initialOrder.map((t) => ({
550
+ task_key: t.key,
551
+ title: t.title,
552
+ status: t.status,
553
+ story_points: t.story_points,
554
+ priority: t.priority,
555
+ depends_on: (t.dependencies ?? []).map((d) => d.depends_on_key ?? d.depends_on_task_id).filter(Boolean),
556
+ dependency_impact: impact.get(t.id),
557
+ })),
558
+ };
559
+ const prompt = [
560
+ "You are assisting with dependency-aware task ordering.",
561
+ "Dependencies must NEVER be violated: a task cannot appear before any of its dependencies.",
562
+ SDS_DEPENDENCY_GUIDE,
563
+ docContext ? `Doc context:\n${docContext.content}` : undefined,
564
+ "Given the current order, suggest a refined tie-break ordering (most depended-on first) and return JSON:",
565
+ `{"order":[{"task_key":"<key>","note":"optional rationale"}]}`,
566
+ "Only include task_keys from the input. Do not invent tasks.",
567
+ "If the current order is fine, return the same order.",
568
+ "Task summary:",
569
+ JSON.stringify(summary, null, 2),
570
+ ]
571
+ .filter(Boolean)
572
+ .join("\n\n");
573
+ const { output } = await this.invokeAgent(agent, prompt, request.agentStream !== false, {
574
+ command: "order-tasks",
575
+ project: project.key,
576
+ epic: epic?.key,
577
+ story: story?.key,
578
+ statuses,
579
+ includeBlocked: request.includeBlocked === true,
580
+ });
581
+ const promptTokens = estimateTokens(prompt);
582
+ const completionTokens = estimateTokens(output);
583
+ if (commandRun && this.recordTelemetry) {
584
+ await this.jobService.recordTokenUsage({
585
+ workspaceId: this.workspace.workspaceId,
586
+ projectId: project.id,
587
+ commandRunId: commandRun.id,
588
+ jobId: job?.id,
589
+ agentId: agent.id,
590
+ modelName: agent.defaultModel,
591
+ timestamp: new Date().toISOString(),
592
+ commandName: "order-tasks",
593
+ action: "ordering_tasks",
594
+ promptTokens,
595
+ completionTokens,
596
+ tokensPrompt: promptTokens,
597
+ tokensCompletion: completionTokens,
598
+ tokensTotal: promptTokens + completionTokens,
599
+ metadata: {
600
+ adapter: agent.adapter,
601
+ epicKey: epic?.key,
602
+ storyKey: story?.key,
603
+ includeBlocked: request.includeBlocked === true,
604
+ statusFilter: statuses,
605
+ agentSlug: agent.slug,
606
+ modelName: agent.defaultModel,
607
+ },
608
+ });
609
+ }
610
+ agentRank = this.applyAgentRanking(initialOrder, output, warnings);
611
+ }
612
+ catch (error) {
613
+ warnings.push(`Agent refinement skipped: ${error.message}`);
614
+ }
615
+ }
616
+ const { ordered, cycle: cycleAfterAgent, cycleMembers: agentCycleMembers } = this.topologicalSort(nodes, dependents, impact, agentRank);
617
+ const finalCycleMembers = new Set([...cycleMembers, ...agentCycleMembers]);
618
+ if (cycleAfterAgent && !cycle) {
619
+ warnings.push("Agent-influenced ordering encountered a cycle; used partial order.");
620
+ }
621
+ const blockedTasks = ordered.filter((t) => blockedSet.has(t.id));
622
+ const unblockedTasks = ordered.filter((t) => !blockedSet.has(t.id));
623
+ const prioritized = [...unblockedTasks, ...blockedTasks];
624
+ const epicMap = new Map();
625
+ const storyMap = new Map();
626
+ prioritized.forEach((task, idx) => {
627
+ task.priority = idx + 1;
628
+ const epicTasks = epicMap.get(task.epic_id) ?? [];
629
+ epicTasks.push(task);
630
+ epicMap.set(task.epic_id, epicTasks);
631
+ const storyTasks = storyMap.get(task.story_id) ?? [];
632
+ storyTasks.push(task);
633
+ storyMap.set(task.story_id, storyTasks);
634
+ });
635
+ await this.persistPriorities(prioritized, epicMap, storyMap);
636
+ const mapped = this.mapResult(prioritized, blockedSet, impact, finalCycleMembers);
637
+ const visibleOrdered = request.includeBlocked ? mapped.ordered : mapped.ordered.filter((t) => !t.blocked);
638
+ const visibleBlocked = request.includeBlocked ? [] : mapped.blocked;
639
+ if (job) {
640
+ await this.jobService.updateJobStatus(job.id, "completed", {
641
+ processedItems: mapped.ordered.length,
642
+ payload: {
643
+ warnings,
644
+ statuses,
645
+ includeBlocked: request.includeBlocked === true,
646
+ epicKey: epic?.key,
647
+ storyKey: story?.key,
648
+ },
649
+ });
650
+ }
651
+ if (commandRun) {
652
+ await this.jobService.finishCommandRun(commandRun.id, "succeeded", undefined, mapped.ordered.length);
653
+ }
654
+ return {
655
+ project,
656
+ epic,
657
+ ordered: visibleOrdered,
658
+ blocked: visibleBlocked,
659
+ warnings,
660
+ jobId: job?.id,
661
+ commandRunId: commandRun?.id,
662
+ };
663
+ }
664
+ catch (error) {
665
+ const message = error instanceof Error ? error.message : String(error);
666
+ if (job) {
667
+ await this.jobService.updateJobStatus(job.id, "failed", { errorSummary: message });
668
+ }
669
+ if (commandRun) {
670
+ await this.jobService.finishCommandRun(commandRun.id, "failed", message);
671
+ }
672
+ throw error;
673
+ }
674
+ }
675
+ }