@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.
- package/CHANGELOG.md +3 -0
- package/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +9 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +201 -6
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +6 -0
- package/dist/api/TasksApi.d.ts.map +1 -1
- package/dist/api/TasksApi.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +9 -1
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +9 -0
- package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
- package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingFormula.js +45 -0
- package/dist/services/agents/AgentRatingService.d.ts +60 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingService.js +363 -0
- package/dist/services/agents/GatewayAgentService.d.ts +11 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +525 -84
- package/dist/services/agents/GatewayHandoff.d.ts +11 -0
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
- package/dist/services/agents/GatewayHandoff.js +141 -0
- 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 +17 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +538 -79
- 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 +70 -3
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1930 -89
- 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 +200 -0
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
- package/dist/services/execution/GatewayTrioService.js +2492 -0
- 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 +2 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +9 -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 +27 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +354 -7
- package/dist/services/execution/QaTasksService.d.ts +59 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +3347 -318
- 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 +32 -1
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +4667 -722
- 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 +51 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +953 -106
- package/dist/services/planning/CreateTasksService.d.ts +21 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +569 -31
- package/dist/services/planning/RefineTasksService.d.ts +9 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +409 -59
- package/dist/services/review/CodeReviewService.d.ts +18 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +1309 -167
- 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 +17 -0
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
- package/dist/services/shared/ProjectGuidance.js +78 -0
- 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/tasks/TaskCommentFormatter.d.ts +20 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
- package/dist/services/tasks/TaskCommentFormatter.js +54 -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 +26 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +206 -32
- package/package.json +6 -5
|
@@ -6,17 +6,134 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
6
6
|
import { DocdexClient } from "@mcoda/integrations";
|
|
7
7
|
import { JobService } from "../jobs/JobService.js";
|
|
8
8
|
import { RoutingService } from "../agents/RoutingService.js";
|
|
9
|
+
import { AgentRatingService } from "../agents/AgentRatingService.js";
|
|
10
|
+
import { classifyTask } from "../backlog/TaskOrderingHeuristics.js";
|
|
11
|
+
import { TaskOrderingService } from "../backlog/TaskOrderingService.js";
|
|
9
12
|
import { createEpicKeyGenerator, createStoryKeyGenerator, createTaskKeyGenerator, } from "./KeyHelpers.js";
|
|
10
13
|
const formatBullets = (items, fallback) => {
|
|
11
14
|
if (!items || items.length === 0)
|
|
12
15
|
return `- ${fallback}`;
|
|
13
16
|
return items.map((item) => `- ${item}`).join("\n");
|
|
14
17
|
};
|
|
18
|
+
const normalizeStringArray = (value) => {
|
|
19
|
+
if (!Array.isArray(value))
|
|
20
|
+
return [];
|
|
21
|
+
return value
|
|
22
|
+
.filter((item) => typeof item === "string")
|
|
23
|
+
.map((item) => item.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
};
|
|
26
|
+
const normalizeEntrypoints = (value) => {
|
|
27
|
+
if (!Array.isArray(value))
|
|
28
|
+
return [];
|
|
29
|
+
return value
|
|
30
|
+
.map((entry) => {
|
|
31
|
+
if (!entry || typeof entry !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
const record = entry;
|
|
34
|
+
const kind = record.kind;
|
|
35
|
+
if (kind !== "web" && kind !== "api" && kind !== "cli")
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
kind,
|
|
39
|
+
base_url: typeof record.base_url === "string" ? record.base_url : undefined,
|
|
40
|
+
command: typeof record.command === "string" ? record.command : undefined,
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter((entry) => Boolean(entry));
|
|
44
|
+
};
|
|
45
|
+
const normalizeQaReadiness = (value) => {
|
|
46
|
+
if (!value || typeof value !== "object")
|
|
47
|
+
return undefined;
|
|
48
|
+
const record = value;
|
|
49
|
+
const qa = {
|
|
50
|
+
profiles_expected: normalizeStringArray(record.profiles_expected),
|
|
51
|
+
requires: normalizeStringArray(record.requires),
|
|
52
|
+
entrypoints: normalizeEntrypoints(record.entrypoints),
|
|
53
|
+
data_setup: normalizeStringArray(record.data_setup),
|
|
54
|
+
blockers: normalizeStringArray(record.blockers),
|
|
55
|
+
notes: typeof record.notes === "string" ? record.notes : undefined,
|
|
56
|
+
};
|
|
57
|
+
const hasValues = (qa.profiles_expected?.length ?? 0) > 0 ||
|
|
58
|
+
(qa.requires?.length ?? 0) > 0 ||
|
|
59
|
+
(qa.entrypoints?.length ?? 0) > 0 ||
|
|
60
|
+
(qa.data_setup?.length ?? 0) > 0 ||
|
|
61
|
+
(qa.blockers?.length ?? 0) > 0 ||
|
|
62
|
+
(qa.notes?.length ?? 0) > 0;
|
|
63
|
+
return hasValues ? qa : undefined;
|
|
64
|
+
};
|
|
65
|
+
const uniqueStrings = (items) => Array.from(new Set(items));
|
|
66
|
+
const uniqueEntrypoints = (items) => {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const result = [];
|
|
69
|
+
for (const entry of items) {
|
|
70
|
+
const key = `${entry.kind}|${entry.base_url ?? ""}|${entry.command ?? ""}`;
|
|
71
|
+
if (seen.has(key))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(key);
|
|
74
|
+
result.push(entry);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
78
|
+
const buildQaReadiness = (params) => {
|
|
79
|
+
const derivedProfiles = ["cli"];
|
|
80
|
+
if (params.classification.stage === "frontend")
|
|
81
|
+
derivedProfiles.push("chromium");
|
|
82
|
+
if (params.classification.stage === "backend")
|
|
83
|
+
derivedProfiles.push("api");
|
|
84
|
+
const profilesExpected = uniqueStrings([
|
|
85
|
+
...derivedProfiles,
|
|
86
|
+
...(params.overrides?.profiles_expected ?? []),
|
|
87
|
+
...(params.planQa?.profiles_expected ?? []),
|
|
88
|
+
]);
|
|
89
|
+
const entrypoints = uniqueEntrypoints([
|
|
90
|
+
...(params.overrides?.entrypoints ?? []),
|
|
91
|
+
...(params.planQa?.entrypoints ?? []),
|
|
92
|
+
...(params.classification.stage === "frontend" ? params.preflight?.entrypoints ?? [] : []),
|
|
93
|
+
]);
|
|
94
|
+
const blockers = uniqueStrings([
|
|
95
|
+
...(params.overrides?.blockers ?? []),
|
|
96
|
+
...(params.planQa?.blockers ?? []),
|
|
97
|
+
...(params.preflight?.blockers ?? []),
|
|
98
|
+
]);
|
|
99
|
+
if (params.classification.stage === "frontend" && entrypoints.length === 0) {
|
|
100
|
+
blockers.push("Missing UI entrypoint (dev/start script).");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
profiles_expected: profilesExpected,
|
|
104
|
+
requires: uniqueStrings([...(params.overrides?.requires ?? []), ...(params.planQa?.requires ?? [])]),
|
|
105
|
+
entrypoints: entrypoints.length ? entrypoints : undefined,
|
|
106
|
+
data_setup: uniqueStrings([...(params.overrides?.data_setup ?? []), ...(params.planQa?.data_setup ?? [])]),
|
|
107
|
+
blockers: blockers.length ? blockers : undefined,
|
|
108
|
+
notes: params.overrides?.notes ?? params.planQa?.notes,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const formatTestList = (items) => {
|
|
112
|
+
if (!items || items.length === 0)
|
|
113
|
+
return "Not applicable";
|
|
114
|
+
return items.join("; ");
|
|
115
|
+
};
|
|
15
116
|
const ensureNonEmpty = (value, fallback) => value && value.trim().length > 0 ? value.trim() : fallback;
|
|
117
|
+
const extractScriptPort = (script) => {
|
|
118
|
+
const matches = [script.match(/(?:--port|-p)\s*(\d{2,5})/), script.match(/PORT\s*=\s*(\d{2,5})/)];
|
|
119
|
+
for (const match of matches) {
|
|
120
|
+
if (!match)
|
|
121
|
+
continue;
|
|
122
|
+
const parsed = Number.parseInt(match[1] ?? "", 10);
|
|
123
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
124
|
+
return parsed;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
16
128
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
17
129
|
const DOC_CONTEXT_BUDGET = 8000;
|
|
130
|
+
const DOCDEX_HANDLE = /^docdex:/i;
|
|
131
|
+
const VALID_AREAS = new Set(["web", "adm", "bck", "ops", "infra", "mobile"]);
|
|
132
|
+
const VALID_TASK_TYPES = new Set(["feature", "bug", "chore", "spike"]);
|
|
18
133
|
const inferDocType = (filePath) => {
|
|
19
134
|
const name = path.basename(filePath).toLowerCase();
|
|
135
|
+
if (name.includes("openapi") || name.includes("swagger"))
|
|
136
|
+
return "OPENAPI";
|
|
20
137
|
if (name.includes("sds"))
|
|
21
138
|
return "SDS";
|
|
22
139
|
if (name.includes("pdr"))
|
|
@@ -25,6 +142,48 @@ const inferDocType = (filePath) => {
|
|
|
25
142
|
return "RFP";
|
|
26
143
|
return "DOC";
|
|
27
144
|
};
|
|
145
|
+
const normalizeArea = (value) => {
|
|
146
|
+
if (typeof value !== "string")
|
|
147
|
+
return undefined;
|
|
148
|
+
const tokens = value
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.split(/[^a-z]+/)
|
|
151
|
+
.map((token) => token.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
for (const token of tokens) {
|
|
154
|
+
if (VALID_AREAS.has(token))
|
|
155
|
+
return token;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
};
|
|
159
|
+
const normalizeTaskType = (value) => {
|
|
160
|
+
if (typeof value !== "string")
|
|
161
|
+
return undefined;
|
|
162
|
+
const tokens = value
|
|
163
|
+
.toLowerCase()
|
|
164
|
+
.split(/[^a-z]+/)
|
|
165
|
+
.map((token) => token.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
for (const token of tokens) {
|
|
168
|
+
if (VALID_TASK_TYPES.has(token))
|
|
169
|
+
return token;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
};
|
|
173
|
+
const normalizeRelatedDocs = (value) => {
|
|
174
|
+
if (!Array.isArray(value))
|
|
175
|
+
return [];
|
|
176
|
+
return value
|
|
177
|
+
.map((entry) => {
|
|
178
|
+
if (typeof entry === "string")
|
|
179
|
+
return entry;
|
|
180
|
+
if (entry && typeof entry === "object" && "handle" in entry && typeof entry.handle === "string") {
|
|
181
|
+
return entry.handle;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
})
|
|
185
|
+
.filter((entry) => Boolean(entry && DOCDEX_HANDLE.test(entry)));
|
|
186
|
+
};
|
|
28
187
|
const describeDoc = (doc, idx) => {
|
|
29
188
|
const title = doc.title ?? doc.path ?? doc.id ?? `doc-${idx + 1}`;
|
|
30
189
|
const source = doc.path ?? doc.id ?? "docdex";
|
|
@@ -32,19 +191,80 @@ const describeDoc = (doc, idx) => {
|
|
|
32
191
|
return `- [${doc.docType}] ${title} (handle: docdex:${doc.id ?? `doc-${idx + 1}`}, source: ${source})${head ? `\n Excerpt: ${head}` : ""}`;
|
|
33
192
|
};
|
|
34
193
|
const extractJson = (raw) => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
194
|
+
const fencedMatches = [...raw.matchAll(/```json([\s\S]*?)```/g)].map((match) => match[1]);
|
|
195
|
+
const stripped = raw.replace(/<think>[\s\S]*?<\/think>/g, "");
|
|
196
|
+
const candidates = [...fencedMatches, stripped, raw].filter((candidate) => candidate.trim().length > 0);
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
const parsed = tryParseJson(candidate);
|
|
199
|
+
if (parsed && isPlanShape(parsed))
|
|
200
|
+
return parsed;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
};
|
|
204
|
+
const isPlanShape = (value) => {
|
|
205
|
+
if (!value || typeof value !== "object")
|
|
206
|
+
return false;
|
|
207
|
+
return Array.isArray(value.epics) || Array.isArray(value.stories) || Array.isArray(value.tasks);
|
|
208
|
+
};
|
|
209
|
+
const tryParseJson = (value) => {
|
|
42
210
|
try {
|
|
43
|
-
return JSON.parse(
|
|
211
|
+
return JSON.parse(value);
|
|
44
212
|
}
|
|
45
213
|
catch {
|
|
46
|
-
|
|
214
|
+
// continue
|
|
47
215
|
}
|
|
216
|
+
const objects = extractJsonObjects(value).reverse();
|
|
217
|
+
for (const obj of objects) {
|
|
218
|
+
try {
|
|
219
|
+
return JSON.parse(obj);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
};
|
|
227
|
+
const extractJsonObjects = (value) => {
|
|
228
|
+
const results = [];
|
|
229
|
+
let depth = 0;
|
|
230
|
+
let start = -1;
|
|
231
|
+
let inString = false;
|
|
232
|
+
let escaped = false;
|
|
233
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
234
|
+
const ch = value[i];
|
|
235
|
+
if (inString) {
|
|
236
|
+
if (escaped) {
|
|
237
|
+
escaped = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (ch === "\\") {
|
|
241
|
+
escaped = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === "\"")
|
|
245
|
+
inString = false;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === "\"") {
|
|
249
|
+
inString = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === "{") {
|
|
253
|
+
if (depth === 0)
|
|
254
|
+
start = i;
|
|
255
|
+
depth += 1;
|
|
256
|
+
}
|
|
257
|
+
else if (ch === "}") {
|
|
258
|
+
if (depth === 0)
|
|
259
|
+
continue;
|
|
260
|
+
depth -= 1;
|
|
261
|
+
if (depth === 0 && start >= 0) {
|
|
262
|
+
results.push(value.slice(start, i + 1));
|
|
263
|
+
start = -1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return results;
|
|
48
268
|
};
|
|
49
269
|
const buildEpicDescription = (epicKey, title, description, acceptance, relatedDocs) => {
|
|
50
270
|
return [
|
|
@@ -100,7 +320,17 @@ const buildStoryDescription = (storyKey, title, userStory, description, acceptan
|
|
|
100
320
|
formatBullets(relatedDocs, "Docdex handles, OpenAPI endpoints, code modules."),
|
|
101
321
|
].join("\n");
|
|
102
322
|
};
|
|
103
|
-
const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, relatedDocs, dependencies) => {
|
|
323
|
+
const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, relatedDocs, dependencies, tests, qa) => {
|
|
324
|
+
const formatEntrypoints = (entrypoints) => {
|
|
325
|
+
if (!entrypoints || entrypoints.length === 0)
|
|
326
|
+
return "- Not specified";
|
|
327
|
+
return entrypoints
|
|
328
|
+
.map((entry) => {
|
|
329
|
+
const target = entry.base_url ?? entry.command ?? "TBD";
|
|
330
|
+
return `- ${entry.kind}: ${target}`;
|
|
331
|
+
})
|
|
332
|
+
.join("\n");
|
|
333
|
+
};
|
|
104
334
|
return [
|
|
105
335
|
`* **Task Key**: ${taskKey}`,
|
|
106
336
|
"* **Objective**",
|
|
@@ -117,7 +347,18 @@ const buildTaskDescription = (taskKey, title, description, storyKey, epicKey, re
|
|
|
117
347
|
"* **Definition of Done**",
|
|
118
348
|
"- Tests passing, docs updated, review/QA complete.",
|
|
119
349
|
"* **Testing & QA**",
|
|
120
|
-
|
|
350
|
+
`- Unit tests: ${formatTestList(tests.unitTests)}`,
|
|
351
|
+
`- Component tests: ${formatTestList(tests.componentTests)}`,
|
|
352
|
+
`- Integration tests: ${formatTestList(tests.integrationTests)}`,
|
|
353
|
+
`- API tests: ${formatTestList(tests.apiTests)}`,
|
|
354
|
+
"* **QA Readiness**",
|
|
355
|
+
`- Profiles: ${qa?.profiles_expected?.length ? qa.profiles_expected.join(", ") : "TBD"}`,
|
|
356
|
+
`- Requires: ${qa?.requires?.length ? qa.requires.join("; ") : "None specified"}`,
|
|
357
|
+
`- Data setup: ${qa?.data_setup?.length ? qa.data_setup.join("; ") : "None specified"}`,
|
|
358
|
+
"* **QA Entry Points**",
|
|
359
|
+
formatEntrypoints(qa?.entrypoints),
|
|
360
|
+
"* **QA Blockers**",
|
|
361
|
+
formatBullets(qa?.blockers, "None known."),
|
|
121
362
|
"* **Dependencies**",
|
|
122
363
|
formatBullets(dependencies, "Enumerate prerequisite tasks by key."),
|
|
123
364
|
"* **Risks & Gotchas**",
|
|
@@ -181,7 +422,18 @@ const TASK_SCHEMA_SNIPPET = `{
|
|
|
181
422
|
"estimatedStoryPoints": 3,
|
|
182
423
|
"priorityHint": 50,
|
|
183
424
|
"dependsOnKeys": ["t0"],
|
|
184
|
-
"relatedDocs": ["docdex:..."]
|
|
425
|
+
"relatedDocs": ["docdex:..."],
|
|
426
|
+
"unitTests": ["unit test description"],
|
|
427
|
+
"componentTests": ["component test description"],
|
|
428
|
+
"integrationTests": ["integration test description"],
|
|
429
|
+
"apiTests": ["api test description"],
|
|
430
|
+
"qa": {
|
|
431
|
+
"profiles_expected": ["cli", "api", "chromium"],
|
|
432
|
+
"requires": ["dev server", "seed data"],
|
|
433
|
+
"entrypoints": [{ "kind": "web", "base_url": "http://localhost:<PORT>", "command": "npm run dev" }],
|
|
434
|
+
"data_setup": ["seed sample data"],
|
|
435
|
+
"notes": "optional QA notes"
|
|
436
|
+
}
|
|
185
437
|
}
|
|
186
438
|
]
|
|
187
439
|
}`;
|
|
@@ -194,14 +446,18 @@ export class CreateTasksService {
|
|
|
194
446
|
this.repo = deps.repo;
|
|
195
447
|
this.workspaceRepo = deps.workspaceRepo;
|
|
196
448
|
this.routingService = deps.routingService;
|
|
449
|
+
this.ratingService = deps.ratingService;
|
|
450
|
+
this.taskOrderingFactory = deps.taskOrderingFactory ?? TaskOrderingService.create;
|
|
197
451
|
}
|
|
198
452
|
static async create(workspace) {
|
|
199
453
|
const repo = await GlobalRepository.create();
|
|
200
454
|
const agentService = new AgentService(repo);
|
|
201
455
|
const routingService = await RoutingService.create();
|
|
456
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
202
457
|
const docdex = new DocdexClient({
|
|
203
458
|
workspaceRoot: workspace.workspaceRoot,
|
|
204
459
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
460
|
+
repoId: docdexRepoId,
|
|
205
461
|
});
|
|
206
462
|
const jobService = new JobService(workspace);
|
|
207
463
|
const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
|
|
@@ -232,6 +488,17 @@ export class CreateTasksService {
|
|
|
232
488
|
const docdex = this.docdex;
|
|
233
489
|
await swallow(docdex?.close?.bind(docdex));
|
|
234
490
|
}
|
|
491
|
+
async seedPriorities(projectKey) {
|
|
492
|
+
const ordering = await this.taskOrderingFactory(this.workspace, { recordTelemetry: false });
|
|
493
|
+
try {
|
|
494
|
+
await ordering.orderTasks({
|
|
495
|
+
projectKey,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await ordering.close();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
235
502
|
async resolveAgent(agentName) {
|
|
236
503
|
const resolved = await this.routingService.resolveAgentForCommand({
|
|
237
504
|
workspace: this.workspace,
|
|
@@ -240,9 +507,23 @@ export class CreateTasksService {
|
|
|
240
507
|
});
|
|
241
508
|
return resolved.agent;
|
|
242
509
|
}
|
|
510
|
+
ensureRatingService() {
|
|
511
|
+
if (!this.ratingService) {
|
|
512
|
+
this.ratingService = new AgentRatingService(this.workspace, {
|
|
513
|
+
workspaceRepo: this.workspaceRepo,
|
|
514
|
+
globalRepo: this.repo,
|
|
515
|
+
agentService: this.agentService,
|
|
516
|
+
routingService: this.routingService,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return this.ratingService;
|
|
520
|
+
}
|
|
243
521
|
async prepareDocs(inputs) {
|
|
522
|
+
const resolvedInputs = inputs.length > 0 ? inputs : await this.resolveDefaultDocInputs();
|
|
523
|
+
if (resolvedInputs.length === 0)
|
|
524
|
+
return [];
|
|
244
525
|
const documents = [];
|
|
245
|
-
for (const input of
|
|
526
|
+
for (const input of resolvedInputs) {
|
|
246
527
|
if (input.startsWith("docdex:")) {
|
|
247
528
|
const docId = input.replace(/^docdex:/, "");
|
|
248
529
|
try {
|
|
@@ -263,6 +544,11 @@ export class CreateTasksService {
|
|
|
263
544
|
throw new Error(`Failed to read input ${input}: ${error.message}`);
|
|
264
545
|
}
|
|
265
546
|
for (const filePath of paths) {
|
|
547
|
+
const baseName = path.basename(filePath);
|
|
548
|
+
if (baseName.endsWith(".meta.json") || baseName.endsWith("-first-draft.md"))
|
|
549
|
+
continue;
|
|
550
|
+
if (!/\.(md|markdown|ya?ml|json)$/i.test(baseName))
|
|
551
|
+
continue;
|
|
266
552
|
const docType = inferDocType(filePath);
|
|
267
553
|
try {
|
|
268
554
|
const doc = await this.docdex.ensureRegisteredFromFile(filePath, docType, {
|
|
@@ -277,6 +563,128 @@ export class CreateTasksService {
|
|
|
277
563
|
}
|
|
278
564
|
return documents;
|
|
279
565
|
}
|
|
566
|
+
async resolveDefaultDocInputs() {
|
|
567
|
+
const candidates = [
|
|
568
|
+
path.join(this.workspace.mcodaDir, "docs"),
|
|
569
|
+
path.join(this.workspace.workspaceRoot, "docs"),
|
|
570
|
+
path.join(this.workspace.workspaceRoot, "openapi"),
|
|
571
|
+
path.join(this.workspace.workspaceRoot, "openapi.yaml"),
|
|
572
|
+
path.join(this.workspace.workspaceRoot, "openapi.yml"),
|
|
573
|
+
path.join(this.workspace.workspaceRoot, "openapi.json"),
|
|
574
|
+
];
|
|
575
|
+
const existing = [];
|
|
576
|
+
for (const candidate of candidates) {
|
|
577
|
+
try {
|
|
578
|
+
const stat = await fs.stat(candidate);
|
|
579
|
+
if (stat.isFile() || stat.isDirectory())
|
|
580
|
+
existing.push(candidate);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Ignore missing candidates; fall back to empty inputs.
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return existing;
|
|
587
|
+
}
|
|
588
|
+
async buildQaPreflight() {
|
|
589
|
+
const preflight = {
|
|
590
|
+
scripts: {},
|
|
591
|
+
entrypoints: [],
|
|
592
|
+
blockers: [],
|
|
593
|
+
};
|
|
594
|
+
const packagePath = path.join(this.workspace.workspaceRoot, "package.json");
|
|
595
|
+
let pkg = null;
|
|
596
|
+
try {
|
|
597
|
+
const raw = await fs.readFile(packagePath, "utf8");
|
|
598
|
+
pkg = JSON.parse(raw);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return preflight;
|
|
602
|
+
}
|
|
603
|
+
const scripts = pkg?.scripts;
|
|
604
|
+
if (scripts && typeof scripts === "object") {
|
|
605
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
606
|
+
if (typeof value === "string") {
|
|
607
|
+
preflight.scripts[name] = value;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const dependencies = {
|
|
612
|
+
...(pkg?.dependencies && typeof pkg.dependencies === "object" ? pkg.dependencies : {}),
|
|
613
|
+
...(pkg?.devDependencies && typeof pkg.devDependencies === "object"
|
|
614
|
+
? pkg.devDependencies
|
|
615
|
+
: {}),
|
|
616
|
+
};
|
|
617
|
+
const hasDev = typeof preflight.scripts.dev === "string";
|
|
618
|
+
const hasStart = typeof preflight.scripts.start === "string";
|
|
619
|
+
const devPort = hasDev ? extractScriptPort(preflight.scripts.dev) : undefined;
|
|
620
|
+
const startPort = hasStart ? extractScriptPort(preflight.scripts.start) : undefined;
|
|
621
|
+
if (hasDev) {
|
|
622
|
+
preflight.entrypoints.push({
|
|
623
|
+
kind: "web",
|
|
624
|
+
base_url: devPort ? `http://localhost:${devPort}` : undefined,
|
|
625
|
+
command: "npm run dev",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
else if (hasStart) {
|
|
629
|
+
preflight.entrypoints.push({
|
|
630
|
+
kind: "web",
|
|
631
|
+
base_url: startPort ? `http://localhost:${startPort}` : undefined,
|
|
632
|
+
command: "npm start",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const testDirs = [
|
|
636
|
+
path.join(this.workspace.workspaceRoot, "tests"),
|
|
637
|
+
path.join(this.workspace.workspaceRoot, "__tests__"),
|
|
638
|
+
];
|
|
639
|
+
const testFiles = [];
|
|
640
|
+
for (const dir of testDirs) {
|
|
641
|
+
try {
|
|
642
|
+
const stat = await fs.stat(dir);
|
|
643
|
+
if (!stat.isDirectory())
|
|
644
|
+
continue;
|
|
645
|
+
testFiles.push(...(await collectFilesRecursively(dir)));
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// ignore missing test dirs
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const testCandidates = testFiles.filter((file) => /\b(test|spec)\b/i.test(path.basename(file)));
|
|
652
|
+
const hasSupertest = typeof dependencies.supertest === "string";
|
|
653
|
+
if (!hasSupertest && testCandidates.length > 0) {
|
|
654
|
+
for (const file of testCandidates) {
|
|
655
|
+
try {
|
|
656
|
+
const content = await fs.readFile(file, "utf8");
|
|
657
|
+
if (content.includes("supertest")) {
|
|
658
|
+
preflight.blockers.push("Missing devDependency: supertest (required by test files).");
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// ignore read errors
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return preflight;
|
|
668
|
+
}
|
|
669
|
+
buildQaOverrides(options) {
|
|
670
|
+
const profiles = options.qaProfiles?.filter(Boolean);
|
|
671
|
+
const requires = options.qaRequires?.filter(Boolean);
|
|
672
|
+
const entrypoints = [];
|
|
673
|
+
if (options.qaEntryUrl || options.qaStartCommand) {
|
|
674
|
+
entrypoints.push({
|
|
675
|
+
kind: "web",
|
|
676
|
+
base_url: options.qaEntryUrl,
|
|
677
|
+
command: options.qaStartCommand,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (!profiles?.length && !requires?.length && entrypoints.length === 0)
|
|
681
|
+
return undefined;
|
|
682
|
+
return {
|
|
683
|
+
profiles_expected: profiles,
|
|
684
|
+
requires,
|
|
685
|
+
entrypoints: entrypoints.length ? entrypoints : undefined,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
280
688
|
buildDocContext(docs) {
|
|
281
689
|
const warnings = [];
|
|
282
690
|
const blocks = [];
|
|
@@ -365,6 +773,10 @@ export class CreateTasksService {
|
|
|
365
773
|
estimatedStoryPoints: 1,
|
|
366
774
|
priorityHint: 10,
|
|
367
775
|
relatedDocs: docRefs,
|
|
776
|
+
unitTests: [],
|
|
777
|
+
componentTests: [],
|
|
778
|
+
integrationTests: [],
|
|
779
|
+
apiTests: [],
|
|
368
780
|
},
|
|
369
781
|
{
|
|
370
782
|
localId: "task-2",
|
|
@@ -375,6 +787,10 @@ export class CreateTasksService {
|
|
|
375
787
|
priorityHint: 20,
|
|
376
788
|
dependsOnKeys: ["task-1"],
|
|
377
789
|
relatedDocs: docRefs,
|
|
790
|
+
unitTests: [],
|
|
791
|
+
componentTests: [],
|
|
792
|
+
integrationTests: [],
|
|
793
|
+
apiTests: [],
|
|
378
794
|
},
|
|
379
795
|
],
|
|
380
796
|
},
|
|
@@ -412,6 +828,7 @@ export class CreateTasksService {
|
|
|
412
828
|
}
|
|
413
829
|
let parsed = extractJson(output);
|
|
414
830
|
if (!parsed) {
|
|
831
|
+
const attempt = 2;
|
|
415
832
|
const fixPrompt = [
|
|
416
833
|
"Rewrite the previous response into valid JSON matching the expected schema.",
|
|
417
834
|
`Schema hint:\n${action === "epics" ? EPIC_SCHEMA_SNIPPET : action === "stories" ? STORY_SCHEMA_SNIPPET : TASK_SCHEMA_SNIPPET}`,
|
|
@@ -422,6 +839,32 @@ export class CreateTasksService {
|
|
|
422
839
|
const fix = await this.agentService.invoke(agent.id, { input: fixPrompt });
|
|
423
840
|
output = fix.output ?? "";
|
|
424
841
|
parsed = extractJson(output);
|
|
842
|
+
if (parsed) {
|
|
843
|
+
const promptTokens = estimateTokens(prompt);
|
|
844
|
+
const completionTokens = estimateTokens(output);
|
|
845
|
+
const durationSeconds = (Date.now() - startedAt) / 1000;
|
|
846
|
+
await this.jobService.recordTokenUsage({
|
|
847
|
+
timestamp: new Date().toISOString(),
|
|
848
|
+
workspaceId: this.workspace.workspaceId,
|
|
849
|
+
jobId,
|
|
850
|
+
commandRunId,
|
|
851
|
+
agentId: agent.id,
|
|
852
|
+
modelName: agent.defaultModel,
|
|
853
|
+
promptTokens,
|
|
854
|
+
completionTokens,
|
|
855
|
+
tokensPrompt: promptTokens,
|
|
856
|
+
tokensCompletion: completionTokens,
|
|
857
|
+
tokensTotal: promptTokens + completionTokens,
|
|
858
|
+
durationSeconds,
|
|
859
|
+
metadata: {
|
|
860
|
+
action: `create_tasks_${action}`,
|
|
861
|
+
phase: `create_tasks_${action}`,
|
|
862
|
+
attempt,
|
|
863
|
+
...(metadata ?? {}),
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
return { output, promptTokens, completionTokens };
|
|
867
|
+
}
|
|
425
868
|
}
|
|
426
869
|
catch (error) {
|
|
427
870
|
throw new Error(`Agent retry failed (${action}): ${error.message}`);
|
|
@@ -446,7 +889,12 @@ export class CreateTasksService {
|
|
|
446
889
|
tokensCompletion: completionTokens,
|
|
447
890
|
tokensTotal: promptTokens + completionTokens,
|
|
448
891
|
durationSeconds,
|
|
449
|
-
metadata: {
|
|
892
|
+
metadata: {
|
|
893
|
+
action: `create_tasks_${action}`,
|
|
894
|
+
phase: `create_tasks_${action}`,
|
|
895
|
+
attempt: 1,
|
|
896
|
+
...(metadata ?? {}),
|
|
897
|
+
},
|
|
450
898
|
});
|
|
451
899
|
return { output, promptTokens, completionTokens };
|
|
452
900
|
}
|
|
@@ -458,11 +906,11 @@ export class CreateTasksService {
|
|
|
458
906
|
return parsed.epics
|
|
459
907
|
.map((epic, idx) => ({
|
|
460
908
|
localId: epic.localId ?? `e${idx + 1}`,
|
|
461
|
-
area: epic.area,
|
|
909
|
+
area: normalizeArea(epic.area),
|
|
462
910
|
title: epic.title ?? "Epic",
|
|
463
911
|
description: epic.description,
|
|
464
912
|
acceptanceCriteria: Array.isArray(epic.acceptanceCriteria) ? epic.acceptanceCriteria : [],
|
|
465
|
-
relatedDocs:
|
|
913
|
+
relatedDocs: normalizeRelatedDocs(epic.relatedDocs),
|
|
466
914
|
priorityHint: typeof epic.priorityHint === "number" ? epic.priorityHint : undefined,
|
|
467
915
|
stories: [],
|
|
468
916
|
}))
|
|
@@ -496,13 +944,21 @@ export class CreateTasksService {
|
|
|
496
944
|
userStory: story.userStory ?? story.description,
|
|
497
945
|
description: story.description,
|
|
498
946
|
acceptanceCriteria: Array.isArray(story.acceptanceCriteria) ? story.acceptanceCriteria : [],
|
|
499
|
-
relatedDocs:
|
|
947
|
+
relatedDocs: normalizeRelatedDocs(story.relatedDocs),
|
|
500
948
|
priorityHint: typeof story.priorityHint === "number" ? story.priorityHint : undefined,
|
|
501
949
|
tasks: [],
|
|
502
950
|
}))
|
|
503
951
|
.filter((s) => s.title);
|
|
504
952
|
}
|
|
505
953
|
async generateTasksForStory(agent, epic, story, docSummary, stream, jobId, commandRunId) {
|
|
954
|
+
const parseTestList = (value) => {
|
|
955
|
+
if (!Array.isArray(value))
|
|
956
|
+
return [];
|
|
957
|
+
return value
|
|
958
|
+
.filter((item) => typeof item === "string")
|
|
959
|
+
.map((item) => item.trim())
|
|
960
|
+
.filter(Boolean);
|
|
961
|
+
};
|
|
506
962
|
const prompt = [
|
|
507
963
|
`Generate tasks for story "${story.title}" (Epic: ${epic.title}).`,
|
|
508
964
|
"Use the Task template: Objective; Context; Inputs; Implementation Plan; DoD; Testing & QA; Dependencies; Risks; References.",
|
|
@@ -510,6 +966,11 @@ export class CreateTasksService {
|
|
|
510
966
|
TASK_SCHEMA_SNIPPET,
|
|
511
967
|
"Rules:",
|
|
512
968
|
"- Each task must include localId, title, description, type, estimatedStoryPoints, priorityHint.",
|
|
969
|
+
"- Include test arrays: unitTests, componentTests, integrationTests, apiTests. Use [] when not applicable.",
|
|
970
|
+
"- Only include tests that are relevant to the task's scope.",
|
|
971
|
+
"- If the task involves code or configuration changes, include at least one relevant test; do not leave all test arrays empty unless it's purely documentation or research.",
|
|
972
|
+
"- When known, include qa object with profiles_expected/requires/entrypoints/data_setup to guide QA.",
|
|
973
|
+
"- Do not hardcode ports. For QA entrypoints, use http://localhost:<PORT> placeholders or omit base_url when unknown.",
|
|
513
974
|
"- dependsOnKeys must reference localIds in this story.",
|
|
514
975
|
"- Use docdex handles when citing docs.",
|
|
515
976
|
`Story context (key=${story.key ?? story.localId ?? "TBD"}):`,
|
|
@@ -526,16 +987,35 @@ export class CreateTasksService {
|
|
|
526
987
|
throw new Error(`Agent did not return tasks for story ${story.title}`);
|
|
527
988
|
}
|
|
528
989
|
return parsed.tasks
|
|
529
|
-
.map((task, idx) =>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
990
|
+
.map((task, idx) => {
|
|
991
|
+
const unitTests = parseTestList(task.unitTests);
|
|
992
|
+
const componentTests = parseTestList(task.componentTests);
|
|
993
|
+
const integrationTests = parseTestList(task.integrationTests);
|
|
994
|
+
const apiTests = parseTestList(task.apiTests);
|
|
995
|
+
const hasTests = unitTests.length || componentTests.length || integrationTests.length || apiTests.length;
|
|
996
|
+
const title = task.title ?? "Task";
|
|
997
|
+
const description = task.description ?? "";
|
|
998
|
+
const docOnly = /doc|documentation|readme|pdr|sds|openapi|spec/.test(`${title} ${description}`.toLowerCase());
|
|
999
|
+
if (!hasTests && !docOnly) {
|
|
1000
|
+
unitTests.push(`Add tests for ${title} (unit/component/integration/api as applicable)`);
|
|
1001
|
+
}
|
|
1002
|
+
const qa = normalizeQaReadiness(task.qa);
|
|
1003
|
+
return {
|
|
1004
|
+
localId: task.localId ?? `t${idx + 1}`,
|
|
1005
|
+
title,
|
|
1006
|
+
type: normalizeTaskType(task.type) ?? "feature",
|
|
1007
|
+
description,
|
|
1008
|
+
estimatedStoryPoints: typeof task.estimatedStoryPoints === "number" ? task.estimatedStoryPoints : undefined,
|
|
1009
|
+
priorityHint: typeof task.priorityHint === "number" ? task.priorityHint : undefined,
|
|
1010
|
+
dependsOnKeys: Array.isArray(task.dependsOnKeys) ? task.dependsOnKeys : [],
|
|
1011
|
+
relatedDocs: normalizeRelatedDocs(task.relatedDocs),
|
|
1012
|
+
unitTests,
|
|
1013
|
+
componentTests,
|
|
1014
|
+
integrationTests,
|
|
1015
|
+
apiTests,
|
|
1016
|
+
qa,
|
|
1017
|
+
};
|
|
1018
|
+
})
|
|
539
1019
|
.filter((t) => t.title);
|
|
540
1020
|
}
|
|
541
1021
|
async generatePlanFromAgent(epics, agent, docSummary, options) {
|
|
@@ -571,7 +1051,7 @@ export class CreateTasksService {
|
|
|
571
1051
|
return { epics: planEpics, stories: planStories, tasks: planTasks };
|
|
572
1052
|
}
|
|
573
1053
|
async writePlanArtifacts(projectKey, plan, docSummary) {
|
|
574
|
-
const baseDir = path.join(this.workspace.
|
|
1054
|
+
const baseDir = path.join(this.workspace.mcodaDir, "tasks", projectKey);
|
|
575
1055
|
await fs.mkdir(baseDir, { recursive: true });
|
|
576
1056
|
const write = async (file, data) => {
|
|
577
1057
|
const target = path.join(baseDir, file);
|
|
@@ -664,6 +1144,17 @@ export class CreateTasksService {
|
|
|
664
1144
|
const epicId = epicIdByKey.get(task.epicKey);
|
|
665
1145
|
if (!storyId || !epicId)
|
|
666
1146
|
continue;
|
|
1147
|
+
const classification = classifyTask({
|
|
1148
|
+
title: task.plan.title ?? `Task ${task.key}`,
|
|
1149
|
+
description: task.plan.description,
|
|
1150
|
+
type: task.plan.type,
|
|
1151
|
+
});
|
|
1152
|
+
const qaReadiness = buildQaReadiness({
|
|
1153
|
+
classification,
|
|
1154
|
+
planQa: task.plan.qa,
|
|
1155
|
+
preflight: options?.qaPreflight,
|
|
1156
|
+
overrides: options?.qaOverrides,
|
|
1157
|
+
});
|
|
667
1158
|
const depSlugs = (task.plan.dependsOnKeys ?? [])
|
|
668
1159
|
.map((dep) => localToKey.get(dep))
|
|
669
1160
|
.filter((value) => Boolean(value));
|
|
@@ -673,12 +1164,28 @@ export class CreateTasksService {
|
|
|
673
1164
|
userStoryId: storyId,
|
|
674
1165
|
key: task.key,
|
|
675
1166
|
title: task.plan.title ?? `Task ${task.key}`,
|
|
676
|
-
description: buildTaskDescription(task.key, task.plan.title ?? `Task ${task.key}`, task.plan.description, task.storyKey, task.epicKey, task.plan.relatedDocs, depSlugs
|
|
1167
|
+
description: buildTaskDescription(task.key, task.plan.title ?? `Task ${task.key}`, task.plan.description, task.storyKey, task.epicKey, task.plan.relatedDocs, depSlugs, {
|
|
1168
|
+
unitTests: task.plan.unitTests,
|
|
1169
|
+
componentTests: task.plan.componentTests,
|
|
1170
|
+
integrationTests: task.plan.integrationTests,
|
|
1171
|
+
apiTests: task.plan.apiTests,
|
|
1172
|
+
}, qaReadiness),
|
|
677
1173
|
type: task.plan.type ?? "feature",
|
|
678
1174
|
status: "not_started",
|
|
679
1175
|
storyPoints: task.plan.estimatedStoryPoints ?? null,
|
|
680
1176
|
priority: task.plan.priorityHint ?? (taskInserts.length + 1),
|
|
681
|
-
metadata:
|
|
1177
|
+
metadata: {
|
|
1178
|
+
doc_links: task.plan.relatedDocs ?? [],
|
|
1179
|
+
test_requirements: {
|
|
1180
|
+
unit: task.plan.unitTests ?? [],
|
|
1181
|
+
component: task.plan.componentTests ?? [],
|
|
1182
|
+
integration: task.plan.integrationTests ?? [],
|
|
1183
|
+
api: task.plan.apiTests ?? [],
|
|
1184
|
+
},
|
|
1185
|
+
stage: classification.stage,
|
|
1186
|
+
foundation: classification.foundation,
|
|
1187
|
+
qa: qaReadiness,
|
|
1188
|
+
},
|
|
682
1189
|
});
|
|
683
1190
|
}
|
|
684
1191
|
taskRows = await this.workspaceRepo.insertTasks(taskInserts, false);
|
|
@@ -771,11 +1278,18 @@ export class CreateTasksService {
|
|
|
771
1278
|
const docs = await this.prepareDocs(options.inputs);
|
|
772
1279
|
const { docSummary, warnings: docWarnings } = this.buildDocContext(docs);
|
|
773
1280
|
const { prompt } = this.buildPrompt(options.projectKey, docs, options);
|
|
1281
|
+
const qaPreflight = await this.buildQaPreflight();
|
|
1282
|
+
const qaOverrides = this.buildQaOverrides(options);
|
|
774
1283
|
await this.jobService.writeCheckpoint(job.id, {
|
|
775
1284
|
stage: "docs_indexed",
|
|
776
1285
|
timestamp: new Date().toISOString(),
|
|
777
1286
|
details: { count: docs.length, warnings: docWarnings },
|
|
778
1287
|
});
|
|
1288
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
1289
|
+
stage: "qa_preflight",
|
|
1290
|
+
timestamp: new Date().toISOString(),
|
|
1291
|
+
details: qaPreflight,
|
|
1292
|
+
});
|
|
779
1293
|
const agent = await this.resolveAgent(options.agentName);
|
|
780
1294
|
const { output: epicOutput } = await this.invokeAgentWithRetry(agent, prompt, "epics", agentStream, job.id, commandRun.id, { docWarnings });
|
|
781
1295
|
const epics = this.parseEpics(epicOutput, docs, options.projectKey).slice(0, options.maxEpics ?? Number.MAX_SAFE_INTEGER);
|
|
@@ -810,7 +1324,10 @@ export class CreateTasksService {
|
|
|
810
1324
|
const { epics: epicRows, stories: storyRows, tasks: taskRows, dependencies: dependencyRows } = await this.persistPlanToDb(project.id, options.projectKey, plan, job.id, commandRun.id, {
|
|
811
1325
|
force: options.force,
|
|
812
1326
|
resetKeys: options.force,
|
|
1327
|
+
qaPreflight,
|
|
1328
|
+
qaOverrides,
|
|
813
1329
|
});
|
|
1330
|
+
await this.seedPriorities(options.projectKey);
|
|
814
1331
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
815
1332
|
payload: {
|
|
816
1333
|
epicsCreated: epicRows.length,
|
|
@@ -822,6 +1339,27 @@ export class CreateTasksService {
|
|
|
822
1339
|
},
|
|
823
1340
|
});
|
|
824
1341
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
1342
|
+
if (options.rateAgents) {
|
|
1343
|
+
try {
|
|
1344
|
+
const ratingService = this.ensureRatingService();
|
|
1345
|
+
await ratingService.rate({
|
|
1346
|
+
workspace: this.workspace,
|
|
1347
|
+
agentId: agent.id,
|
|
1348
|
+
commandName: "create-tasks",
|
|
1349
|
+
jobId: job.id,
|
|
1350
|
+
commandRunId: commandRun.id,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
catch (error) {
|
|
1354
|
+
const message = `Agent rating failed: ${error.message ?? String(error)}`;
|
|
1355
|
+
try {
|
|
1356
|
+
await this.jobService.appendLog(job.id, `${message}\n`);
|
|
1357
|
+
}
|
|
1358
|
+
catch {
|
|
1359
|
+
/* ignore rating log failures */
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
825
1363
|
return {
|
|
826
1364
|
jobId: job.id,
|
|
827
1365
|
commandRunId: commandRun.id,
|
|
@@ -860,7 +1398,7 @@ export class CreateTasksService {
|
|
|
860
1398
|
commandName: "migrate-tasks",
|
|
861
1399
|
payload: { projectKey, planDir: options.planDir },
|
|
862
1400
|
});
|
|
863
|
-
const planDir = options.planDir ?? path.join(this.workspace.
|
|
1401
|
+
const planDir = options.planDir ?? path.join(this.workspace.mcodaDir, "tasks", projectKey);
|
|
864
1402
|
try {
|
|
865
1403
|
const planPath = path.join(planDir, "plan.json");
|
|
866
1404
|
const loadJson = async (file) => {
|