@mcoda/core 0.1.9 → 0.1.12
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.
- package/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +1 -0
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +136 -11
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +6 -0
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +7 -0
- package/dist/services/agents/AgentRatingService.d.ts +19 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -1
- package/dist/services/agents/AgentRatingService.js +66 -2
- package/dist/services/agents/GatewayAgentService.d.ts +8 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +462 -65
- package/dist/services/agents/GatewayHandoff.d.ts +5 -1
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -1
- package/dist/services/agents/GatewayHandoff.js +65 -32
- package/dist/services/agents/RoutingService.d.ts +1 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -1
- package/dist/services/agents/RoutingService.js +4 -4
- package/dist/services/backlog/BacklogService.d.ts +23 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +62 -7
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +16 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +529 -73
- package/dist/services/docs/DocInventory.d.ts +11 -0
- package/dist/services/docs/DocInventory.d.ts.map +1 -0
- package/dist/services/docs/DocInventory.js +230 -0
- package/dist/services/docs/DocgenRunContext.d.ts +59 -0
- package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
- package/dist/services/docs/DocgenRunContext.js +4 -0
- package/dist/services/docs/DocsService.d.ts +59 -2
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1701 -48
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
- package/dist/services/docs/patch/DocPatchEngine.js +331 -0
- package/dist/services/docs/review/Glossary.d.ts +16 -0
- package/dist/services/docs/review/Glossary.d.ts.map +1 -0
- package/dist/services/docs/review/Glossary.js +47 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportSchema.js +47 -0
- package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
- package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewTypes.js +94 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
- package/dist/services/docs/review/glossary.json +47 -0
- package/dist/services/estimate/EstimateService.d.ts +2 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -1
- package/dist/services/estimate/EstimateService.js +66 -18
- package/dist/services/estimate/VelocityService.d.ts +4 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -1
- package/dist/services/estimate/VelocityService.js +179 -36
- package/dist/services/estimate/types.d.ts +1 -0
- package/dist/services/estimate/types.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.d.ts +71 -4
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.js +1695 -328
- package/dist/services/execution/QaApiRunner.d.ts +30 -0
- package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
- package/dist/services/execution/QaApiRunner.js +881 -0
- package/dist/services/execution/QaFollowupService.d.ts +1 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +8 -2
- package/dist/services/execution/QaPlanValidator.d.ts +10 -0
- package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
- package/dist/services/execution/QaPlanValidator.js +128 -0
- package/dist/services/execution/QaProfileService.d.ts +21 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +214 -29
- package/dist/services/execution/QaTasksService.d.ts +41 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +2851 -500
- package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
- package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
- package/dist/services/execution/QaTestCommandBuilder.js +495 -0
- package/dist/services/execution/TaskSelectionService.d.ts +4 -2
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +144 -28
- package/dist/services/execution/TaskStateService.d.ts +19 -6
- package/dist/services/execution/TaskStateService.d.ts.map +1 -1
- package/dist/services/execution/TaskStateService.js +128 -13
- package/dist/services/execution/WorkOnTasksService.d.ts +19 -2
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +3913 -1225
- package/dist/services/jobs/JobInsightsService.d.ts +4 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
- package/dist/services/jobs/JobInsightsService.js +51 -5
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
- package/dist/services/jobs/JobResumeService.js +23 -10
- package/dist/services/jobs/JobService.d.ts +56 -4
- package/dist/services/jobs/JobService.d.ts.map +1 -1
- package/dist/services/jobs/JobService.js +232 -1
- package/dist/services/openapi/OpenApiService.d.ts +41 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +889 -98
- package/dist/services/planning/CreateTasksService.d.ts +15 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +311 -6
- package/dist/services/planning/RefineTasksService.d.ts +4 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +225 -24
- package/dist/services/review/CodeReviewService.d.ts +4 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +778 -232
- package/dist/services/review/ReviewNormalizer.d.ts +9 -0
- package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
- package/dist/services/review/ReviewNormalizer.js +147 -0
- package/dist/services/shared/AuthErrors.d.ts +3 -0
- package/dist/services/shared/AuthErrors.d.ts.map +1 -0
- package/dist/services/shared/AuthErrors.js +17 -0
- package/dist/services/shared/DocdexGuidance.d.ts +7 -0
- package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
- package/dist/services/shared/DocdexGuidance.js +12 -0
- package/dist/services/shared/ProjectGuidance.d.ts +12 -1
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +64 -7
- package/dist/services/system/ToolDenylist.d.ts +13 -0
- package/dist/services/system/ToolDenylist.d.ts.map +1 -0
- package/dist/services/system/ToolDenylist.js +85 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/services/telemetry/TelemetryService.js +39 -7
- package/dist/workspace/WorkspaceManager.d.ts +22 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +203 -32
- 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
|
-
|
|
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
|
-
|
|
16
|
+
[READY_TO_CODE_REVIEW]: 2,
|
|
14
17
|
ready_to_qa: 3,
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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,
|
|
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
|
-
|
|
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,25 +921,16 @@ 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
|
-
|
|
510
|
-
|
|
511
|
-
warnings.push(`Missing dependencies referenced: ${Array.from(missingRefs).join(", ")}`);
|
|
924
|
+
if (request.injectFoundationDeps !== false) {
|
|
925
|
+
await this.injectFoundationDependencies(tasks, deps, warnings);
|
|
512
926
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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 enableAgent = Boolean(request.agentName);
|
|
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;
|
|
526
932
|
let docContext;
|
|
527
|
-
if (
|
|
933
|
+
if (useAgent) {
|
|
528
934
|
docContext = await this.buildDocContext(project.key, warnings);
|
|
529
935
|
if (docContext && commandRun && this.recordTelemetry) {
|
|
530
936
|
const contextTokens = estimateTokens(docContext.content);
|
|
@@ -542,9 +948,62 @@ export class TaskOrderingService {
|
|
|
542
948
|
});
|
|
543
949
|
}
|
|
544
950
|
}
|
|
545
|
-
|
|
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
|
+
}
|
|
980
|
+
if (missingRefs.size > 0) {
|
|
981
|
+
warnings.push(`Missing dependencies referenced: ${Array.from(missingRefs).join(", ")}`);
|
|
982
|
+
}
|
|
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);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (stageOrderMap.size === 0) {
|
|
997
|
+
DEFAULT_STAGE_ORDER.forEach((stage, idx) => stageOrderMap.set(stage, idx));
|
|
998
|
+
}
|
|
999
|
+
const impact = this.dependencyImpactMap(dependents);
|
|
1000
|
+
const { ordered: initialOrder, cycle, cycleMembers } = this.topologicalSort(nodes, dependents, impact, undefined, stageOrderMap);
|
|
1001
|
+
if (cycle) {
|
|
1002
|
+
warnings.push("Dependency cycle detected; ordering may be partial.");
|
|
1003
|
+
}
|
|
1004
|
+
let agentRank;
|
|
1005
|
+
if (enableAgentRanking && resolvedAgent) {
|
|
546
1006
|
try {
|
|
547
|
-
const agent = await this.resolveAgent(request.agentName);
|
|
548
1007
|
const summary = {
|
|
549
1008
|
project: project.key,
|
|
550
1009
|
epic: epic?.key,
|
|
@@ -573,13 +1032,12 @@ export class TaskOrderingService {
|
|
|
573
1032
|
]
|
|
574
1033
|
.filter(Boolean)
|
|
575
1034
|
.join("\n\n");
|
|
576
|
-
const { output } = await this.invokeAgent(
|
|
1035
|
+
const { output } = await this.invokeAgent(resolvedAgent, prompt, agentStream, {
|
|
577
1036
|
command: "order-tasks",
|
|
578
1037
|
project: project.key,
|
|
579
1038
|
epic: epic?.key,
|
|
580
1039
|
story: story?.key,
|
|
581
1040
|
statuses,
|
|
582
|
-
includeBlocked: request.includeBlocked === true,
|
|
583
1041
|
});
|
|
584
1042
|
const promptTokens = estimateTokens(prompt);
|
|
585
1043
|
const completionTokens = estimateTokens(output);
|
|
@@ -589,8 +1047,8 @@ export class TaskOrderingService {
|
|
|
589
1047
|
projectId: project.id,
|
|
590
1048
|
commandRunId: commandRun.id,
|
|
591
1049
|
jobId: job?.id,
|
|
592
|
-
agentId:
|
|
593
|
-
modelName:
|
|
1050
|
+
agentId: resolvedAgent.id,
|
|
1051
|
+
modelName: resolvedAgent.defaultModel,
|
|
594
1052
|
timestamp: new Date().toISOString(),
|
|
595
1053
|
commandName: "order-tasks",
|
|
596
1054
|
action: "ordering_tasks",
|
|
@@ -600,13 +1058,14 @@ export class TaskOrderingService {
|
|
|
600
1058
|
tokensCompletion: completionTokens,
|
|
601
1059
|
tokensTotal: promptTokens + completionTokens,
|
|
602
1060
|
metadata: {
|
|
603
|
-
adapter:
|
|
1061
|
+
adapter: resolvedAgent.adapter,
|
|
604
1062
|
epicKey: epic?.key,
|
|
605
1063
|
storyKey: story?.key,
|
|
606
|
-
includeBlocked: request.includeBlocked === true,
|
|
607
1064
|
statusFilter: statuses,
|
|
608
|
-
agentSlug:
|
|
609
|
-
modelName:
|
|
1065
|
+
agentSlug: resolvedAgent.slug,
|
|
1066
|
+
modelName: resolvedAgent.defaultModel,
|
|
1067
|
+
phase: "agent_ordering",
|
|
1068
|
+
attempt: 1,
|
|
610
1069
|
},
|
|
611
1070
|
});
|
|
612
1071
|
}
|
|
@@ -616,14 +1075,15 @@ export class TaskOrderingService {
|
|
|
616
1075
|
warnings.push(`Agent refinement skipped: ${error.message}`);
|
|
617
1076
|
}
|
|
618
1077
|
}
|
|
619
|
-
|
|
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);
|
|
620
1082
|
const finalCycleMembers = new Set([...cycleMembers, ...agentCycleMembers]);
|
|
621
1083
|
if (cycleAfterAgent && !cycle) {
|
|
622
1084
|
warnings.push("Agent-influenced ordering encountered a cycle; used partial order.");
|
|
623
1085
|
}
|
|
624
|
-
const
|
|
625
|
-
const unblockedTasks = ordered.filter((t) => !blockedSet.has(t.id));
|
|
626
|
-
const prioritized = [...unblockedTasks, ...blockedTasks];
|
|
1086
|
+
const prioritized = ordered;
|
|
627
1087
|
const epicMap = new Map();
|
|
628
1088
|
const storyMap = new Map();
|
|
629
1089
|
prioritized.forEach((task, idx) => {
|
|
@@ -636,16 +1096,13 @@ export class TaskOrderingService {
|
|
|
636
1096
|
storyMap.set(task.story_id, storyTasks);
|
|
637
1097
|
});
|
|
638
1098
|
await this.persistPriorities(prioritized, epicMap, storyMap);
|
|
639
|
-
const mapped = this.mapResult(prioritized,
|
|
640
|
-
const visibleOrdered = request.includeBlocked ? mapped.ordered : mapped.ordered.filter((t) => !t.blocked);
|
|
641
|
-
const visibleBlocked = request.includeBlocked ? [] : mapped.blocked;
|
|
1099
|
+
const mapped = this.mapResult(prioritized, impact, finalCycleMembers);
|
|
642
1100
|
if (job) {
|
|
643
1101
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
644
1102
|
processedItems: mapped.ordered.length,
|
|
645
1103
|
payload: {
|
|
646
1104
|
warnings,
|
|
647
1105
|
statuses,
|
|
648
|
-
includeBlocked: request.includeBlocked === true,
|
|
649
1106
|
epicKey: epic?.key,
|
|
650
1107
|
storyKey: story?.key,
|
|
651
1108
|
},
|
|
@@ -657,8 +1114,7 @@ export class TaskOrderingService {
|
|
|
657
1114
|
return {
|
|
658
1115
|
project,
|
|
659
1116
|
epic,
|
|
660
|
-
ordered:
|
|
661
|
-
blocked: visibleBlocked,
|
|
1117
|
+
ordered: mapped.ordered,
|
|
662
1118
|
warnings,
|
|
663
1119
|
jobId: job?.id,
|
|
664
1120
|
commandRunId: commandRun?.id,
|