@mcoda/core 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +2 -2
  2. package/dist/api/AgentsApi.d.ts +1 -0
  3. package/dist/api/AgentsApi.d.ts.map +1 -1
  4. package/dist/api/AgentsApi.js +136 -11
  5. package/dist/api/QaTasksApi.d.ts.map +1 -1
  6. package/dist/api/QaTasksApi.js +4 -0
  7. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  8. package/dist/prompts/PdrPrompts.js +6 -0
  9. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  10. package/dist/prompts/SdsPrompts.js +7 -0
  11. package/dist/services/agents/AgentRatingService.d.ts +19 -0
  12. package/dist/services/agents/AgentRatingService.d.ts.map +1 -1
  13. package/dist/services/agents/AgentRatingService.js +66 -2
  14. package/dist/services/agents/GatewayAgentService.d.ts +8 -0
  15. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
  16. package/dist/services/agents/GatewayAgentService.js +462 -65
  17. package/dist/services/agents/GatewayHandoff.d.ts +5 -1
  18. package/dist/services/agents/GatewayHandoff.d.ts.map +1 -1
  19. package/dist/services/agents/GatewayHandoff.js +65 -32
  20. package/dist/services/agents/RoutingService.d.ts +1 -0
  21. package/dist/services/agents/RoutingService.d.ts.map +1 -1
  22. package/dist/services/agents/RoutingService.js +4 -4
  23. package/dist/services/backlog/BacklogService.d.ts +23 -0
  24. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  25. package/dist/services/backlog/BacklogService.js +62 -7
  26. package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
  27. package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
  28. package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
  29. package/dist/services/backlog/TaskOrderingService.d.ts +16 -4
  30. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  31. package/dist/services/backlog/TaskOrderingService.js +529 -73
  32. package/dist/services/docs/DocInventory.d.ts +11 -0
  33. package/dist/services/docs/DocInventory.d.ts.map +1 -0
  34. package/dist/services/docs/DocInventory.js +230 -0
  35. package/dist/services/docs/DocgenRunContext.d.ts +59 -0
  36. package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
  37. package/dist/services/docs/DocgenRunContext.js +4 -0
  38. package/dist/services/docs/DocsService.d.ts +59 -2
  39. package/dist/services/docs/DocsService.d.ts.map +1 -1
  40. package/dist/services/docs/DocsService.js +1701 -48
  41. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
  42. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
  43. package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
  44. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
  45. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
  46. package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
  47. package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
  48. package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
  49. package/dist/services/docs/patch/DocPatchEngine.js +331 -0
  50. package/dist/services/docs/review/Glossary.d.ts +16 -0
  51. package/dist/services/docs/review/Glossary.d.ts.map +1 -0
  52. package/dist/services/docs/review/Glossary.js +47 -0
  53. package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
  54. package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
  55. package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
  56. package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
  57. package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
  58. package/dist/services/docs/review/ReviewReportSchema.js +47 -0
  59. package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
  60. package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
  61. package/dist/services/docs/review/ReviewTypes.js +94 -0
  62. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
  63. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
  64. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
  65. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
  66. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
  67. package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
  68. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
  69. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
  70. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
  71. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
  72. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
  73. package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
  74. package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
  75. package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
  76. package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
  77. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
  78. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
  79. package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
  80. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
  81. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
  82. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
  83. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
  84. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
  85. package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
  86. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
  87. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
  88. package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
  89. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
  90. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
  91. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
  92. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
  93. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
  94. package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
  95. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
  96. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
  97. package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
  98. package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
  99. package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
  100. package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
  101. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
  102. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
  103. package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
  104. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
  105. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
  106. package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
  107. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
  108. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
  109. package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
  110. package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
  111. package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
  112. package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
  113. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
  114. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
  115. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
  116. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
  117. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
  118. package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
  119. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
  120. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
  121. package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
  122. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
  123. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
  124. package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
  125. package/dist/services/docs/review/glossary.json +47 -0
  126. package/dist/services/estimate/EstimateService.d.ts +2 -0
  127. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  128. package/dist/services/estimate/EstimateService.js +66 -18
  129. package/dist/services/estimate/VelocityService.d.ts +4 -0
  130. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  131. package/dist/services/estimate/VelocityService.js +179 -36
  132. package/dist/services/estimate/types.d.ts +1 -0
  133. package/dist/services/estimate/types.d.ts.map +1 -1
  134. package/dist/services/execution/GatewayTrioService.d.ts +71 -4
  135. package/dist/services/execution/GatewayTrioService.d.ts.map +1 -1
  136. package/dist/services/execution/GatewayTrioService.js +1695 -328
  137. package/dist/services/execution/QaApiRunner.d.ts +30 -0
  138. package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
  139. package/dist/services/execution/QaApiRunner.js +881 -0
  140. package/dist/services/execution/QaFollowupService.d.ts +1 -0
  141. package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
  142. package/dist/services/execution/QaFollowupService.js +8 -2
  143. package/dist/services/execution/QaPlanValidator.d.ts +10 -0
  144. package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
  145. package/dist/services/execution/QaPlanValidator.js +128 -0
  146. package/dist/services/execution/QaProfileService.d.ts +21 -1
  147. package/dist/services/execution/QaProfileService.d.ts.map +1 -1
  148. package/dist/services/execution/QaProfileService.js +214 -29
  149. package/dist/services/execution/QaTasksService.d.ts +41 -1
  150. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  151. package/dist/services/execution/QaTasksService.js +2851 -500
  152. package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
  153. package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
  154. package/dist/services/execution/QaTestCommandBuilder.js +495 -0
  155. package/dist/services/execution/TaskSelectionService.d.ts +4 -2
  156. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  157. package/dist/services/execution/TaskSelectionService.js +144 -28
  158. package/dist/services/execution/TaskStateService.d.ts +19 -6
  159. package/dist/services/execution/TaskStateService.d.ts.map +1 -1
  160. package/dist/services/execution/TaskStateService.js +128 -13
  161. package/dist/services/execution/WorkOnTasksService.d.ts +19 -2
  162. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  163. package/dist/services/execution/WorkOnTasksService.js +3913 -1225
  164. package/dist/services/jobs/JobInsightsService.d.ts +4 -0
  165. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
  166. package/dist/services/jobs/JobInsightsService.js +51 -5
  167. package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
  168. package/dist/services/jobs/JobResumeService.js +23 -10
  169. package/dist/services/jobs/JobService.d.ts +56 -4
  170. package/dist/services/jobs/JobService.d.ts.map +1 -1
  171. package/dist/services/jobs/JobService.js +232 -1
  172. package/dist/services/openapi/OpenApiService.d.ts +41 -0
  173. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  174. package/dist/services/openapi/OpenApiService.js +889 -98
  175. package/dist/services/planning/CreateTasksService.d.ts +15 -0
  176. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  177. package/dist/services/planning/CreateTasksService.js +311 -6
  178. package/dist/services/planning/RefineTasksService.d.ts +4 -0
  179. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  180. package/dist/services/planning/RefineTasksService.js +225 -24
  181. package/dist/services/review/CodeReviewService.d.ts +4 -0
  182. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  183. package/dist/services/review/CodeReviewService.js +778 -232
  184. package/dist/services/review/ReviewNormalizer.d.ts +9 -0
  185. package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
  186. package/dist/services/review/ReviewNormalizer.js +147 -0
  187. package/dist/services/shared/AuthErrors.d.ts +3 -0
  188. package/dist/services/shared/AuthErrors.d.ts.map +1 -0
  189. package/dist/services/shared/AuthErrors.js +17 -0
  190. package/dist/services/shared/DocdexGuidance.d.ts +7 -0
  191. package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
  192. package/dist/services/shared/DocdexGuidance.js +12 -0
  193. package/dist/services/shared/ProjectGuidance.d.ts +12 -1
  194. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
  195. package/dist/services/shared/ProjectGuidance.js +64 -7
  196. package/dist/services/system/ToolDenylist.d.ts +13 -0
  197. package/dist/services/system/ToolDenylist.d.ts.map +1 -0
  198. package/dist/services/system/ToolDenylist.js +85 -0
  199. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
  200. package/dist/services/telemetry/TelemetryService.js +39 -7
  201. package/dist/workspace/WorkspaceManager.d.ts +22 -0
  202. package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
  203. package/dist/workspace/WorkspaceManager.js +203 -32
  204. 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) => {
@@ -178,21 +311,29 @@ const EXPLORATION_RATE = 0.1;
178
311
  const DEFAULT_STATUS_FILTER = [
179
312
  "not_started",
180
313
  "in_progress",
181
- "blocked",
182
- "ready_to_review",
314
+ READY_TO_CODE_REVIEW,
183
315
  "ready_to_qa",
184
316
  "completed",
185
317
  "cancelled",
186
318
  "failed",
187
319
  "skipped",
188
320
  ];
189
- const summarizeDoc = (doc, index) => {
321
+ const summarizeDoc = (doc, index, warnings) => {
190
322
  const title = doc.title ?? doc.path ?? doc.id ?? `doc-${index + 1}`;
191
323
  const excerptSource = doc.segments?.[0]?.content ?? doc.content ?? "";
192
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
+ }
193
334
  return {
194
335
  id: doc.id ?? `doc-${index + 1}`,
195
- docType: doc.docType,
336
+ docType: normalized.docType,
196
337
  title,
197
338
  path: doc.path,
198
339
  excerpt,
@@ -211,6 +352,25 @@ const buildDocContext = (docs) => {
211
352
  }),
212
353
  ].join("\n");
213
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
+ };
214
374
  const buildTaskContext = (tasks) => {
215
375
  if (tasks.length === 0)
216
376
  return "Task context: (no task records found)";
@@ -241,9 +401,11 @@ export class GatewayAgentService {
241
401
  const globalRepo = await GlobalRepository.create();
242
402
  const agentService = new AgentService(globalRepo);
243
403
  const routingService = await RoutingService.create();
404
+ const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
244
405
  const docdex = new DocdexClient({
245
406
  workspaceRoot: workspace.workspaceRoot,
246
407
  baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
408
+ repoId: docdexRepoId,
247
409
  });
248
410
  const workspaceRepo = await WorkspaceRepository.create(workspace.workspaceRoot);
249
411
  const jobService = new JobService(workspace, workspaceRepo);
@@ -292,10 +454,31 @@ export class GatewayAgentService {
292
454
  await maybeClose(this.deps.workspaceRepo);
293
455
  await maybeClose(this.deps.routingService);
294
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
+ }
295
477
  async loadGatewayPrompts(agentId) {
296
478
  const agentPrompts = "getPrompts" in this.deps.agentService ? await this.deps.agentService.getPrompts(agentId) : undefined;
297
- const mcodaPromptPath = path.join(this.workspace.workspaceRoot, ".mcoda", "prompts", "gateway-agent.md");
479
+ const mcodaPromptPath = path.join(this.workspace.mcodaDir, "prompts", "gateway-agent.md");
298
480
  const workspacePromptPath = path.join(this.workspace.workspaceRoot, "prompts", "gateway-agent.md");
481
+ const repoPromptPath = resolveRepoPromptPath("gateway-agent.md");
299
482
  try {
300
483
  await fs.promises.mkdir(path.dirname(mcodaPromptPath), { recursive: true });
301
484
  await fs.promises.access(mcodaPromptPath);
@@ -306,7 +489,13 @@ export class GatewayAgentService {
306
489
  await fs.promises.copyFile(workspacePromptPath, mcodaPromptPath);
307
490
  }
308
491
  catch {
309
- 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
+ }
310
499
  }
311
500
  }
312
501
  try {
@@ -320,7 +509,15 @@ export class GatewayAgentService {
320
509
  }
321
510
  }
322
511
  catch {
323
- /* 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
+ }
324
521
  }
325
522
  await fs.promises.writeFile(mcodaPromptPath, nextPrompt, "utf8");
326
523
  }
@@ -328,7 +525,7 @@ export class GatewayAgentService {
328
525
  catch {
329
526
  /* ignore */
330
527
  }
331
- const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath])).filter(hasRequiredPromptMarkers);
528
+ const commandPromptFiles = (await this.readPromptFiles([mcodaPromptPath, workspacePromptPath, repoPromptPath])).filter(hasRequiredPromptMarkers);
332
529
  const mergedCommandPrompt = (() => {
333
530
  const parts = [...commandPromptFiles];
334
531
  const agentCommandPrompt = agentPrompts?.commandPrompts?.["gateway-agent"];
@@ -339,9 +536,11 @@ export class GatewayAgentService {
339
536
  parts.push(DEFAULT_GATEWAY_PROMPT);
340
537
  return parts.filter(Boolean).join("\n\n");
341
538
  })();
539
+ const sanitizedJobPrompt = sanitizeGatewayPrompt(agentPrompts?.jobPrompt);
540
+ const sanitizedCharacterPrompt = sanitizeGatewayPrompt(agentPrompts?.characterPrompt);
342
541
  return {
343
- jobPrompt: agentPrompts?.jobPrompt ?? DEFAULT_JOB_PROMPT,
344
- characterPrompt: agentPrompts?.characterPrompt ?? DEFAULT_CHARACTER_PROMPT,
542
+ jobPrompt: sanitizedJobPrompt ?? DEFAULT_JOB_PROMPT,
543
+ characterPrompt: sanitizedCharacterPrompt ?? DEFAULT_CHARACTER_PROMPT,
345
544
  commandPrompt: mergedCommandPrompt,
346
545
  };
347
546
  }
@@ -413,7 +612,7 @@ export class GatewayAgentService {
413
612
  if (text && onChunk)
414
613
  onChunk(text);
415
614
  }
416
- return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
615
+ return { output: sanitizeAgentOutput(output), durationSeconds: (Date.now() - startedAt) / 1000 };
417
616
  }
418
617
  }
419
618
  catch (error) {
@@ -426,7 +625,7 @@ export class GatewayAgentService {
426
625
  input: prompt,
427
626
  metadata: { command: "gateway-agent", job },
428
627
  });
429
- const output = response.output ?? "";
628
+ const output = sanitizeAgentOutput(response.output ?? "");
430
629
  if (output && onChunk)
431
630
  onChunk(output);
432
631
  return { output, durationSeconds: (Date.now() - startedAt) / 1000 };
@@ -447,27 +646,81 @@ export class GatewayAgentService {
447
646
  taskKeys: request.taskKeys,
448
647
  statusFilter: request.statusFilter?.length ? request.statusFilter : DEFAULT_STATUS_FILTER,
449
648
  limit,
649
+ ignoreDependencies: request.taskKeys && request.taskKeys.length > 0 ? true : request.ignoreDependencies,
450
650
  };
451
651
  const selection = await this.taskSelectionService.selectTasks(filters);
452
652
  if (selection.warnings.length)
453
653
  warnings.push(...selection.warnings);
454
- const combined = [...selection.ordered, ...selection.blocked];
455
- return combined.slice(0, limit).map((entry) => ({
456
- key: entry.task.key,
457
- title: entry.task.title,
458
- description: entry.task.description ?? undefined,
459
- status: entry.task.status,
460
- storyPoints: entry.task.storyPoints ?? undefined,
461
- storyKey: entry.task.storyKey,
462
- storyTitle: entry.task.storyTitle,
463
- epicKey: entry.task.epicKey,
464
- epicTitle: entry.task.epicTitle,
465
- acceptanceCriteria: entry.task.acceptanceCriteria,
466
- 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,
467
702
  }));
468
703
  }
469
704
  catch (error) {
470
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
+ }
471
724
  return [];
472
725
  }
473
726
  }
@@ -511,22 +764,59 @@ export class GatewayAgentService {
511
764
  const maxDocs = request.maxDocs ?? 4;
512
765
  if (maxDocs <= 0)
513
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
+ }
514
776
  const docTypes = this.pickDocTypes(request.job, request.inputText);
515
777
  const query = this.buildQuerySeed(tasks, request.inputText);
516
778
  const summaries = [];
779
+ let openApiIncluded = false;
780
+ let reindexed = false;
517
781
  for (const docType of docTypes) {
518
782
  if (summaries.length >= maxDocs)
519
783
  break;
520
784
  try {
521
- const docs = await this.deps.docdex.search({
785
+ let docs = await this.deps.docdex.search({
522
786
  projectKey: request.projectKey,
523
787
  docType,
524
788
  query,
525
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
+ }
526
805
  for (const doc of docs) {
527
806
  if (summaries.length >= maxDocs)
528
807
  break;
529
- 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);
530
820
  }
531
821
  }
532
822
  catch (error) {
@@ -549,7 +839,10 @@ export class GatewayAgentService {
549
839
  const understanding = normalizeTextField(raw?.understanding) ?? "";
550
840
  const plan = normalizeList(raw?.plan);
551
841
  const filesLikelyTouched = normalizeFileList(raw?.filesLikelyTouched);
552
- 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)];
553
846
  const complexityRaw = Number(raw?.complexity);
554
847
  const complexity = Number.isFinite(complexityRaw) ? clamp(Math.round(complexityRaw), 1, 10) : 5;
555
848
  const discipline = normalizeDiscipline(typeof raw?.discipline === "string" ? raw.discipline : undefined) ??
@@ -579,6 +872,7 @@ export class GatewayAgentService {
579
872
  discipline,
580
873
  filesLikelyTouched,
581
874
  filesToCreate,
875
+ dirsToCreate,
582
876
  assumptions: normalizeList(raw?.assumptions),
583
877
  risks: normalizeList(raw?.risks),
584
878
  docdexNotes: normalizeList(raw?.docdexNotes),
@@ -594,6 +888,36 @@ export class GatewayAgentService {
594
888
  const isInside = (relative) => !relative.startsWith("..") && !path.isAbsolute(relative);
595
889
  const touched = [];
596
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}/`));
597
921
  for (const file of analysis.filesLikelyTouched) {
598
922
  const { relative, resolved } = normalize(file);
599
923
  if (!isInside(relative)) {
@@ -628,8 +952,14 @@ export class GatewayAgentService {
628
952
  }
629
953
  }
630
954
  catch {
631
- warnings.push(`Gateway create path parent does not exist: ${file}`);
632
- 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
+ }
633
963
  }
634
964
  try {
635
965
  const stat = await fs.promises.stat(resolved);
@@ -648,6 +978,7 @@ export class GatewayAgentService {
648
978
  ...analysis,
649
979
  filesLikelyTouched: touched,
650
980
  filesToCreate: created,
981
+ dirsToCreate: dirs,
651
982
  };
652
983
  }
653
984
  async listCandidates(requiredCaps, discipline, avoidAgents = []) {
@@ -786,13 +1117,46 @@ export class GatewayAgentService {
786
1117
  rationale: `Complexity ${normalizedComplexity}/10 targets a comparable tier agent; selected closest match with discipline fit and cost awareness.${gatingNote}`,
787
1118
  };
788
1119
  }
789
- async selectAgentForJob(job, analysis, avoidAgents = [], forceStronger = false) {
1120
+ async selectAgentForJob(job, analysis, avoidAgents = [], forceStronger = false, forceTier, warnings = []) {
790
1121
  const normalizedJob = canonicalizeCommandName(job);
791
1122
  const requiredCaps = getCommandRequiredCapabilities(normalizedJob);
792
- const candidates = await this.listCandidates(requiredCaps, analysis.discipline, avoidAgents);
793
- const boostedComplexity = forceStronger ? clamp(Math.round(analysis.complexity) + 1, 1, 10) : analysis.complexity;
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
+ }
794
1154
  const { pick, rationale } = this.chooseCandidate(candidates, boostedComplexity, analysis.discipline);
795
- const finalRationale = forceStronger ? `${rationale} (force_stronger applied)` : rationale;
1155
+ const finalRationale = forceTier
1156
+ ? `${rationale} (force_tier:${forceTier})`
1157
+ : forceStronger
1158
+ ? `${rationale} (force_stronger applied)`
1159
+ : rationale;
796
1160
  return {
797
1161
  agentId: pick.agent.id,
798
1162
  agentSlug: pick.agent.slug ?? pick.agent.id,
@@ -803,6 +1167,23 @@ export class GatewayAgentService {
803
1167
  rationale: finalRationale,
804
1168
  };
805
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
+ }
806
1187
  async run(request) {
807
1188
  const warnings = [];
808
1189
  const normalizedJob = canonicalizeCommandName(request.job);
@@ -820,7 +1201,7 @@ export class GatewayAgentService {
820
1201
  ]
821
1202
  .filter(Boolean)
822
1203
  .join("\n\n");
823
- const recordUsage = async (promptText, outputText, durationSeconds, action) => {
1204
+ const recordUsage = async (promptText, outputText, durationSeconds, action, attempt) => {
824
1205
  const promptTokens = estimateTokens(promptText);
825
1206
  const completionTokens = estimateTokens(outputText ?? "");
826
1207
  await this.deps.jobService.recordTokenUsage({
@@ -836,20 +1217,24 @@ export class GatewayAgentService {
836
1217
  tokensCompletion: completionTokens,
837
1218
  tokensTotal: promptTokens + completionTokens,
838
1219
  durationSeconds,
839
- metadata: { action, job: normalizedJob },
1220
+ metadata: { action, job: normalizedJob, phase: action, attempt },
840
1221
  });
841
1222
  };
842
1223
  const response = await this.invokeGatewayAgent(gatewayAgent, prompt, normalizedJob, {
843
1224
  stream: request.agentStream !== false,
844
1225
  onChunk: request.onStreamChunk,
845
1226
  });
846
- await recordUsage(prompt, response.output ?? "", response.durationSeconds, "gateway_summary");
847
- 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 };
848
1231
  let missingFields = parsed
849
- ? listMissingFields(parsed)
850
- : ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "files"];
1232
+ ? listMissingFields(parsed, { allowEmptyFiles: true })
1233
+ : ["summary", "reasoningSummary", "currentState", "todo", "understanding", "plan", "filesLikelyTouched", "filesToCreate", "dirsToCreate"];
851
1234
  if (!parsed) {
852
- 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.");
853
1238
  }
854
1239
  if (missingFields.length) {
855
1240
  const repairPrompt = [
@@ -857,7 +1242,8 @@ export class GatewayAgentService {
857
1242
  "",
858
1243
  "Your previous response was incomplete or invalid. Return JSON only with the exact schema.",
859
1244
  `Missing fields: ${missingFields.join(", ")}.`,
860
- "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.",
861
1247
  "Use real file paths only (no placeholders like (unknown), TBD, or glob patterns).",
862
1248
  "If docdex returned no results, say so in docdexNotes.",
863
1249
  ].join("\n");
@@ -868,18 +1254,29 @@ export class GatewayAgentService {
868
1254
  stream: request.agentStream !== false,
869
1255
  onChunk: request.onStreamChunk,
870
1256
  });
871
- await recordUsage(repairPrompt, repairResponse.output ?? "", repairResponse.durationSeconds, "gateway_summary_repair");
872
- const repaired = extractJson(repairResponse.output);
873
- if (repaired) {
874
- parsed = repaired;
875
- 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 });
876
1263
  }
877
1264
  else {
878
- 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.");
879
1268
  }
880
1269
  }
881
1270
  if (missingFields.length) {
882
- 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.");
883
1280
  }
884
1281
  let analysis = this.normalizeAnalysis(parsed ?? {}, normalizedJob, tasks, request.inputText);
885
1282
  if (analysis.docdexNotes.length === 0) {
@@ -893,7 +1290,7 @@ export class GatewayAgentService {
893
1290
  analysis.docdexNotes.push(...docdexWarnings);
894
1291
  }
895
1292
  analysis = await this.validateFilePlan(analysis, warnings);
896
- const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis, request.avoidAgents ?? [], request.forceStronger ?? false);
1293
+ const chosenAgent = await this.selectAgentForJob(normalizedJob, analysis, request.avoidAgents ?? [], request.forceStronger ?? false, request.forceTier, warnings);
897
1294
  await this.deps.jobService.finishCommandRun(commandRun.id, "succeeded");
898
1295
  return {
899
1296
  commandRunId: commandRun.id,