@mcoda/core 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +2 -2
  3. package/dist/api/AgentsApi.d.ts +9 -1
  4. package/dist/api/AgentsApi.d.ts.map +1 -1
  5. package/dist/api/AgentsApi.js +201 -6
  6. package/dist/api/QaTasksApi.d.ts.map +1 -1
  7. package/dist/api/QaTasksApi.js +6 -0
  8. package/dist/api/TasksApi.d.ts.map +1 -1
  9. package/dist/api/TasksApi.js +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  14. package/dist/prompts/PdrPrompts.js +9 -1
  15. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  16. package/dist/prompts/SdsPrompts.js +9 -0
  17. package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
  18. package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
  19. package/dist/services/agents/AgentRatingFormula.js +45 -0
  20. package/dist/services/agents/AgentRatingService.d.ts +60 -0
  21. package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
  22. package/dist/services/agents/AgentRatingService.js +363 -0
  23. package/dist/services/agents/GatewayAgentService.d.ts +11 -0
  24. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
  25. package/dist/services/agents/GatewayAgentService.js +525 -84
  26. package/dist/services/agents/GatewayHandoff.d.ts +11 -0
  27. package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
  28. package/dist/services/agents/GatewayHandoff.js +141 -0
  29. package/dist/services/agents/RoutingService.d.ts +1 -0
  30. package/dist/services/agents/RoutingService.d.ts.map +1 -1
  31. package/dist/services/agents/RoutingService.js +4 -4
  32. package/dist/services/backlog/BacklogService.d.ts +23 -0
  33. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  34. package/dist/services/backlog/BacklogService.js +62 -7
  35. package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
  36. package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
  37. package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
  38. package/dist/services/backlog/TaskOrderingService.d.ts +17 -4
  39. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  40. package/dist/services/backlog/TaskOrderingService.js +538 -79
  41. package/dist/services/docs/DocInventory.d.ts +11 -0
  42. package/dist/services/docs/DocInventory.d.ts.map +1 -0
  43. package/dist/services/docs/DocInventory.js +230 -0
  44. package/dist/services/docs/DocgenRunContext.d.ts +59 -0
  45. package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
  46. package/dist/services/docs/DocgenRunContext.js +4 -0
  47. package/dist/services/docs/DocsService.d.ts +70 -3
  48. package/dist/services/docs/DocsService.d.ts.map +1 -1
  49. package/dist/services/docs/DocsService.js +1930 -89
  50. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
  51. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
  52. package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
  53. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
  54. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
  55. package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
  56. package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
  57. package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
  58. package/dist/services/docs/patch/DocPatchEngine.js +331 -0
  59. package/dist/services/docs/review/Glossary.d.ts +16 -0
  60. package/dist/services/docs/review/Glossary.d.ts.map +1 -0
  61. package/dist/services/docs/review/Glossary.js +47 -0
  62. package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
  63. package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
  64. package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
  65. package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
  66. package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
  67. package/dist/services/docs/review/ReviewReportSchema.js +47 -0
  68. package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
  69. package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
  70. package/dist/services/docs/review/ReviewTypes.js +94 -0
  71. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
  72. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
  73. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
  74. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
  75. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
  76. package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
  77. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
  78. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
  79. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
  80. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
  81. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
  82. package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
  83. package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
  84. package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
  85. package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
  86. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
  87. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
  88. package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
  89. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
  90. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
  91. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
  92. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
  93. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
  94. package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
  95. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
  96. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
  97. package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
  98. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
  99. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
  100. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
  101. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
  102. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
  103. package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
  104. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
  105. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
  106. package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
  107. package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
  108. package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
  109. package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
  110. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
  111. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
  112. package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
  113. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
  114. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
  115. package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
  116. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
  117. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
  118. package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
  119. package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
  120. package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
  121. package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
  122. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
  123. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
  124. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
  125. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
  126. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
  127. package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
  128. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
  129. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
  130. package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
  131. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
  132. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
  133. package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
  134. package/dist/services/docs/review/glossary.json +47 -0
  135. package/dist/services/estimate/EstimateService.d.ts +2 -0
  136. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  137. package/dist/services/estimate/EstimateService.js +66 -18
  138. package/dist/services/estimate/VelocityService.d.ts +4 -0
  139. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  140. package/dist/services/estimate/VelocityService.js +179 -36
  141. package/dist/services/estimate/types.d.ts +1 -0
  142. package/dist/services/estimate/types.d.ts.map +1 -1
  143. package/dist/services/execution/GatewayTrioService.d.ts +200 -0
  144. package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
  145. package/dist/services/execution/GatewayTrioService.js +2492 -0
  146. package/dist/services/execution/QaApiRunner.d.ts +30 -0
  147. package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
  148. package/dist/services/execution/QaApiRunner.js +881 -0
  149. package/dist/services/execution/QaFollowupService.d.ts +2 -0
  150. package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
  151. package/dist/services/execution/QaFollowupService.js +9 -2
  152. package/dist/services/execution/QaPlanValidator.d.ts +10 -0
  153. package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
  154. package/dist/services/execution/QaPlanValidator.js +128 -0
  155. package/dist/services/execution/QaProfileService.d.ts +27 -1
  156. package/dist/services/execution/QaProfileService.d.ts.map +1 -1
  157. package/dist/services/execution/QaProfileService.js +354 -7
  158. package/dist/services/execution/QaTasksService.d.ts +59 -1
  159. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  160. package/dist/services/execution/QaTasksService.js +3347 -318
  161. package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
  162. package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
  163. package/dist/services/execution/QaTestCommandBuilder.js +495 -0
  164. package/dist/services/execution/TaskSelectionService.d.ts +4 -2
  165. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  166. package/dist/services/execution/TaskSelectionService.js +144 -28
  167. package/dist/services/execution/TaskStateService.d.ts +19 -6
  168. package/dist/services/execution/TaskStateService.d.ts.map +1 -1
  169. package/dist/services/execution/TaskStateService.js +128 -13
  170. package/dist/services/execution/WorkOnTasksService.d.ts +32 -1
  171. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  172. package/dist/services/execution/WorkOnTasksService.js +4667 -722
  173. package/dist/services/jobs/JobInsightsService.d.ts +4 -0
  174. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
  175. package/dist/services/jobs/JobInsightsService.js +51 -5
  176. package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
  177. package/dist/services/jobs/JobResumeService.js +23 -10
  178. package/dist/services/jobs/JobService.d.ts +56 -4
  179. package/dist/services/jobs/JobService.d.ts.map +1 -1
  180. package/dist/services/jobs/JobService.js +232 -1
  181. package/dist/services/openapi/OpenApiService.d.ts +51 -0
  182. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  183. package/dist/services/openapi/OpenApiService.js +953 -106
  184. package/dist/services/planning/CreateTasksService.d.ts +21 -0
  185. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  186. package/dist/services/planning/CreateTasksService.js +569 -31
  187. package/dist/services/planning/RefineTasksService.d.ts +9 -0
  188. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  189. package/dist/services/planning/RefineTasksService.js +409 -59
  190. package/dist/services/review/CodeReviewService.d.ts +18 -0
  191. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  192. package/dist/services/review/CodeReviewService.js +1309 -167
  193. package/dist/services/review/ReviewNormalizer.d.ts +9 -0
  194. package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
  195. package/dist/services/review/ReviewNormalizer.js +147 -0
  196. package/dist/services/shared/AuthErrors.d.ts +3 -0
  197. package/dist/services/shared/AuthErrors.d.ts.map +1 -0
  198. package/dist/services/shared/AuthErrors.js +17 -0
  199. package/dist/services/shared/DocdexGuidance.d.ts +7 -0
  200. package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
  201. package/dist/services/shared/DocdexGuidance.js +12 -0
  202. package/dist/services/shared/ProjectGuidance.d.ts +17 -0
  203. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
  204. package/dist/services/shared/ProjectGuidance.js +78 -0
  205. package/dist/services/system/ToolDenylist.d.ts +13 -0
  206. package/dist/services/system/ToolDenylist.d.ts.map +1 -0
  207. package/dist/services/system/ToolDenylist.js +85 -0
  208. package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
  209. package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
  210. package/dist/services/tasks/TaskCommentFormatter.js +54 -0
  211. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
  212. package/dist/services/telemetry/TelemetryService.js +39 -7
  213. package/dist/workspace/WorkspaceManager.d.ts +26 -0
  214. package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
  215. package/dist/workspace/WorkspaceManager.js +206 -32
  216. package/package.json +6 -5
@@ -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 fenced = raw.match(/```json([\s\S]*?)```/);
36
- const candidate = fenced ? fenced[1] : raw;
37
- const start = candidate.indexOf("{");
38
- const end = candidate.lastIndexOf("}");
39
- if (start === -1 || end === -1 || end <= start)
40
- return undefined;
41
- const body = candidate.slice(start, end + 1);
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(body);
211
+ return JSON.parse(value);
44
212
  }
45
213
  catch {
46
- return undefined;
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
- "- Unit/integration coverage for changed areas.",
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 inputs) {
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: { action: `create_tasks_${action}`, ...(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: Array.isArray(epic.relatedDocs) ? epic.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: Array.isArray(story.relatedDocs) ? story.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
- localId: task.localId ?? `t${idx + 1}`,
531
- title: task.title ?? "Task",
532
- type: task.type,
533
- description: task.description,
534
- estimatedStoryPoints: typeof task.estimatedStoryPoints === "number" ? task.estimatedStoryPoints : undefined,
535
- priorityHint: typeof task.priorityHint === "number" ? task.priorityHint : undefined,
536
- dependsOnKeys: Array.isArray(task.dependsOnKeys) ? task.dependsOnKeys : [],
537
- relatedDocs: Array.isArray(task.relatedDocs) ? task.relatedDocs : [],
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.workspaceRoot, ".mcoda", "tasks", projectKey);
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: task.plan.relatedDocs ? { doc_links: task.plan.relatedDocs } : undefined,
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.workspaceRoot, ".mcoda", "tasks", projectKey);
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) => {