@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
@@ -1,16 +1,21 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { AgentService } from "@mcoda/agents";
4
5
  import { DocdexClient } from "@mcoda/integrations";
5
6
  import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
6
- import { canonicalizeCommandName, getCommandRequiredCapabilities } from "@mcoda/shared";
7
+ import { READY_TO_CODE_REVIEW, canonicalizeCommandName, getCommandRequiredCapabilities } from "@mcoda/shared";
7
8
  import { JobService } from "../jobs/JobService.js";
8
9
  import { TaskSelectionService } from "../execution/TaskSelectionService.js";
9
10
  import { RoutingService } from "./RoutingService.js";
11
+ import { isDocContextExcluded, normalizeDocType } from "../shared/ProjectGuidance.js";
10
12
  const DEFAULT_GATEWAY_PROMPT = [
11
13
  "You are the gateway agent. Read the task context and docdex snippets, digest the task, decide what is done vs. remaining, and plan the work.",
12
14
  "You must identify concrete file paths to modify or create before offloading.",
15
+ "If new directories are required for planned files, list them explicitly in dirsToCreate.",
13
16
  "Do not use placeholders like (unknown), TBD, or glob patterns in file paths.",
17
+ "Do not assume repository structure; only name paths grounded in provided file content or docdex context.",
18
+ "If you add or modify tests, ensure tests/all.js is updated (or state that it already covers the new tests).",
14
19
  "If docdex returns no results, say so in docdexNotes.",
15
20
  "Do not leave currentState, todo, or understanding blank.",
16
21
  "Put reasoningSummary near the top of the JSON object so it appears early in the stream.",
@@ -28,12 +33,15 @@ const DEFAULT_GATEWAY_PROMPT = [
28
33
  ' "discipline": "backend|frontend|uiux|docs|architecture|qa|planning|ops|other",',
29
34
  ' "filesLikelyTouched": ["path/to/file.ext"],',
30
35
  ' "filesToCreate": ["path/to/new_file.ext"],',
36
+ ' "dirsToCreate": ["path/to/new_dir"],',
31
37
  ' "assumptions": ["assumption 1"],',
32
38
  ' "risks": ["risk 1"],',
33
39
  ' "docdexNotes": ["notes about docdex coverage/gaps"]',
34
40
  "}",
35
41
  "If information is missing, keep arrays empty and mention the gap in assumptions or docdexNotes.",
36
42
  ].join("\n");
43
+ const REPO_PROMPTS_DIR = fileURLToPath(new URL("../../../../../prompts/", import.meta.url));
44
+ const resolveRepoPromptPath = (filename) => path.join(REPO_PROMPTS_DIR, filename);
37
45
  const REQUIRED_PROMPT_MARKERS = [
38
46
  '"summary"',
39
47
  '"reasoningSummary"',
@@ -42,27 +50,63 @@ const REQUIRED_PROMPT_MARKERS = [
42
50
  '"understanding"',
43
51
  '"filesLikelyTouched"',
44
52
  '"filesToCreate"',
53
+ '"dirsToCreate"',
45
54
  ];
46
55
  const hasRequiredPromptMarkers = (content) => REQUIRED_PROMPT_MARKERS.every((marker) => content.includes(marker));
47
56
  const DEFAULT_JOB_PROMPT = "You are an mcoda agent that follows workspace runbooks and responds with actionable, concise output.";
48
57
  const DEFAULT_CHARACTER_PROMPT = "Write clearly, avoid hallucinations, cite assumptions, and prioritize risk mitigation for the user.";
49
- const extractJson = (raw) => {
50
- const fenced = raw.match(/```json([\s\S]*?)```/);
51
- const candidate = fenced ? fenced[1] : raw;
52
- const start = candidate.indexOf("{");
53
- const end = candidate.lastIndexOf("}");
54
- if (start === -1 || end === -1 || end <= start)
55
- return undefined;
56
- const body = candidate.slice(start, end + 1);
58
+ const ROUTING_PROMPT_MARKERS = [
59
+ "routing gateway",
60
+ "choose a route",
61
+ "devstral-local",
62
+ "glm-worker",
63
+ "codex-architect",
64
+ "complexity from 1 to 5",
65
+ ];
66
+ const sanitizeAgentOutput = (output) => {
67
+ if (!output.includes("[agent-io]"))
68
+ return output;
69
+ const lines = output.split(/\r?\n/);
70
+ const cleaned = [];
71
+ for (const line of lines) {
72
+ if (!line.includes("[agent-io]")) {
73
+ cleaned.push(line);
74
+ continue;
75
+ }
76
+ if (/^\s*\[agent-io\]\s*(?:begin|input|meta)/i.test(line)) {
77
+ continue;
78
+ }
79
+ const stripped = line.replace(/^\s*\[agent-io\]\s*(?:output\s*)?/i, "");
80
+ if (stripped.trim())
81
+ cleaned.push(stripped);
82
+ }
83
+ return cleaned.join("\n");
84
+ };
85
+ const extractJsonOnly = (raw) => {
86
+ const trimmed = raw.trim();
87
+ if (!trimmed)
88
+ return { jsonOnly: false };
89
+ let candidate = trimmed;
90
+ if (trimmed.startsWith("```")) {
91
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)```$/i);
92
+ if (!match)
93
+ return { jsonOnly: false };
94
+ candidate = match[1].trim();
95
+ }
96
+ else if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
97
+ return { jsonOnly: false };
98
+ }
57
99
  try {
58
- return JSON.parse(body);
100
+ return { payload: JSON.parse(candidate), jsonOnly: true };
59
101
  }
60
102
  catch {
61
- return undefined;
103
+ return { jsonOnly: true };
62
104
  }
63
105
  };
64
106
  const estimateTokens = (text) => Math.max(1, Math.ceil((text ?? "").length / 4));
65
107
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
108
+ const STRONG_TIER_MIN_COMPLEXITY = 5;
109
+ const SPECIALIST_TIER_MIN_COMPLEXITY = 8;
66
110
  const normalizeList = (value) => {
67
111
  if (Array.isArray(value))
68
112
  return value.map((item) => String(item)).filter(Boolean);
@@ -81,6 +125,20 @@ const normalizeTextField = (value) => {
81
125
  }
82
126
  return undefined;
83
127
  };
128
+ const isRoutingPrompt = (value) => {
129
+ const lower = value.toLowerCase();
130
+ return ROUTING_PROMPT_MARKERS.some((marker) => lower.includes(marker));
131
+ };
132
+ const sanitizeGatewayPrompt = (value) => {
133
+ if (!value)
134
+ return undefined;
135
+ const trimmed = value.trim();
136
+ if (!trimmed)
137
+ return undefined;
138
+ if (isRoutingPrompt(trimmed))
139
+ return undefined;
140
+ return trimmed;
141
+ };
84
142
  const isPlaceholderPath = (value) => {
85
143
  const lower = value.trim().toLowerCase();
86
144
  if (!lower)
@@ -93,7 +151,77 @@ const isPlaceholderPath = (value) => {
93
151
  return false;
94
152
  };
95
153
  const normalizeFileList = (value) => normalizeList(value).map((item) => item.trim()).filter((item) => item.length > 0 && !isPlaceholderPath(item));
96
- const listMissingFields = (raw) => {
154
+ const normalizeDirList = (value) => normalizeList(value)
155
+ .map((item) => item.trim().replace(/[/\\]+$/, ""))
156
+ .filter((item) => item.length > 0 && !isPlaceholderPath(item));
157
+ const isDirMarker = (value) => {
158
+ const trimmed = value.trim();
159
+ if (!trimmed)
160
+ return false;
161
+ if (/[/\\]$/.test(trimmed))
162
+ return true;
163
+ if (/^\s*(?:dir|directory)\s*:/i.test(trimmed))
164
+ return true;
165
+ if (/\(\s*dir\s*\)\s*$/i.test(trimmed))
166
+ return true;
167
+ return false;
168
+ };
169
+ const stripDirMarker = (value) => {
170
+ let trimmed = value.trim();
171
+ trimmed = trimmed.replace(/^\s*(?:dir|directory)\s*:/i, "");
172
+ trimmed = trimmed.replace(/\(\s*dir\s*\)\s*$/i, "");
173
+ trimmed = trimmed.replace(/[/\\]+$/, "");
174
+ return trimmed.trim();
175
+ };
176
+ const splitFileAndDirEntries = (values) => {
177
+ const files = [];
178
+ const dirs = [];
179
+ for (const value of values) {
180
+ const trimmed = value.trim();
181
+ if (!trimmed)
182
+ continue;
183
+ if (isDirMarker(trimmed)) {
184
+ const cleaned = stripDirMarker(trimmed);
185
+ if (cleaned)
186
+ dirs.push(cleaned);
187
+ continue;
188
+ }
189
+ files.push(trimmed);
190
+ }
191
+ return { files, dirs };
192
+ };
193
+ const hasFileContextJustification = (raw) => {
194
+ const notes = [...normalizeList(raw?.assumptions), ...normalizeList(raw?.docdexNotes)]
195
+ .map((item) => item.toLowerCase())
196
+ .join(" ");
197
+ if (!notes.trim())
198
+ return false;
199
+ const patterns = [
200
+ /\bno file(?:s)?\b/,
201
+ /\bmissing file(?:s)?\b/,
202
+ /\bunknown file(?:s)?\b/,
203
+ /\bfile context\b/,
204
+ /\binsufficient context\b/,
205
+ /\bnot enough context\b/,
206
+ /\bnot provided\b/,
207
+ /\bdocdex unavailable\b/,
208
+ /\bdocdex missing\b/,
209
+ /\bno matching docs?\b/,
210
+ /\bno matching documents?\b/,
211
+ /\bno results\b/,
212
+ ];
213
+ return patterns.some((pattern) => pattern.test(notes));
214
+ };
215
+ const assessFileListCoverage = (raw) => {
216
+ const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
217
+ const rawCreate = normalizeList(raw?.filesToCreate);
218
+ const splitCreate = splitFileAndDirEntries(rawCreate);
219
+ const filesToCreate = normalizeFileList(splitCreate.files);
220
+ const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
221
+ const empty = filesLikelyTouched.length === 0 && filesToCreate.length === 0 && dirsToCreate.length === 0;
222
+ return { empty, justified: empty && hasFileContextJustification(raw) };
223
+ };
224
+ const listMissingFields = (raw, options) => {
97
225
  const missing = [];
98
226
  const summary = normalizeTextField(raw?.summary);
99
227
  const reasoningSummary = normalizeTextField(raw?.reasoningSummary);
@@ -102,7 +230,11 @@ const listMissingFields = (raw) => {
102
230
  const understanding = normalizeTextField(raw?.understanding);
103
231
  const plan = normalizeList(raw?.plan);
104
232
  const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
105
- const filesToCreate = normalizeFileList(raw?.filesToCreate);
233
+ const rawCreate = normalizeList(raw?.filesToCreate);
234
+ const splitCreate = splitFileAndDirEntries(rawCreate);
235
+ const filesToCreate = normalizeFileList(splitCreate.files);
236
+ const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
237
+ const allowEmptyFiles = options?.allowEmptyFiles ?? false;
106
238
  if (!summary)
107
239
  missing.push("summary");
108
240
  if (!reasoningSummary)
@@ -115,8 +247,9 @@ const listMissingFields = (raw) => {
115
247
  missing.push("understanding");
116
248
  if (plan.length === 0)
117
249
  missing.push("plan");
118
- if (filesLikelyTouched.length === 0 && filesToCreate.length === 0)
119
- missing.push("files");
250
+ if (!allowEmptyFiles && filesLikelyTouched.length === 0 && filesToCreate.length === 0 && dirsToCreate.length === 0) {
251
+ missing.push("filesLikelyTouched", "filesToCreate", "dirsToCreate");
252
+ }
120
253
  return missing;
121
254
  };
122
255
  const normalizeDiscipline = (value) => {
@@ -174,24 +307,33 @@ const scoreUsage = (discipline, bestUsage, capabilities) => {
174
307
  score += 0.5;
175
308
  return score;
176
309
  };
310
+ const EXPLORATION_RATE = 0.1;
177
311
  const DEFAULT_STATUS_FILTER = [
178
312
  "not_started",
179
313
  "in_progress",
180
- "blocked",
181
- "ready_to_review",
314
+ READY_TO_CODE_REVIEW,
182
315
  "ready_to_qa",
183
316
  "completed",
184
317
  "cancelled",
185
318
  "failed",
186
319
  "skipped",
187
320
  ];
188
- const summarizeDoc = (doc, index) => {
321
+ const summarizeDoc = (doc, index, warnings) => {
189
322
  const title = doc.title ?? doc.path ?? doc.id ?? `doc-${index + 1}`;
190
323
  const excerptSource = doc.segments?.[0]?.content ?? doc.content ?? "";
191
324
  const excerpt = excerptSource ? (excerptSource.length > 480 ? `${excerptSource.slice(0, 480)}...` : excerptSource) : undefined;
325
+ const normalized = normalizeDocType({
326
+ docType: doc.docType,
327
+ path: doc.path,
328
+ title: doc.title,
329
+ content: excerptSource,
330
+ });
331
+ if (normalized.downgraded && warnings) {
332
+ warnings.push(`Docdex docType downgraded from SDS to DOC for ${doc.path ?? doc.title ?? doc.id}: ${normalized.reason ?? "not_sds"}`);
333
+ }
192
334
  return {
193
335
  id: doc.id ?? `doc-${index + 1}`,
194
- docType: doc.docType,
336
+ docType: normalized.docType,
195
337
  title,
196
338
  path: doc.path,
197
339
  excerpt,
@@ -210,6 +352,25 @@ const buildDocContext = (docs) => {
210
352
  }),
211
353
  ].join("\n");
212
354
  };
355
+ const PLACEHOLDER_DEPENDENCY_PATTERN = /^(?:t|task-?)\d+$/i;
356
+ const normalizeDependencies = (deps) => {
357
+ if (!deps?.length)
358
+ return undefined;
359
+ const seen = new Set();
360
+ const filtered = [];
361
+ for (const dep of deps) {
362
+ const trimmed = dep.trim();
363
+ if (!trimmed)
364
+ continue;
365
+ if (PLACEHOLDER_DEPENDENCY_PATTERN.test(trimmed))
366
+ continue;
367
+ if (seen.has(trimmed))
368
+ continue;
369
+ seen.add(trimmed);
370
+ filtered.push(trimmed);
371
+ }
372
+ return filtered.length ? filtered : undefined;
373
+ };
213
374
  const buildTaskContext = (tasks) => {
214
375
  if (tasks.length === 0)
215
376
  return "Task context: (no task records found)";
@@ -240,9 +401,11 @@ export class GatewayAgentService {
240
401
  const globalRepo = await GlobalRepository.create();
241
402
  const agentService = new AgentService(globalRepo);
242
403
  const routingService = await RoutingService.create();
404
+ const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
243
405
  const docdex = new DocdexClient({
244
406
  workspaceRoot: workspace.workspaceRoot,
245
407
  baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
408
+ repoId: docdexRepoId,
246
409
  });
247
410
  const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
248
411
  const jobService = new JobService(workspace, workspaceRepo);
@@ -291,10 +454,31 @@ export class GatewayAgentService {
291
454
  await maybeClose(this.deps.workspaceRepo);
292
455
  await maybeClose(this.deps.routingService);
293
456
  }
457
+ setDocdexAvailability(available, reason) {
458
+ if (available)
459
+ return;
460
+ const docdex = this.deps.docdex;
461
+ if (docdex && typeof docdex.disable === "function") {
462
+ docdex.disable(reason);
463
+ }
464
+ }
465
+ async saveRepoMemory(text) {
466
+ const docdex = this.deps.docdex;
467
+ if (!docdex || typeof docdex.memorySave !== "function")
468
+ return;
469
+ await docdex.memorySave(text);
470
+ }
471
+ async savePreference(category, content, agentId = "default") {
472
+ const docdex = this.deps.docdex;
473
+ if (!docdex || typeof docdex.savePreference !== "function")
474
+ return;
475
+ await docdex.savePreference(agentId, category, content);
476
+ }
294
477
  async loadGatewayPrompts(agentId) {
295
478
  const agentPrompts = "getPrompts" in this.deps.agentService ? await this.deps.agentService.getPrompts(agentId) : undefined;
296
- const mcodaPromptPath = path.join(this.workspace.workspaceRoot, ".mcoda", "prompts", "gateway-agent.md");
479
+ const mcodaPromptPath = path.join(this.workspace.mcodaDir, "prompts", "gateway-agent.md");
297
480
  const workspacePromptPath = path.join(this.workspace.workspaceRoot, "prompts", "gateway-agent.md");
481
+ const repoPromptPath = resolveRepoPromptPath("gateway-agent.md");
298
482
  try {
299
483
  await fs.promises.mkdir(path.dirname(mcodaPromptPath), { recursive: true });
300
484
  await fs.promises.access(mcodaPromptPath);
@@ -305,7 +489,13 @@ export class GatewayAgentService {
305
489
  await fs.promises.copyFile(workspacePromptPath, mcodaPromptPath);
306
490
  }
307
491
  catch {
308
- await fs.promises.writeFile(mcodaPromptPath, DEFAULT_GATEWAY_PROMPT, "utf8");
492
+ try {
493
+ await fs.promises.access(repoPromptPath);
494
+ await fs.promises.copyFile(repoPromptPath, mcodaPromptPath);
495
+ }
496
+ catch {
497
+ await fs.promises.writeFile(mcodaPromptPath, DEFAULT_GATEWAY_PROMPT, "utf8");
498
+ }
309
499
  }
310
500
  }
311
501
  try {
@@ -319,7 +509,15 @@ export class GatewayAgentService {
319
509
  }
320
510
  }
321
511
  catch {
322
- /* ignore */
512
+ try {
513
+ const repoPrompt = await fs.promises.readFile(repoPromptPath, "utf8");
514
+ if (hasRequiredPromptMarkers(repoPrompt)) {
515
+ nextPrompt = repoPrompt.trim();
516
+ }
517
+ }
518
+ catch {
519
+ /* ignore */
520
+ }
323
521
  }
324
522
  await fs.promises.writeFile(mcodaPromptPath, nextPrompt, "utf8");
325
523
  }
@@ -327,7 +525,7 @@ export class GatewayAgentService {
327
525
  catch {
328
526
  /* ignore */
329
527
  }
330
- const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath])).filter(hasRequiredPromptMarkers);
528
+ const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath, repoPromptPath])).filter(hasRequiredPromptMarkers);
331
529
  const mergedCommandPrompt = (() => {
332
530
  const parts = [...commandPromptFiles];
333
531
  const agentCommandPrompt = agentPrompts?.commandPrompts?.["gateway-agent"];
@@ -338,9 +536,11 @@ export class GatewayAgentService {
338
536
  parts.push(DEFAULT_GATEWAY_PROMPT);
339
537
  return parts.filter(Boolean).join("\n\n");
340
538
  })();
539
+ const sanitizedJobPrompt = sanitizeGatewayPrompt(agentPrompts?.jobPrompt);
540
+ const sanitizedCharacterPrompt = sanitizeGatewayPrompt(agentPrompts?.characterPrompt);
341
541
  return {
342
- jobPrompt: agentPrompts?.jobPrompt ?? DEFAULT_JOB_PROMPT,
343
- characterPrompt: agentPrompts?.characterPrompt ?? DEFAULT_CHARACTER_PROMPT,
542
+ jobPrompt: sanitizedJobPrompt ?? DEFAULT_JOB_PROMPT,
543
+ characterPrompt: sanitizedCharacterPrompt ?? DEFAULT_CHARACTER_PROMPT,
344
544
  commandPrompt: mergedCommandPrompt,
345
545
  };
346
546
  }
@@ -362,14 +562,15 @@ export class GatewayAgentService {
362
562
  const caps = await this.deps.globalRepo.getAgentCapabilities(overrideAgent.id);
363
563
  const missing = requiredCaps.filter((cap) => !caps.includes(cap));
364
564
  const health = await this.deps.globalRepo.getAgentHealth(overrideAgent.id);
365
- if (missing.length === 0 && health?.status !== "unreachable") {
366
- return overrideAgent;
565
+ if (health?.status === "unreachable") {
566
+ warnings.push(`Override agent ${overrideAgent.slug} is unreachable; ignoring override.`);
367
567
  }
368
- if (missing.length) {
369
- warnings.push(`Override agent ${overrideAgent.slug} is missing gateway capabilities (${missing.join(", ")}); ignoring override.`);
568
+ else if (missing.length === 0) {
569
+ return overrideAgent;
370
570
  }
371
- else if (health?.status === "unreachable") {
372
- warnings.push(`Override agent ${overrideAgent.slug} is unreachable; ignoring override.`);
571
+ else {
572
+ warnings.push(`Override agent ${overrideAgent.slug} is missing gateway capabilities (${missing.join(", ")}); proceeding with override as requested.`);
573
+ return overrideAgent;
373
574
  }
374
575
  }
375
576
  catch (overrideError) {
@@ -411,7 +612,7 @@ export class GatewayAgentService {
411
612
  if (text && onChunk)
412
613
  onChunk(text);
413
614
  }
414
- return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
615
+ return { output: sanitizeAgentOutput(output), durationSeconds: (Date.now() - startedAt) / 1000 };
415
616
  }
416
617
  }
417
618
  catch (error) {
@@ -424,7 +625,7 @@ export class GatewayAgentService {
424
625
  input: prompt,
425
626
  metadata: { command: "gateway-agent", job },
426
627
  });
427
- const output = response.output ?? "";
628
+ const output = sanitizeAgentOutput(response.output ?? "");
428
629
  if (output && onChunk)
429
630
  onChunk(output);
430
631
  return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
@@ -445,27 +646,81 @@ export class GatewayAgentService {
445
646
  taskKeys: request.taskKeys,
446
647
  statusFilter: request.statusFilter?.length ? request.statusFilter : DEFAULT_STATUS_FILTER,
447
648
  limit,
649
+ ignoreDependencies: request.taskKeys && request.taskKeys.length > 0 ? true : request.ignoreDependencies,
448
650
  };
449
651
  const selection = await this.taskSelectionService.selectTasks(filters);
450
652
  if (selection.warnings.length)
451
653
  warnings.push(...selection.warnings);
452
- const combined = [...selection.ordered, ...selection.blocked];
453
- return combined.slice(0, limit).map((entry) => ({
454
- key: entry.task.key,
455
- title: entry.task.title,
456
- description: entry.task.description ?? undefined,
457
- status: entry.task.status,
458
- storyPoints: entry.task.storyPoints ?? undefined,
459
- storyKey: entry.task.storyKey,
460
- storyTitle: entry.task.storyTitle,
461
- epicKey: entry.task.epicKey,
462
- epicTitle: entry.task.epicTitle,
463
- acceptanceCriteria: entry.task.acceptanceCriteria,
464
- dependencies: entry.dependencies.keys,
654
+ const combined = [...selection.ordered];
655
+ if (combined.length) {
656
+ return combined.slice(0, limit).map((entry) => ({
657
+ key: entry.task.key,
658
+ title: entry.task.title,
659
+ description: entry.task.description ?? undefined,
660
+ status: entry.task.status,
661
+ storyPoints: entry.task.storyPoints ?? undefined,
662
+ storyKey: entry.task.storyKey,
663
+ storyTitle: entry.task.storyTitle,
664
+ epicKey: entry.task.epicKey,
665
+ epicTitle: entry.task.epicTitle,
666
+ acceptanceCriteria: entry.task.acceptanceCriteria,
667
+ dependencies: normalizeDependencies(entry.dependencies.keys),
668
+ }));
669
+ }
670
+ if (!request.taskKeys?.length)
671
+ return [];
672
+ warnings.push("Gateway task selection returned no tasks; falling back to direct task lookup.");
673
+ const tasks = await Promise.all(request.taskKeys.map((key) => this.deps.workspaceRepo.getTaskByKey(key)));
674
+ const found = tasks.filter((task) => Boolean(task));
675
+ const missing = request.taskKeys.filter((key, index) => !tasks[index]);
676
+ if (missing.length) {
677
+ warnings.push(`Fallback task lookup missing keys: ${missing.join(", ")}`);
678
+ }
679
+ if (!found.length)
680
+ return [];
681
+ let withRelations = [];
682
+ try {
683
+ const related = await this.deps.workspaceRepo.getTasksWithRelations(found.map((task) => task.id));
684
+ const relatedById = new Map(related.map((task) => [task.id, task]));
685
+ withRelations = found.map((task) => relatedById.get(task.id) ?? task);
686
+ }
687
+ catch {
688
+ withRelations = found;
689
+ }
690
+ return withRelations.slice(0, limit).map((task) => ({
691
+ key: task.key,
692
+ title: task.title,
693
+ description: task.description ?? undefined,
694
+ status: task.status,
695
+ storyPoints: task.storyPoints ?? undefined,
696
+ storyKey: task.storyKey,
697
+ storyTitle: task.storyTitle,
698
+ epicKey: task.epicKey,
699
+ epicTitle: task.epicTitle,
700
+ acceptanceCriteria: task.acceptanceCriteria,
701
+ dependencies: undefined,
465
702
  }));
466
703
  }
467
704
  catch (error) {
468
705
  warnings.push(`Task lookup failed: ${error.message}`);
706
+ if (request.taskKeys?.length) {
707
+ warnings.push("Task lookup failed; attempting direct task fallback.");
708
+ try {
709
+ const tasks = await Promise.all(request.taskKeys.map((key) => this.deps.workspaceRepo.getTaskByKey(key)));
710
+ const found = tasks.filter((task) => Boolean(task));
711
+ return found.map((task) => ({
712
+ key: task.key,
713
+ title: task.title,
714
+ description: task.description ?? undefined,
715
+ status: task.status,
716
+ storyPoints: task.storyPoints ?? undefined,
717
+ dependencies: undefined,
718
+ }));
719
+ }
720
+ catch {
721
+ return [];
722
+ }
723
+ }
469
724
  return [];
470
725
  }
471
726
  }
@@ -509,22 +764,59 @@ export class GatewayAgentService {
509
764
  const maxDocs = request.maxDocs ?? 4;
510
765
  if (maxDocs <= 0)
511
766
  return [];
767
+ if (typeof this.deps.docdex?.ensureRepoScope === "function") {
768
+ try {
769
+ await this.deps.docdex.ensureRepoScope();
770
+ }
771
+ catch (error) {
772
+ warnings.push(`Docdex scope missing: ${error.message}`);
773
+ return [];
774
+ }
775
+ }
512
776
  const docTypes = this.pickDocTypes(request.job, request.inputText);
513
777
  const query = this.buildQuerySeed(tasks, request.inputText);
514
778
  const summaries = [];
779
+ let openApiIncluded = false;
780
+ let reindexed = false;
515
781
  for (const docType of docTypes) {
516
782
  if (summaries.length >= maxDocs)
517
783
  break;
518
784
  try {
519
- const docs = await this.deps.docdex.search({
785
+ let docs = await this.deps.docdex.search({
520
786
  projectKey: request.projectKey,
521
787
  docType,
522
788
  query,
523
789
  });
790
+ if (!docs.length && !reindexed && typeof this.deps.docdex.reindex === "function") {
791
+ reindexed = true;
792
+ try {
793
+ warnings.push("Docdex search returned no results; reindexing and retrying.");
794
+ await this.deps.docdex.reindex();
795
+ docs = await this.deps.docdex.search({
796
+ projectKey: request.projectKey,
797
+ docType,
798
+ query,
799
+ });
800
+ }
801
+ catch (error) {
802
+ warnings.push(`Docdex reindex failed: ${error.message}`);
803
+ }
804
+ }
524
805
  for (const doc of docs) {
525
806
  if (summaries.length >= maxDocs)
526
807
  break;
527
- summaries.push(summarizeDoc(doc, summaries.length));
808
+ const ref = doc.path ?? doc.title ?? doc.id;
809
+ if (isDocContextExcluded(ref, false)) {
810
+ warnings.push(`Docdex doc filtered from gateway context: ${ref}`);
811
+ continue;
812
+ }
813
+ const summary = summarizeDoc(doc, summaries.length, warnings);
814
+ if (summary.docType === "OPENAPI") {
815
+ if (openApiIncluded)
816
+ continue;
817
+ openApiIncluded = true;
818
+ }
819
+ summaries.push(summary);
528
820
  }
529
821
  }
530
822
  catch (error) {
@@ -547,7 +839,10 @@ export class GatewayAgentService {
547
839
  const understanding = normalizeTextField(raw?.understanding) ?? "";
548
840
  const plan = normalizeList(raw?.plan);
549
841
  const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
550
- const filesToCreate = normalizeFileList(raw?.filesToCreate);
842
+ const rawCreate = normalizeList(raw?.filesToCreate);
843
+ const splitCreate = splitFileAndDirEntries(rawCreate);
844
+ const filesToCreate = normalizeFileList(splitCreate.files);
845
+ const dirsToCreate = [...normalizeDirList(raw?.dirsToCreate), ...normalizeDirList(splitCreate.dirs)];
551
846
  const complexityRaw = Number(raw?.complexity);
552
847
  const complexity = Number.isFinite(complexityRaw) ? clamp(Math.round(complexityRaw), 1, 10) : 5;
553
848
  const discipline = normalizeDiscipline(typeof raw?.discipline === "string" ? raw.discipline : undefined) ??
@@ -577,6 +872,7 @@ export class GatewayAgentService {
577
872
  discipline,
578
873
  filesLikelyTouched,
579
874
  filesToCreate,
875
+ dirsToCreate,
580
876
  assumptions: normalizeList(raw?.assumptions),
581
877
  risks: normalizeList(raw?.risks),
582
878
  docdexNotes: normalizeList(raw?.docdexNotes),
@@ -592,6 +888,36 @@ export class GatewayAgentService {
592
888
  const isInside = (relative) => !relative.startsWith("..") && !path.isAbsolute(relative);
593
889
  const touched = [];
594
890
  const created = [];
891
+ const dirs = [];
892
+ const dirSet = new Set();
893
+ for (const dir of analysis.dirsToCreate ?? []) {
894
+ const { relative, resolved } = normalize(dir);
895
+ if (!isInside(relative)) {
896
+ warnings.push(`Gateway directory path outside workspace ignored: ${dir}`);
897
+ continue;
898
+ }
899
+ try {
900
+ const stat = await fs.promises.stat(resolved);
901
+ if (stat.isDirectory()) {
902
+ const normalized = relative.replace(/\\/g, "/");
903
+ if (!dirSet.has(normalized)) {
904
+ dirSet.add(normalized);
905
+ dirs.push(normalized);
906
+ }
907
+ continue;
908
+ }
909
+ warnings.push(`Gateway directory path is not a directory: ${dir}`);
910
+ continue;
911
+ }
912
+ catch {
913
+ const normalized = relative.replace(/\\/g, "/");
914
+ if (!dirSet.has(normalized)) {
915
+ dirSet.add(normalized);
916
+ dirs.push(normalized);
917
+ }
918
+ }
919
+ }
920
+ const dirPrefixes = Array.from(dirSet).map((dir) => (dir.endsWith("/") ? dir : `${dir}/`));
595
921
  for (const file of analysis.filesLikelyTouched) {
596
922
  const { relative, resolved } = normalize(file);
597
923
  if (!isInside(relative)) {
@@ -626,8 +952,14 @@ export class GatewayAgentService {
626
952
  }
627
953
  }
628
954
  catch {
629
- warnings.push(`Gateway create path parent does not exist: ${file}`);
630
- continue;
955
+ const parentRelative = path.relative(root, parent).replace(/\\/g, "/");
956
+ const parentAllowed = parentRelative === "" ||
957
+ dirSet.has(parentRelative) ||
958
+ dirPrefixes.some((prefix) => parentRelative.startsWith(prefix));
959
+ if (!parentAllowed) {
960
+ warnings.push(`Gateway create path parent does not exist: ${file}`);
961
+ continue;
962
+ }
631
963
  }
632
964
  try {
633
965
  const stat = await fs.promises.stat(resolved);
@@ -646,17 +978,22 @@ export class GatewayAgentService {
646
978
  ...analysis,
647
979
  filesLikelyTouched: touched,
648
980
  filesToCreate: created,
981
+ dirsToCreate: dirs,
649
982
  };
650
983
  }
651
- async listCandidates(requiredCaps, discipline) {
984
+ async listCandidates(requiredCaps, discipline, avoidAgents = []) {
652
985
  const agents = await this.deps.globalRepo.listAgents();
653
986
  if (agents.length === 0) {
654
987
  throw new Error("No agents available; register one with mcoda agent add");
655
988
  }
989
+ const avoidSet = new Set(avoidAgents.map((value) => value.toLowerCase()));
656
990
  const health = await this.deps.globalRepo.listAgentHealthSummary();
657
991
  const healthById = new Map(health.map((row) => [row.agentId, row]));
658
992
  const candidates = [];
659
993
  for (const agent of agents) {
994
+ const slug = agent.slug ?? agent.id;
995
+ if (avoidSet.has(agent.id.toLowerCase()) || avoidSet.has(slug.toLowerCase()))
996
+ continue;
660
997
  const capabilities = await this.deps.globalRepo.getAgentCapabilities(agent.id);
661
998
  const missing = requiredCaps.filter((cap) => !capabilities.includes(cap));
662
999
  if (missing.length)
@@ -672,6 +1009,9 @@ export class GatewayAgentService {
672
1009
  const usageScore = scoreUsage(discipline, agent.bestUsage, capabilities);
673
1010
  const cost = agent.costPerMillion ?? Number.POSITIVE_INFINITY;
674
1011
  const adjustedQuality = healthEntry?.status === "degraded" ? quality - 0.5 : quality;
1012
+ const maxComplexity = typeof agent.maxComplexity === "number" && Number.isFinite(agent.maxComplexity)
1013
+ ? clamp(Math.round(agent.maxComplexity), 1, 10)
1014
+ : 5;
675
1015
  candidates.push({
676
1016
  agent,
677
1017
  capabilities,
@@ -680,6 +1020,7 @@ export class GatewayAgentService {
680
1020
  reasoning,
681
1021
  usageScore,
682
1022
  cost,
1023
+ maxComplexity,
683
1024
  });
684
1025
  }
685
1026
  return candidates;
@@ -688,10 +1029,42 @@ export class GatewayAgentService {
688
1029
  if (candidates.length === 0) {
689
1030
  throw new Error("No eligible agents available for this job");
690
1031
  }
691
- const sortedQuality = candidates.map((c) => c.quality);
1032
+ const normalizedComplexity = clamp(Math.round(complexity), 1, 10);
1033
+ const eligible = candidates.filter((c) => c.maxComplexity >= normalizedComplexity);
1034
+ let pool = eligible;
1035
+ let gatingNote = "";
1036
+ if (!eligible.length) {
1037
+ const fallback = normalizedComplexity > 1
1038
+ ? candidates.filter((c) => c.maxComplexity >= normalizedComplexity - 1)
1039
+ : candidates;
1040
+ pool = fallback.length ? fallback : candidates;
1041
+ gatingNote = fallback.length
1042
+ ? ` No agents meet max complexity ${normalizedComplexity}; allowing ${normalizedComplexity - 1} fallback.`
1043
+ : ` No agents meet max complexity ${normalizedComplexity}; using best available.`;
1044
+ }
1045
+ if (Math.random() < EXPLORATION_RATE) {
1046
+ const stretchPool = normalizedComplexity > 1
1047
+ ? candidates.filter((c) => c.maxComplexity < normalizedComplexity && c.maxComplexity >= normalizedComplexity - 1)
1048
+ : [];
1049
+ const allowRedemption = normalizedComplexity <= 4;
1050
+ const sortedByQuality = pool.slice().sort((a, b) => a.quality - b.quality);
1051
+ const redemptionPool = allowRedemption ? sortedByQuality.slice(0, Math.max(1, Math.ceil(pool.length * 0.2))) : [];
1052
+ const canUseStretch = stretchPool.length > 0;
1053
+ const canUseRedemption = redemptionPool.length > 0;
1054
+ if (canUseStretch || canUseRedemption) {
1055
+ const useStretch = canUseStretch && (!canUseRedemption || Math.random() < 0.5);
1056
+ const explorePool = useStretch ? stretchPool : redemptionPool;
1057
+ const pick = explorePool[Math.floor(Math.random() * explorePool.length)];
1058
+ const rationale = useStretch
1059
+ ? `Exploration: stretching an agent (max complexity ${pick.maxComplexity}) for task complexity ${normalizedComplexity}/10.${gatingNote}`
1060
+ : `Exploration: redemption run for a lower-rated agent to reassess performance.${gatingNote}`;
1061
+ return { pick, rationale };
1062
+ }
1063
+ }
1064
+ const sortedQuality = pool.map((c) => c.quality);
692
1065
  const maxQuality = Math.max(...sortedQuality);
693
- if (complexity >= 9) {
694
- const pick = candidates
1066
+ if (normalizedComplexity >= 9) {
1067
+ const pick = pool
695
1068
  .slice()
696
1069
  .sort((a, b) => {
697
1070
  if (b.quality !== a.quality)
@@ -704,12 +1077,12 @@ export class GatewayAgentService {
704
1077
  })[0];
705
1078
  return {
706
1079
  pick,
707
- rationale: `Complexity ${complexity}/10 requires the highest capability; selected top-rated agent with best fit for ${discipline}.`,
1080
+ rationale: `Complexity ${normalizedComplexity}/10 requires the highest capability; selected top-rated agent with best fit for ${discipline}.${gatingNote}`,
708
1081
  };
709
1082
  }
710
- if (complexity >= 8) {
711
- const pool = candidates.filter((c) => c.quality >= maxQuality - 1);
712
- const pick = (pool.length ? pool : candidates)
1083
+ if (normalizedComplexity >= 8) {
1084
+ const qualityPool = pool.filter((c) => c.quality >= maxQuality - 1);
1085
+ const pick = (qualityPool.length ? qualityPool : pool)
713
1086
  .slice()
714
1087
  .sort((a, b) => {
715
1088
  if (b.usageScore !== a.usageScore)
@@ -720,12 +1093,12 @@ export class GatewayAgentService {
720
1093
  })[0];
721
1094
  return {
722
1095
  pick,
723
- rationale: `Complexity ${complexity}/10 favors strong agents with good cost/fit balance; selected best-fit candidate.`,
1096
+ rationale: `Complexity ${normalizedComplexity}/10 favors strong agents with good cost/fit balance; selected best-fit candidate.${gatingNote}`,
724
1097
  };
725
1098
  }
726
- const target = complexity;
727
- const pool = candidates.filter((c) => c.quality >= target);
728
- const base = pool.length ? pool : candidates;
1099
+ const target = normalizedComplexity;
1100
+ const qualityPool = pool.filter((c) => c.quality >= target);
1101
+ const base = qualityPool.length ? qualityPool : pool;
729
1102
  const pick = base
730
1103
  .slice()
731
1104
  .sort((a, b) => {
@@ -741,14 +1114,49 @@ export class GatewayAgentService {
741
1114
  })[0];
742
1115
  return {
743
1116
  pick,
744
- rationale: `Complexity ${complexity}/10 targets a comparable tier agent; selected closest match with discipline fit and cost awareness.`,
1117
+ rationale: `Complexity ${normalizedComplexity}/10 targets a comparable tier agent; selected closest match with discipline fit and cost awareness.${gatingNote}`,
745
1118
  };
746
1119
  }
747
- async selectAgentForJob(job, analysis) {
1120
+ async selectAgentForJob(job, analysis, avoidAgents = [], forceStronger = false, forceTier, warnings = []) {
748
1121
  const normalizedJob = canonicalizeCommandName(job);
749
1122
  const requiredCaps = getCommandRequiredCapabilities(normalizedJob);
750
- const candidates = await this.listCandidates(requiredCaps, analysis.discipline);
751
- const { pick, rationale } = this.chooseCandidate(candidates, analysis.complexity, analysis.discipline);
1123
+ let candidates = await this.listCandidates(requiredCaps, analysis.discipline, avoidAgents);
1124
+ if (!candidates.length && avoidAgents.length) {
1125
+ const fallback = await this.listCandidates(requiredCaps, analysis.discipline, []);
1126
+ if (fallback.length) {
1127
+ candidates = fallback;
1128
+ warnings.push("Avoid list removed all eligible agents; reusing available agent.");
1129
+ }
1130
+ }
1131
+ const baseComplexity = Number.isFinite(analysis.complexity)
1132
+ ? clamp(Math.round(analysis.complexity), 1, 10)
1133
+ : 5;
1134
+ const forcedMin = forceTier === "specialist"
1135
+ ? SPECIALIST_TIER_MIN_COMPLEXITY
1136
+ : forceTier === "strong"
1137
+ ? STRONG_TIER_MIN_COMPLEXITY
1138
+ : undefined;
1139
+ let boostedComplexity = baseComplexity;
1140
+ if (typeof forcedMin === "number") {
1141
+ boostedComplexity = Math.max(baseComplexity, forcedMin);
1142
+ }
1143
+ else if (forceStronger) {
1144
+ if (baseComplexity < STRONG_TIER_MIN_COMPLEXITY) {
1145
+ boostedComplexity = STRONG_TIER_MIN_COMPLEXITY;
1146
+ }
1147
+ else if (baseComplexity < SPECIALIST_TIER_MIN_COMPLEXITY) {
1148
+ boostedComplexity = SPECIALIST_TIER_MIN_COMPLEXITY;
1149
+ }
1150
+ else {
1151
+ boostedComplexity = clamp(baseComplexity + 1, 1, 10);
1152
+ }
1153
+ }
1154
+ const { pick, rationale } = this.chooseCandidate(candidates, boostedComplexity, analysis.discipline);
1155
+ const finalRationale = forceTier
1156
+ ? `${rationale} (force_tier:${forceTier})`
1157
+ : forceStronger
1158
+ ? `${rationale} (force_stronger applied)`
1159
+ : rationale;
752
1160
  return {
753
1161
  agentId: pick.agent.id,
754
1162
  agentSlug: pick.agent.slug ?? pick.agent.id,
@@ -756,9 +1164,26 @@ export class GatewayAgentService {
756
1164
  reasoningRating: pick.agent.reasoningRating ?? undefined,
757
1165
  bestUsage: pick.agent.bestUsage ?? undefined,
758
1166
  costPerMillion: Number.isFinite(pick.cost) ? pick.cost : undefined,
759
- rationale,
1167
+ rationale: finalRationale,
760
1168
  };
761
1169
  }
1170
+ async preflightExecutionAgents(job, overrideAgent) {
1171
+ const normalizedJob = canonicalizeCommandName(job);
1172
+ const requiredCaps = getCommandRequiredCapabilities(normalizedJob);
1173
+ if (overrideAgent) {
1174
+ const resolved = await this.deps.agentService.resolveAgent(overrideAgent);
1175
+ const capabilities = await this.deps.globalRepo.getAgentCapabilities(resolved.id);
1176
+ const missing = requiredCaps.filter((cap) => !capabilities.includes(cap));
1177
+ if (missing.length) {
1178
+ throw new Error(`Agent ${overrideAgent} is missing required capabilities for ${normalizedJob}: ${missing.join(", ")}`);
1179
+ }
1180
+ return;
1181
+ }
1182
+ const candidates = await this.listCandidates(requiredCaps, "other");
1183
+ if (!candidates.length) {
1184
+ throw new Error(`No eligible execution agents available for ${normalizedJob}.`);
1185
+ }
1186
+ }
762
1187
  async run(request) {
763
1188
  const warnings = [];
764
1189
  const normalizedJob = canonicalizeCommandName(request.job);
@@ -776,7 +1201,7 @@ export class GatewayAgentService {
776
1201
  ]
777
1202
  .filter(Boolean)
778
1203
  .join("\n\n");
779
- const recordUsage = async (promptText, outputText, durationSeconds, action) => {
1204
+ const recordUsage = async (promptText, outputText, durationSeconds, action, attempt) => {
780
1205
  const promptTokens = estimateTokens(promptText);
781
1206
  const completionTokens = estimateTokens(outputText ?? "");
782
1207
  await this.deps.jobService.recordTokenUsage({
@@ -792,20 +1217,24 @@ export class GatewayAgentService {
792
1217
  tokensCompletion: completionTokens,
793
1218
  tokensTotal: promptTokens + completionTokens,
794
1219
  durationSeconds,
795
- metadata: { action, job: normalizedJob },
1220
+ metadata: { action, job: normalizedJob, phase: action, attempt },
796
1221
  });
797
1222
  };
798
1223
  const response = await this.invokeGatewayAgent(gatewayAgent, prompt, normalizedJob, {
799
1224
  stream: request.agentStream !== false,
800
1225
  onChunk: request.onStreamChunk,
801
1226
  });
802
- await recordUsage(prompt, response.output ?? "", response.durationSeconds, "gateway_summary");
803
- let parsed = extractJson(response.output);
1227
+ await recordUsage(prompt, response.output ?? "", response.durationSeconds, "gateway_summary", 1);
1228
+ const jsonResult = extractJsonOnly(response.output);
1229
+ let parsed = jsonResult.payload;
1230
+ let fileListCoverage = parsed ? assessFileListCoverage(parsed) : { empty: false, justified: false };
804
1231
  let missingFields = parsed
805
- ? listMissingFields(parsed)
806
- : ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "files"];
1232
+ ? listMissingFields(parsed, { allowEmptyFiles: true })
1233
+ : ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "filesLikelyTouched", "filesToCreate", "dirsToCreate"];
807
1234
  if (!parsed) {
808
- warnings.push("Gateway analysis response was not valid JSON; falling back to defaults.");
1235
+ warnings.push(jsonResult.jsonOnly
1236
+ ? "Gateway analysis response was invalid JSON."
1237
+ : "Gateway analysis response was not JSON-only.");
809
1238
  }
810
1239
  if (missingFields.length) {
811
1240
  const repairPrompt = [
@@ -813,7 +1242,8 @@ export class GatewayAgentService {
813
1242
  "",
814
1243
  "Your previous response was incomplete or invalid. Return JSON only with the exact schema.",
815
1244
  `Missing fields: ${missingFields.join(", ")}.`,
816
- "Ensure reasoningSummary, currentState, todo, understanding, plan, and filesLikelyTouched/filesToCreate are populated.",
1245
+ "Ensure reasoningSummary, currentState, todo, understanding, and plan are populated.",
1246
+ "If file paths are unknown, leave filesLikelyTouched/filesToCreate/dirsToCreate empty and explain the gap in assumptions or docdexNotes.",
817
1247
  "Use real file paths only (no placeholders like (unknown), TBD, or glob patterns).",
818
1248
  "If docdex returned no results, say so in docdexNotes.",
819
1249
  ].join("\n");
@@ -824,18 +1254,29 @@ export class GatewayAgentService {
824
1254
  stream: request.agentStream !== false,
825
1255
  onChunk: request.onStreamChunk,
826
1256
  });
827
- await recordUsage(repairPrompt, repairResponse.output ?? "", repairResponse.durationSeconds, "gateway_summary_repair");
828
- const repaired = extractJson(repairResponse.output);
829
- if (repaired) {
830
- parsed = repaired;
831
- missingFields = listMissingFields(parsed);
1257
+ await recordUsage(repairPrompt, repairResponse.output ?? "", repairResponse.durationSeconds, "gateway_summary_repair", 2);
1258
+ const repaired = extractJsonOnly(repairResponse.output);
1259
+ if (repaired.payload) {
1260
+ parsed = repaired.payload;
1261
+ fileListCoverage = assessFileListCoverage(parsed);
1262
+ missingFields = listMissingFields(parsed, { allowEmptyFiles: true });
832
1263
  }
833
1264
  else {
834
- warnings.push("Gateway repair response was not valid JSON; using fallback analysis.");
1265
+ warnings.push(repaired.jsonOnly
1266
+ ? "Gateway repair response was invalid JSON."
1267
+ : "Gateway repair response was not JSON-only.");
835
1268
  }
836
1269
  }
837
1270
  if (missingFields.length) {
838
- warnings.push(`Gateway analysis missing fields: ${missingFields.join(", ")}.`);
1271
+ throw new Error(`Gateway analysis missing required fields: ${missingFields.join(", ")}.`);
1272
+ }
1273
+ if (!parsed) {
1274
+ throw new Error(jsonResult.jsonOnly
1275
+ ? "Gateway analysis response was invalid JSON."
1276
+ : "Gateway analysis response was not JSON-only.");
1277
+ }
1278
+ if (fileListCoverage.empty && !fileListCoverage.justified) {
1279
+ warnings.push("Gateway analysis returned no file paths; proceeding without file context.");
839
1280
  }
840
1281
  let analysis = this.normalizeAnalysis(parsed ?? {}, normalizedJob, tasks, request.inputText);
841
1282
  if (analysis.docdexNotes.length === 0) {
@@ -849,7 +1290,7 @@ export class GatewayAgentService {
849
1290
  analysis.docdexNotes.push(...docdexWarnings);
850
1291
  }
851
1292
  analysis = await this.validateFilePlan(analysis, warnings);
852
- const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis);
1293
+ const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis, request.avoidAgents ?? [], request.forceStronger ?? false, request.forceTier, warnings);
853
1294
  await this.deps.jobService.finishCommandRun(commandRun.id, "succeeded");
854
1295
  return {
855
1296
  commandRunId: commandRun.id,