@mcoda/core 0.1.8 → 0.1.11

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 (216) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +2 -2
  3. package/dist/api/AgentsApi.d.ts +9 -1
  4. package/dist/api/AgentsApi.d.ts.map +1 -1
  5. package/dist/api/AgentsApi.js +201 -6
  6. package/dist/api/QaTasksApi.d.ts.map +1 -1
  7. package/dist/api/QaTasksApi.js +6 -0
  8. package/dist/api/TasksApi.d.ts.map +1 -1
  9. package/dist/api/TasksApi.js +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  14. package/dist/prompts/PdrPrompts.js +9 -1
  15. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  16. package/dist/prompts/SdsPrompts.js +9 -0
  17. package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
  18. package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
  19. package/dist/services/agents/AgentRatingFormula.js +45 -0
  20. package/dist/services/agents/AgentRatingService.d.ts +60 -0
  21. package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
  22. package/dist/services/agents/AgentRatingService.js +363 -0
  23. package/dist/services/agents/GatewayAgentService.d.ts +11 -0
  24. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
  25. package/dist/services/agents/GatewayAgentService.js +525 -84
  26. package/dist/services/agents/GatewayHandoff.d.ts +11 -0
  27. package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
  28. package/dist/services/agents/GatewayHandoff.js +141 -0
  29. package/dist/services/agents/RoutingService.d.ts +1 -0
  30. package/dist/services/agents/RoutingService.d.ts.map +1 -1
  31. package/dist/services/agents/RoutingService.js +4 -4
  32. package/dist/services/backlog/BacklogService.d.ts +23 -0
  33. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  34. package/dist/services/backlog/BacklogService.js +62 -7
  35. package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
  36. package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
  37. package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
  38. package/dist/services/backlog/TaskOrderingService.d.ts +17 -4
  39. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  40. package/dist/services/backlog/TaskOrderingService.js +538 -79
  41. package/dist/services/docs/DocInventory.d.ts +11 -0
  42. package/dist/services/docs/DocInventory.d.ts.map +1 -0
  43. package/dist/services/docs/DocInventory.js +230 -0
  44. package/dist/services/docs/DocgenRunContext.d.ts +59 -0
  45. package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
  46. package/dist/services/docs/DocgenRunContext.js +4 -0
  47. package/dist/services/docs/DocsService.d.ts +70 -3
  48. package/dist/services/docs/DocsService.d.ts.map +1 -1
  49. package/dist/services/docs/DocsService.js +1930 -89
  50. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
  51. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
  52. package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
  53. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
  54. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
  55. package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
  56. package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
  57. package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
  58. package/dist/services/docs/patch/DocPatchEngine.js +331 -0
  59. package/dist/services/docs/review/Glossary.d.ts +16 -0
  60. package/dist/services/docs/review/Glossary.d.ts.map +1 -0
  61. package/dist/services/docs/review/Glossary.js +47 -0
  62. package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
  63. package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
  64. package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
  65. package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
  66. package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
  67. package/dist/services/docs/review/ReviewReportSchema.js +47 -0
  68. package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
  69. package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
  70. package/dist/services/docs/review/ReviewTypes.js +94 -0
  71. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
  72. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
  73. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
  74. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
  75. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
  76. package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
  77. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
  78. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
  79. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
  80. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
  81. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
  82. package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
  83. package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
  84. package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
  85. package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
  86. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
  87. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
  88. package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
  89. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
  90. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
  91. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
  92. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
  93. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
  94. package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
  95. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
  96. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
  97. package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
  98. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
  99. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
  100. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
  101. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
  102. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
  103. package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
  104. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
  105. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
  106. package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
  107. package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
  108. package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
  109. package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
  110. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
  111. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
  112. package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
  113. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
  114. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
  115. package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
  116. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
  117. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
  118. package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
  119. package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
  120. package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
  121. package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
  122. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
  123. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
  124. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
  125. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
  126. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
  127. package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
  128. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
  129. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
  130. package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
  131. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
  132. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
  133. package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
  134. package/dist/services/docs/review/glossary.json +47 -0
  135. package/dist/services/estimate/EstimateService.d.ts +2 -0
  136. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  137. package/dist/services/estimate/EstimateService.js +66 -18
  138. package/dist/services/estimate/VelocityService.d.ts +4 -0
  139. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  140. package/dist/services/estimate/VelocityService.js +179 -36
  141. package/dist/services/estimate/types.d.ts +1 -0
  142. package/dist/services/estimate/types.d.ts.map +1 -1
  143. package/dist/services/execution/GatewayTrioService.d.ts +200 -0
  144. package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
  145. package/dist/services/execution/GatewayTrioService.js +2492 -0
  146. package/dist/services/execution/QaApiRunner.d.ts +30 -0
  147. package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
  148. package/dist/services/execution/QaApiRunner.js +881 -0
  149. package/dist/services/execution/QaFollowupService.d.ts +2 -0
  150. package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
  151. package/dist/services/execution/QaFollowupService.js +9 -2
  152. package/dist/services/execution/QaPlanValidator.d.ts +10 -0
  153. package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
  154. package/dist/services/execution/QaPlanValidator.js +128 -0
  155. package/dist/services/execution/QaProfileService.d.ts +27 -1
  156. package/dist/services/execution/QaProfileService.d.ts.map +1 -1
  157. package/dist/services/execution/QaProfileService.js +354 -7
  158. package/dist/services/execution/QaTasksService.d.ts +59 -1
  159. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  160. package/dist/services/execution/QaTasksService.js +3347 -318
  161. package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
  162. package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
  163. package/dist/services/execution/QaTestCommandBuilder.js +495 -0
  164. package/dist/services/execution/TaskSelectionService.d.ts +4 -2
  165. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  166. package/dist/services/execution/TaskSelectionService.js +144 -28
  167. package/dist/services/execution/TaskStateService.d.ts +19 -6
  168. package/dist/services/execution/TaskStateService.d.ts.map +1 -1
  169. package/dist/services/execution/TaskStateService.js +128 -13
  170. package/dist/services/execution/WorkOnTasksService.d.ts +32 -1
  171. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  172. package/dist/services/execution/WorkOnTasksService.js +4667 -722
  173. package/dist/services/jobs/JobInsightsService.d.ts +4 -0
  174. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
  175. package/dist/services/jobs/JobInsightsService.js +51 -5
  176. package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
  177. package/dist/services/jobs/JobResumeService.js +23 -10
  178. package/dist/services/jobs/JobService.d.ts +56 -4
  179. package/dist/services/jobs/JobService.d.ts.map +1 -1
  180. package/dist/services/jobs/JobService.js +232 -1
  181. package/dist/services/openapi/OpenApiService.d.ts +51 -0
  182. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  183. package/dist/services/openapi/OpenApiService.js +953 -106
  184. package/dist/services/planning/CreateTasksService.d.ts +21 -0
  185. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  186. package/dist/services/planning/CreateTasksService.js +569 -31
  187. package/dist/services/planning/RefineTasksService.d.ts +9 -0
  188. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  189. package/dist/services/planning/RefineTasksService.js +409 -59
  190. package/dist/services/review/CodeReviewService.d.ts +18 -0
  191. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  192. package/dist/services/review/CodeReviewService.js +1309 -167
  193. package/dist/services/review/ReviewNormalizer.d.ts +9 -0
  194. package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
  195. package/dist/services/review/ReviewNormalizer.js +147 -0
  196. package/dist/services/shared/AuthErrors.d.ts +3 -0
  197. package/dist/services/shared/AuthErrors.d.ts.map +1 -0
  198. package/dist/services/shared/AuthErrors.js +17 -0
  199. package/dist/services/shared/DocdexGuidance.d.ts +7 -0
  200. package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
  201. package/dist/services/shared/DocdexGuidance.js +12 -0
  202. package/dist/services/shared/ProjectGuidance.d.ts +17 -0
  203. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
  204. package/dist/services/shared/ProjectGuidance.js +78 -0
  205. package/dist/services/system/ToolDenylist.d.ts +13 -0
  206. package/dist/services/system/ToolDenylist.d.ts.map +1 -0
  207. package/dist/services/system/ToolDenylist.js +85 -0
  208. package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
  209. package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
  210. package/dist/services/tasks/TaskCommentFormatter.js +54 -0
  211. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
  212. package/dist/services/telemetry/TelemetryService.js +39 -7
  213. package/dist/workspace/WorkspaceManager.d.ts +26 -0
  214. package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
  215. package/dist/workspace/WorkspaceManager.js +206 -32
  216. package/package.json +6 -5
@@ -2,19 +2,21 @@ import fs from "node:fs/promises";
2
2
  import { AgentService } from "@mcoda/agents";
3
3
  import { DocdexClient } from "@mcoda/integrations";
4
4
  import { GlobalRepository, WorkspaceRepository, Connection } from "@mcoda/db";
5
- import { PathHelper } from "@mcoda/shared";
5
+ import { PathHelper, READY_TO_CODE_REVIEW, normalizeReviewStatuses } from "@mcoda/shared";
6
6
  import { JobService } from "../jobs/JobService.js";
7
7
  import { RoutingService } from "../agents/RoutingService.js";
8
- const DEFAULT_STATUSES = ["not_started", "in_progress", "blocked", "ready_to_review", "ready_to_qa"];
8
+ import { classifyTask } from "./TaskOrderingHeuristics.js";
9
+ const DEFAULT_STATUSES = ["not_started", "in_progress", "changes_requested", READY_TO_CODE_REVIEW, "ready_to_qa"];
9
10
  const DONE_STATUSES = new Set(["completed", "cancelled"]);
11
+ const DEFAULT_STAGE_ORDER = ["foundation", "backend", "frontend", "other"];
10
12
  const STATUS_RANK = {
11
13
  in_progress: 0,
14
+ changes_requested: 0,
12
15
  not_started: 1,
13
- ready_to_review: 2,
16
+ [READY_TO_CODE_REVIEW]: 2,
14
17
  ready_to_qa: 3,
15
- blocked: 4,
16
- completed: 5,
17
- cancelled: 6,
18
+ completed: 4,
19
+ cancelled: 5,
18
20
  };
19
21
  const hasTables = async (db, required) => {
20
22
  const placeholders = required.map(() => "?").join(", ");
@@ -24,7 +26,8 @@ const hasTables = async (db, required) => {
24
26
  const normalizeStatuses = (statuses) => {
25
27
  if (!statuses || statuses.length === 0)
26
28
  return DEFAULT_STATUSES;
27
- return Array.from(new Set(statuses.map((s) => s.toLowerCase().trim()).filter(Boolean)));
29
+ const normalized = Array.from(new Set(statuses.map((s) => s.toLowerCase().trim()).filter(Boolean))).filter((status) => status !== "blocked");
30
+ return normalizeReviewStatuses(normalized);
28
31
  };
29
32
  const estimateTokens = (text) => Math.max(1, Math.ceil((text ?? "").length / 4));
30
33
  const SDS_DEPENDENCY_GUIDE = [
@@ -32,8 +35,160 @@ const SDS_DEPENDENCY_GUIDE = [
32
35
  "- Enforce topological ordering: never place a task before any of its dependencies.",
33
36
  "- Prioritize tasks that unlock the most downstream work (direct + indirect dependents).",
34
37
  "- 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
38
  ].join("\n");
39
+ const extractJson = (raw) => {
40
+ if (!raw)
41
+ return undefined;
42
+ const fencedMatches = [...raw.matchAll(/```json([\s\S]*?)```/g)].map((match) => match[1]);
43
+ const stripped = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
44
+ const candidates = [...fencedMatches, stripped, raw].filter((candidate) => candidate.trim().length > 0);
45
+ for (const candidate of candidates) {
46
+ const parsed = tryParseJson(candidate);
47
+ if (parsed !== undefined)
48
+ return parsed;
49
+ }
50
+ return undefined;
51
+ };
52
+ const tryParseJson = (value) => {
53
+ try {
54
+ return JSON.parse(value);
55
+ }
56
+ catch {
57
+ // continue
58
+ }
59
+ const blocks = extractJsonBlocks(value).reverse();
60
+ for (const block of blocks) {
61
+ try {
62
+ return JSON.parse(block);
63
+ }
64
+ catch {
65
+ // continue
66
+ }
67
+ }
68
+ return undefined;
69
+ };
70
+ const extractJsonBlocks = (value) => {
71
+ const results = [];
72
+ const stack = [];
73
+ let start = -1;
74
+ let inString = false;
75
+ let escaped = false;
76
+ for (let i = 0; i < value.length; i += 1) {
77
+ const ch = value[i];
78
+ if (inString) {
79
+ if (escaped) {
80
+ escaped = false;
81
+ continue;
82
+ }
83
+ if (ch === "\\") {
84
+ escaped = true;
85
+ continue;
86
+ }
87
+ if (ch === "\"")
88
+ inString = false;
89
+ continue;
90
+ }
91
+ if (ch === "\"") {
92
+ inString = true;
93
+ continue;
94
+ }
95
+ if (ch === "{") {
96
+ if (stack.length === 0)
97
+ start = i;
98
+ stack.push("}");
99
+ continue;
100
+ }
101
+ if (ch === "[") {
102
+ if (stack.length === 0)
103
+ start = i;
104
+ stack.push("]");
105
+ continue;
106
+ }
107
+ if (stack.length > 0 && ch === stack[stack.length - 1]) {
108
+ stack.pop();
109
+ if (stack.length === 0 && start >= 0) {
110
+ results.push(value.slice(start, i + 1));
111
+ start = -1;
112
+ }
113
+ }
114
+ }
115
+ return results;
116
+ };
117
+ export const parseDependencyInferenceOutput = (output, validTaskKeys, warnings) => {
118
+ const parsed = extractJson(output);
119
+ if (!parsed) {
120
+ warnings.push("Agent dependency inference output could not be parsed; skipping.");
121
+ return [];
122
+ }
123
+ const dependencyEntries = Array.isArray(parsed)
124
+ ? parsed
125
+ : Array.isArray(parsed?.dependencies)
126
+ ? parsed.dependencies
127
+ : Array.isArray(parsed?.deps)
128
+ ? parsed.deps
129
+ : undefined;
130
+ if (!Array.isArray(dependencyEntries)) {
131
+ warnings.push("Agent dependency inference missing dependencies list; skipping.");
132
+ return [];
133
+ }
134
+ const dependenciesByTask = new Map();
135
+ let invalidTasks = 0;
136
+ let invalidDeps = 0;
137
+ let selfDeps = 0;
138
+ for (const entry of dependencyEntries) {
139
+ const taskKey = typeof entry?.task_key === "string"
140
+ ? entry.task_key
141
+ : typeof entry?.taskKey === "string"
142
+ ? entry.taskKey
143
+ : undefined;
144
+ if (!taskKey || !validTaskKeys.has(taskKey)) {
145
+ invalidTasks += 1;
146
+ continue;
147
+ }
148
+ const rawDepends = entry?.depends_on ?? entry?.dependsOn;
149
+ if (rawDepends === undefined) {
150
+ continue;
151
+ }
152
+ if (!Array.isArray(rawDepends)) {
153
+ invalidDeps += 1;
154
+ continue;
155
+ }
156
+ const dependsRaw = rawDepends;
157
+ const deps = dependenciesByTask.get(taskKey) ?? new Set();
158
+ for (const dep of dependsRaw) {
159
+ if (typeof dep !== "string") {
160
+ invalidDeps += 1;
161
+ continue;
162
+ }
163
+ if (dep === taskKey) {
164
+ selfDeps += 1;
165
+ continue;
166
+ }
167
+ if (!validTaskKeys.has(dep)) {
168
+ invalidDeps += 1;
169
+ continue;
170
+ }
171
+ deps.add(dep);
172
+ }
173
+ if (deps.size > 0) {
174
+ dependenciesByTask.set(taskKey, deps);
175
+ }
176
+ }
177
+ if (invalidTasks > 0) {
178
+ warnings.push(`Agent dependency inference ignored ${invalidTasks} invalid task keys.`);
179
+ }
180
+ if (invalidDeps > 0) {
181
+ warnings.push(`Agent dependency inference ignored ${invalidDeps} invalid dependency keys.`);
182
+ }
183
+ if (selfDeps > 0) {
184
+ warnings.push(`Agent dependency inference ignored ${selfDeps} self-dependencies.`);
185
+ }
186
+ const inferred = [];
187
+ for (const [taskKey, deps] of dependenciesByTask.entries()) {
188
+ inferred.push({ taskKey, dependsOnKeys: Array.from(deps) });
189
+ }
190
+ return inferred;
191
+ };
37
192
  export class TaskOrderingService {
38
193
  constructor(workspace, db, repo, jobService, agentService, globalRepo, routingService, docdex, recordTelemetry) {
39
194
  this.workspace = workspace;
@@ -65,9 +220,11 @@ export class TaskOrderingService {
65
220
  const globalRepo = await GlobalRepository.create();
66
221
  const agentService = new AgentService(globalRepo);
67
222
  const routingService = await RoutingService.create();
223
+ const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
68
224
  const docdex = new DocdexClient({
69
225
  workspaceRoot: workspace.workspaceRoot,
70
226
  baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
227
+ repoId: docdexRepoId,
71
228
  });
72
229
  return new TaskOrderingService(workspace, connection.db, repo, jobService, agentService, globalRepo, routingService, docdex, options.recordTelemetry !== false);
73
230
  }
@@ -207,6 +364,19 @@ export class TaskOrderingService {
207
364
  }
208
365
  return grouped;
209
366
  }
367
+ async loadMissingContext(taskIds) {
368
+ if (!taskIds.length)
369
+ return new Set();
370
+ const placeholders = taskIds.map(() => "?").join(", ");
371
+ const rows = await this.db.all(`
372
+ SELECT DISTINCT task_id
373
+ FROM task_comments
374
+ WHERE task_id IN (${placeholders})
375
+ AND LOWER(category) = 'missing_context'
376
+ AND (status IS NULL OR LOWER(status) = 'open')
377
+ `, ...taskIds);
378
+ return new Set(rows.map((row) => row.task_id));
379
+ }
210
380
  dependencyImpactMap(dependents) {
211
381
  const memo = new Map();
212
382
  const visit = (taskId, stack) => {
@@ -238,7 +408,210 @@ export class TaskOrderingService {
238
408
  }
239
409
  return memo;
240
410
  }
241
- compareTasks(a, b, impact, agentRank) {
411
+ resolveClassification(task) {
412
+ const metadata = task.metadata ?? {};
413
+ const stage = typeof metadata.stage === "string" ? metadata.stage.toLowerCase() : undefined;
414
+ const foundation = typeof metadata.foundation === "boolean" ? metadata.foundation : undefined;
415
+ if (stage && ["foundation", "backend", "frontend", "other"].includes(stage)) {
416
+ return {
417
+ stage: stage,
418
+ foundation: foundation ?? stage === "foundation",
419
+ };
420
+ }
421
+ const inferred = classifyTask({ title: task.title, description: task.description, type: task.type ?? undefined });
422
+ return { stage: inferred.stage, foundation: inferred.foundation };
423
+ }
424
+ buildDependencyGraph(tasks, deps) {
425
+ const taskIds = new Set(tasks.map((task) => task.id));
426
+ const graph = new Map();
427
+ for (const task of tasks) {
428
+ const rows = deps.get(task.id) ?? [];
429
+ const edges = new Set();
430
+ for (const dep of rows) {
431
+ if (!dep.depends_on_task_id)
432
+ continue;
433
+ if (!taskIds.has(dep.depends_on_task_id))
434
+ continue;
435
+ edges.add(dep.depends_on_task_id);
436
+ }
437
+ if (edges.size > 0) {
438
+ graph.set(task.id, edges);
439
+ }
440
+ }
441
+ return graph;
442
+ }
443
+ hasDependencyPath(graph, fromId, toId) {
444
+ if (fromId === toId)
445
+ return true;
446
+ const visited = new Set();
447
+ const stack = [fromId];
448
+ while (stack.length > 0) {
449
+ const current = stack.pop();
450
+ if (current === toId)
451
+ return true;
452
+ if (visited.has(current))
453
+ continue;
454
+ visited.add(current);
455
+ const neighbors = graph.get(current);
456
+ if (!neighbors)
457
+ continue;
458
+ for (const next of neighbors) {
459
+ if (!visited.has(next))
460
+ stack.push(next);
461
+ }
462
+ }
463
+ return false;
464
+ }
465
+ async injectFoundationDependencies(tasks, deps, warnings) {
466
+ const classification = new Map();
467
+ for (const task of tasks) {
468
+ classification.set(task.id, this.resolveClassification(task));
469
+ }
470
+ const foundationTasks = tasks.filter((task) => classification.get(task.id)?.foundation);
471
+ const nonFoundationTasks = tasks.filter((task) => !classification.get(task.id)?.foundation);
472
+ if (foundationTasks.length === 0 || nonFoundationTasks.length === 0)
473
+ return;
474
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
475
+ const dependencyGraph = this.buildDependencyGraph(tasks, deps);
476
+ const inserts = [];
477
+ let skippedCycles = 0;
478
+ const skippedEdges = [];
479
+ for (const task of nonFoundationTasks) {
480
+ const existing = new Set((deps.get(task.id) ?? [])
481
+ .map((dep) => dep.depends_on_task_id ?? "")
482
+ .filter(Boolean));
483
+ for (const foundation of foundationTasks) {
484
+ if (task.id === foundation.id)
485
+ continue;
486
+ if (existing.has(foundation.id))
487
+ continue;
488
+ if (this.hasDependencyPath(dependencyGraph, foundation.id, task.id)) {
489
+ skippedCycles += 1;
490
+ if (skippedEdges.length < 5) {
491
+ skippedEdges.push(`${task.key}->${foundation.key}`);
492
+ }
493
+ continue;
494
+ }
495
+ inserts.push({
496
+ taskId: task.id,
497
+ dependsOnTaskId: foundation.id,
498
+ relationType: "inferred_foundation",
499
+ });
500
+ existing.add(foundation.id);
501
+ const edges = dependencyGraph.get(task.id) ?? new Set();
502
+ edges.add(foundation.id);
503
+ dependencyGraph.set(task.id, edges);
504
+ }
505
+ }
506
+ if (inserts.length === 0) {
507
+ if (skippedCycles > 0) {
508
+ warnings.push(`Skipped ${skippedCycles} inferred foundation deps due to cycles.`);
509
+ if (skippedEdges.length > 0) {
510
+ warnings.push(`Skipped inferred foundation deps (cycle sample): ${skippedEdges.join(", ")}`);
511
+ }
512
+ }
513
+ return;
514
+ }
515
+ await this.repo.insertTaskDependencies(inserts, true);
516
+ for (const insert of inserts) {
517
+ const depList = deps.get(insert.taskId) ?? [];
518
+ const dependsOn = taskById.get(insert.dependsOnTaskId);
519
+ depList.push({
520
+ task_id: insert.taskId,
521
+ depends_on_task_id: insert.dependsOnTaskId,
522
+ depends_on_key: dependsOn?.key,
523
+ depends_on_status: dependsOn?.status,
524
+ });
525
+ deps.set(insert.taskId, depList);
526
+ }
527
+ warnings.push(`Injected ${inserts.length} inferred foundation deps.`);
528
+ if (skippedCycles > 0) {
529
+ warnings.push(`Skipped ${skippedCycles} inferred foundation deps due to cycles.`);
530
+ if (skippedEdges.length > 0) {
531
+ warnings.push(`Skipped inferred foundation deps (cycle sample): ${skippedEdges.join(", ")}`);
532
+ }
533
+ }
534
+ }
535
+ async applyInferredDependencies(tasks, deps, inferred, warnings) {
536
+ if (inferred.length === 0)
537
+ return;
538
+ const taskByKey = new Map(tasks.map((task) => [task.key, task]));
539
+ const dependencyGraph = this.buildDependencyGraph(tasks, deps);
540
+ const inserts = [];
541
+ let skippedCycles = 0;
542
+ const skippedEdges = [];
543
+ for (const entry of inferred) {
544
+ const task = taskByKey.get(entry.taskKey);
545
+ if (!task)
546
+ continue;
547
+ const existing = new Set((deps.get(task.id) ?? [])
548
+ .map((dep) => dep.depends_on_task_id ?? "")
549
+ .filter(Boolean));
550
+ for (const depKey of entry.dependsOnKeys) {
551
+ const dependsOn = taskByKey.get(depKey);
552
+ if (!dependsOn)
553
+ continue;
554
+ if (dependsOn.id === task.id)
555
+ continue;
556
+ if (existing.has(dependsOn.id))
557
+ continue;
558
+ if (this.hasDependencyPath(dependencyGraph, dependsOn.id, task.id)) {
559
+ skippedCycles += 1;
560
+ if (skippedEdges.length < 5) {
561
+ skippedEdges.push(`${task.key}->${dependsOn.key}`);
562
+ }
563
+ continue;
564
+ }
565
+ inserts.push({
566
+ taskId: task.id,
567
+ dependsOnTaskId: dependsOn.id,
568
+ relationType: "inferred_agent",
569
+ });
570
+ existing.add(dependsOn.id);
571
+ const edges = dependencyGraph.get(task.id) ?? new Set();
572
+ edges.add(dependsOn.id);
573
+ dependencyGraph.set(task.id, edges);
574
+ }
575
+ }
576
+ if (inserts.length === 0) {
577
+ if (skippedCycles > 0) {
578
+ warnings.push(`Skipped ${skippedCycles} inferred agent deps due to cycles.`);
579
+ if (skippedEdges.length > 0) {
580
+ warnings.push(`Skipped inferred agent deps (cycle sample): ${skippedEdges.join(", ")}`);
581
+ }
582
+ }
583
+ return;
584
+ }
585
+ await this.repo.insertTaskDependencies(inserts, true);
586
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
587
+ for (const insert of inserts) {
588
+ const depList = deps.get(insert.taskId) ?? [];
589
+ const dependsOn = taskById.get(insert.dependsOnTaskId);
590
+ depList.push({
591
+ task_id: insert.taskId,
592
+ depends_on_task_id: insert.dependsOnTaskId,
593
+ depends_on_key: dependsOn?.key,
594
+ depends_on_status: dependsOn?.status,
595
+ });
596
+ deps.set(insert.taskId, depList);
597
+ }
598
+ warnings.push(`Applied ${inserts.length} inferred agent deps.`);
599
+ if (skippedCycles > 0) {
600
+ warnings.push(`Skipped ${skippedCycles} inferred agent deps due to cycles.`);
601
+ if (skippedEdges.length > 0) {
602
+ warnings.push(`Skipped inferred agent deps (cycle sample): ${skippedEdges.join(", ")}`);
603
+ }
604
+ }
605
+ }
606
+ compareTasks(a, b, impact, agentRank, stageOrderMap) {
607
+ const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
608
+ const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
609
+ if (priorityA !== priorityB)
610
+ return priorityA - priorityB;
611
+ const impactA = impact.get(a.id)?.total ?? 0;
612
+ const impactB = impact.get(b.id)?.total ?? 0;
613
+ if (impactA !== impactB)
614
+ return impactB - impactA;
242
615
  const rankA = agentRank?.get(a.id);
243
616
  const rankB = agentRank?.get(b.id);
244
617
  if (rankA !== undefined || rankB !== undefined) {
@@ -249,14 +622,17 @@ export class TaskOrderingService {
249
622
  if (rankA !== rankB)
250
623
  return rankA - rankB;
251
624
  }
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;
625
+ const classA = this.resolveClassification(a);
626
+ const classB = this.resolveClassification(b);
627
+ if (classA.foundation !== classB.foundation) {
628
+ return classA.foundation ? -1 : 1;
629
+ }
630
+ if (stageOrderMap) {
631
+ const stageA = stageOrderMap.get(classA.stage) ?? stageOrderMap.get("other") ?? Number.MAX_SAFE_INTEGER;
632
+ const stageB = stageOrderMap.get(classB.stage) ?? stageOrderMap.get("other") ?? Number.MAX_SAFE_INTEGER;
633
+ if (stageA !== stageB)
634
+ return stageA - stageB;
635
+ }
260
636
  const spA = a.story_points ?? Number.POSITIVE_INFINITY;
261
637
  const spB = b.story_points ?? Number.POSITIVE_INFINITY;
262
638
  if (spA !== spB)
@@ -271,7 +647,7 @@ export class TaskOrderingService {
271
647
  return statusA - statusB;
272
648
  return a.key.localeCompare(b.key);
273
649
  }
274
- topologicalSort(tasks, edges, impact, agentRank) {
650
+ topologicalSort(tasks, edges, impact, agentRank, stageOrderMap) {
275
651
  const indegree = new Map();
276
652
  const taskMap = new Map(tasks.map((t) => [t.id, t]));
277
653
  for (const task of tasks) {
@@ -285,7 +661,7 @@ export class TaskOrderingService {
285
661
  }
286
662
  }
287
663
  const queue = tasks.filter((t) => (indegree.get(t.id) ?? 0) === 0);
288
- const sortQueue = () => queue.sort((a, b) => this.compareTasks(a, b, impact, agentRank));
664
+ const sortQueue = () => queue.sort((a, b) => this.compareTasks(a, b, impact, agentRank, stageOrderMap));
289
665
  sortQueue();
290
666
  const ordered = [];
291
667
  const visited = new Set();
@@ -313,7 +689,7 @@ export class TaskOrderingService {
313
689
  }
314
690
  }
315
691
  const remaining = tasks.filter((t) => !visited.has(t.id));
316
- remaining.sort((a, b) => this.compareTasks(a, b, impact, agentRank));
692
+ remaining.sort((a, b) => this.compareTasks(a, b, impact, agentRank, stageOrderMap));
317
693
  ordered.push(...remaining);
318
694
  }
319
695
  return { ordered, cycle, cycleMembers };
@@ -324,29 +700,23 @@ export class TaskOrderingService {
324
700
  const missingRefs = new Set();
325
701
  const nodes = tasks.map((task) => {
326
702
  const taskDeps = deps.get(task.id) ?? [];
327
- const blockedBy = [];
328
703
  const missing = [];
329
704
  for (const dep of taskDeps) {
330
705
  const status = dep.depends_on_status?.toLowerCase();
331
706
  if (!dep.depends_on_task_id) {
332
707
  missing.push(dep.depends_on_key ?? "unknown");
333
708
  missingRefs.add(dep.depends_on_key ?? "unknown");
334
- blockedBy.push(dep.depends_on_key ?? "unknown");
335
709
  continue;
336
710
  }
337
711
  const inScope = taskIds.has(dep.depends_on_task_id);
338
712
  const isDone = DONE_STATUSES.has(status ?? "");
339
713
  if (!inScope) {
340
714
  if (!isDone) {
341
- blockedBy.push(dep.depends_on_key ?? dep.depends_on_task_id);
342
715
  missing.push(dep.depends_on_key ?? dep.depends_on_task_id);
343
716
  missingRefs.add(dep.depends_on_key ?? dep.depends_on_task_id);
344
717
  }
345
718
  continue;
346
719
  }
347
- if (!isDone) {
348
- blockedBy.push(dep.depends_on_key ?? dep.depends_on_task_id);
349
- }
350
720
  const list = dependents.get(dep.depends_on_task_id) ?? [];
351
721
  list.push(task.id);
352
722
  dependents.set(dep.depends_on_task_id, list);
@@ -354,7 +724,6 @@ export class TaskOrderingService {
354
724
  return {
355
725
  ...task,
356
726
  dependencies: taskDeps,
357
- blockedBy,
358
727
  missingDependencies: missing,
359
728
  };
360
729
  });
@@ -388,12 +757,20 @@ export class TaskOrderingService {
388
757
  return { output: result.output, adapter: result.adapter };
389
758
  }
390
759
  applyAgentRanking(ordered, agentOutput, warnings) {
760
+ const parsed = extractJson(agentOutput);
761
+ if (!parsed) {
762
+ warnings.push("Agent output could not be parsed; using dependency-only ordering.");
763
+ return undefined;
764
+ }
765
+ const order = Array.isArray(parsed) ? parsed : parsed.order;
766
+ if (!Array.isArray(order)) {
767
+ warnings.push("Agent output missing order list; using dependency-only ordering.");
768
+ return undefined;
769
+ }
391
770
  try {
392
- const parsed = JSON.parse(agentOutput);
393
- const order = parsed.order ?? [];
394
771
  const ranking = new Map();
395
772
  order.forEach((entry, idx) => {
396
- const key = entry.task_key ?? entry.key;
773
+ const key = typeof entry === "string" ? entry : entry.task_key ?? entry.taskKey ?? entry.key;
397
774
  if (typeof key === "string") {
398
775
  ranking.set(key, idx);
399
776
  }
@@ -414,6 +791,45 @@ export class TaskOrderingService {
414
791
  return undefined;
415
792
  }
416
793
  }
794
+ async inferDependenciesWithAgent(agent, tasks, context) {
795
+ const summary = {
796
+ project: context.project.key,
797
+ epic: context.epic?.key,
798
+ story: context.story?.key,
799
+ tasks: tasks.map((task) => ({
800
+ task_key: task.key,
801
+ epic_key: task.epic_key,
802
+ story_key: task.story_key,
803
+ title: task.title,
804
+ description: task.description,
805
+ type: task.type,
806
+ depends_on: (task.dependencies ?? [])
807
+ .map((dep) => dep.depends_on_key ?? dep.depends_on_task_id)
808
+ .filter(Boolean),
809
+ })),
810
+ };
811
+ const prompt = [
812
+ "You are inferring dependencies across epics, stories, and tasks.",
813
+ "Return ONLY JSON matching:",
814
+ `{"dependencies":[{"task_key":"<key>","depends_on":["<key>"]}]}`,
815
+ "Only include task_key values from the input.",
816
+ "Do not add self-dependencies. Omit empty depends_on arrays.",
817
+ context.docContext ? `Doc context:\n${context.docContext.content}` : undefined,
818
+ "Task summary:",
819
+ JSON.stringify(summary, null, 2),
820
+ ]
821
+ .filter(Boolean)
822
+ .join("\n\n");
823
+ const { output } = await this.invokeAgent(agent, prompt, context.stream, {
824
+ command: "order-tasks",
825
+ phase: "infer_dependencies",
826
+ project: context.project.key,
827
+ epic: context.epic?.key,
828
+ story: context.story?.key,
829
+ });
830
+ const taskKeys = new Set(tasks.map((task) => task.key));
831
+ return parseDependencyInferenceOutput(output, taskKeys, context.warnings);
832
+ }
417
833
  async persistPriorities(ordered, epicMap, storyMap) {
418
834
  await this.repo.withTransaction(async () => {
419
835
  for (let i = 0; i < ordered.length; i += 1) {
@@ -440,7 +856,7 @@ export class TaskOrderingService {
440
856
  }
441
857
  });
442
858
  }
443
- mapResult(ordered, blockedSet, impact, cycleMembers) {
859
+ mapResult(ordered, impact, cycleMembers) {
444
860
  const result = ordered.map((task, idx) => ({
445
861
  taskId: task.id,
446
862
  taskKey: task.key,
@@ -453,15 +869,12 @@ export class TaskOrderingService {
453
869
  storyId: task.story_id,
454
870
  storyKey: task.story_key,
455
871
  storyTitle: task.story_title,
456
- blocked: blockedSet.has(task.id),
457
- blockedBy: task.blockedBy,
458
872
  dependencyKeys: (task.dependencies ?? []).map((d) => d.depends_on_key ?? d.depends_on_task_id ?? "").filter(Boolean),
459
873
  dependencyImpact: impact.get(task.id) ?? { direct: 0, total: 0 },
460
874
  cycleDetected: cycleMembers.has(task.id) || undefined,
461
875
  metadata: task.metadata,
462
876
  }));
463
- const blocked = result.filter((t) => t.blocked);
464
- return { ordered: result, blocked };
877
+ return { ordered: result };
465
878
  }
466
879
  async orderTasks(request) {
467
880
  if (!request.projectKey) {
@@ -469,6 +882,9 @@ export class TaskOrderingService {
469
882
  }
470
883
  const statuses = normalizeStatuses(request.statusFilter);
471
884
  const warnings = [];
885
+ if (request.statusFilter?.some((status) => status.toLowerCase().trim() === "blocked")) {
886
+ warnings.push("Status 'blocked' is no longer supported; ignoring it in order-tasks.");
887
+ }
472
888
  const commandRun = this.recordTelemetry
473
889
  ? await this.jobService.startCommandRun("order-tasks", request.projectKey, {
474
890
  taskIds: undefined,
@@ -486,7 +902,6 @@ export class TaskOrderingService {
486
902
  storyKey: request.storyKey,
487
903
  assignee: request.assignee,
488
904
  statuses,
489
- includeBlocked: request.includeBlocked === true,
490
905
  agent: request.agentName,
491
906
  },
492
907
  })
@@ -506,42 +921,89 @@ export class TaskOrderingService {
506
921
  }
507
922
  const tasks = await this.fetchTasks(project.id, epic?.id, statuses, story?.id, request.assignee);
508
923
  const deps = await this.fetchDependencies(tasks.map((t) => t.id));
509
- const { nodes, dependents, missingRefs } = this.buildNodes(tasks, deps);
924
+ if (request.injectFoundationDeps !== false) {
925
+ await this.injectFoundationDependencies(tasks, deps, warnings);
926
+ }
927
+ let { nodes, dependents, missingRefs } = this.buildNodes(tasks, deps);
928
+ const enableAgentRanking = Boolean(request.agentName);
929
+ const enableInference = request.inferDependencies === true;
930
+ const useAgent = enableAgentRanking || enableInference;
931
+ const agentStream = request.agentStream !== false;
932
+ let docContext;
933
+ if (useAgent) {
934
+ docContext = await this.buildDocContext(project.key, warnings);
935
+ if (docContext && commandRun && this.recordTelemetry) {
936
+ const contextTokens = estimateTokens(docContext.content);
937
+ await this.jobService.recordTokenUsage({
938
+ workspaceId: this.workspace.workspaceId,
939
+ projectId: project.id,
940
+ commandRunId: commandRun.id,
941
+ jobId: job?.id,
942
+ timestamp: new Date().toISOString(),
943
+ commandName: "order-tasks",
944
+ action: "docdex_context",
945
+ tokensPrompt: contextTokens,
946
+ tokensTotal: contextTokens,
947
+ metadata: { source: docContext.source },
948
+ });
949
+ }
950
+ }
951
+ let resolvedAgent;
952
+ if (useAgent) {
953
+ try {
954
+ resolvedAgent = await this.resolveAgent(request.agentName);
955
+ }
956
+ catch (error) {
957
+ warnings.push(`Agent resolution failed: ${error.message}`);
958
+ }
959
+ }
960
+ if (enableInference && resolvedAgent) {
961
+ try {
962
+ const inferred = await this.inferDependenciesWithAgent(resolvedAgent, nodes, {
963
+ project,
964
+ epic,
965
+ story,
966
+ docContext,
967
+ stream: agentStream,
968
+ warnings,
969
+ });
970
+ await this.applyInferredDependencies(tasks, deps, inferred, warnings);
971
+ ({ nodes, dependents, missingRefs } = this.buildNodes(tasks, deps));
972
+ }
973
+ catch (error) {
974
+ warnings.push(`Dependency inference skipped: ${error.message}`);
975
+ }
976
+ }
977
+ else if (enableInference && !resolvedAgent) {
978
+ warnings.push("Dependency inference skipped: no agent resolved.");
979
+ }
510
980
  if (missingRefs.size > 0) {
511
981
  warnings.push(`Missing dependencies referenced: ${Array.from(missingRefs).join(", ")}`);
512
982
  }
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);
983
+ const missingContext = await this.loadMissingContext(nodes.map((node) => node.id));
984
+ if (missingContext.size > 0) {
985
+ warnings.push(`Tasks with open missing_context comments: ${Array.from(missingContext).length}`);
986
+ }
987
+ const stageOrder = (request.stageOrder && request.stageOrder.length > 0
988
+ ? request.stageOrder
989
+ : DEFAULT_STAGE_ORDER);
990
+ const stageOrderMap = new Map();
991
+ for (const [idx, stage] of stageOrder.entries()) {
992
+ if (["foundation", "backend", "frontend", "other"].includes(stage)) {
993
+ stageOrderMap.set(stage, idx);
517
994
  }
518
995
  }
996
+ if (stageOrderMap.size === 0) {
997
+ DEFAULT_STAGE_ORDER.forEach((stage, idx) => stageOrderMap.set(stage, idx));
998
+ }
519
999
  const impact = this.dependencyImpactMap(dependents);
520
- const { ordered: initialOrder, cycle, cycleMembers } = this.topologicalSort(nodes, dependents, impact);
1000
+ const { ordered: initialOrder, cycle, cycleMembers } = this.topologicalSort(nodes, dependents, impact, undefined, stageOrderMap);
521
1001
  if (cycle) {
522
1002
  warnings.push("Dependency cycle detected; ordering may be partial.");
523
1003
  }
524
1004
  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) {
1005
+ if (enableAgentRanking && resolvedAgent) {
543
1006
  try {
544
- const agent = await this.resolveAgent(request.agentName);
545
1007
  const summary = {
546
1008
  project: project.key,
547
1009
  epic: epic?.key,
@@ -570,13 +1032,12 @@ export class TaskOrderingService {
570
1032
  ]
571
1033
  .filter(Boolean)
572
1034
  .join("\n\n");
573
- const { output } = await this.invokeAgent(agent, prompt, request.agentStream !== false, {
1035
+ const { output } = await this.invokeAgent(resolvedAgent, prompt, agentStream, {
574
1036
  command: "order-tasks",
575
1037
  project: project.key,
576
1038
  epic: epic?.key,
577
1039
  story: story?.key,
578
1040
  statuses,
579
- includeBlocked: request.includeBlocked === true,
580
1041
  });
581
1042
  const promptTokens = estimateTokens(prompt);
582
1043
  const completionTokens = estimateTokens(output);
@@ -586,8 +1047,8 @@ export class TaskOrderingService {
586
1047
  projectId: project.id,
587
1048
  commandRunId: commandRun.id,
588
1049
  jobId: job?.id,
589
- agentId: agent.id,
590
- modelName: agent.defaultModel,
1050
+ agentId: resolvedAgent.id,
1051
+ modelName: resolvedAgent.defaultModel,
591
1052
  timestamp: new Date().toISOString(),
592
1053
  commandName: "order-tasks",
593
1054
  action: "ordering_tasks",
@@ -597,13 +1058,14 @@ export class TaskOrderingService {
597
1058
  tokensCompletion: completionTokens,
598
1059
  tokensTotal: promptTokens + completionTokens,
599
1060
  metadata: {
600
- adapter: agent.adapter,
1061
+ adapter: resolvedAgent.adapter,
601
1062
  epicKey: epic?.key,
602
1063
  storyKey: story?.key,
603
- includeBlocked: request.includeBlocked === true,
604
1064
  statusFilter: statuses,
605
- agentSlug: agent.slug,
606
- modelName: agent.defaultModel,
1065
+ agentSlug: resolvedAgent.slug,
1066
+ modelName: resolvedAgent.defaultModel,
1067
+ phase: "agent_ordering",
1068
+ attempt: 1,
607
1069
  },
608
1070
  });
609
1071
  }
@@ -613,14 +1075,15 @@ export class TaskOrderingService {
613
1075
  warnings.push(`Agent refinement skipped: ${error.message}`);
614
1076
  }
615
1077
  }
616
- const { ordered, cycle: cycleAfterAgent, cycleMembers: agentCycleMembers } = this.topologicalSort(nodes, dependents, impact, agentRank);
1078
+ else if (enableAgentRanking && !resolvedAgent) {
1079
+ warnings.push("Agent refinement skipped: no agent resolved.");
1080
+ }
1081
+ const { ordered, cycle: cycleAfterAgent, cycleMembers: agentCycleMembers } = this.topologicalSort(nodes, dependents, impact, agentRank, stageOrderMap);
617
1082
  const finalCycleMembers = new Set([...cycleMembers, ...agentCycleMembers]);
618
1083
  if (cycleAfterAgent && !cycle) {
619
1084
  warnings.push("Agent-influenced ordering encountered a cycle; used partial order.");
620
1085
  }
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];
1086
+ const prioritized = ordered;
624
1087
  const epicMap = new Map();
625
1088
  const storyMap = new Map();
626
1089
  prioritized.forEach((task, idx) => {
@@ -633,16 +1096,13 @@ export class TaskOrderingService {
633
1096
  storyMap.set(task.story_id, storyTasks);
634
1097
  });
635
1098
  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;
1099
+ const mapped = this.mapResult(prioritized, impact, finalCycleMembers);
639
1100
  if (job) {
640
1101
  await this.jobService.updateJobStatus(job.id, "completed", {
641
1102
  processedItems: mapped.ordered.length,
642
1103
  payload: {
643
1104
  warnings,
644
1105
  statuses,
645
- includeBlocked: request.includeBlocked === true,
646
1106
  epicKey: epic?.key,
647
1107
  storyKey: story?.key,
648
1108
  },
@@ -654,8 +1114,7 @@ export class TaskOrderingService {
654
1114
  return {
655
1115
  project,
656
1116
  epic,
657
- ordered: visibleOrdered,
658
- blocked: visibleBlocked,
1117
+ ordered: mapped.ordered,
659
1118
  warnings,
660
1119
  jobId: job?.id,
661
1120
  commandRunId: commandRun?.id,