@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,15 +1,81 @@
1
1
  import path from "node:path";
2
2
  import { promises as fs } from "node:fs";
3
3
  import { AgentService } from "@mcoda/agents";
4
- import { GlobalRepository } from "@mcoda/db";
4
+ import { DocsScaffolder } from "@mcoda/generators";
5
+ import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
5
6
  import { DocdexClient } from "@mcoda/integrations";
6
7
  import { DEFAULT_PDR_CHARACTER_PROMPT, DEFAULT_PDR_JOB_PROMPT, DEFAULT_PDR_RUNBOOK_PROMPT, } from "../../prompts/PdrPrompts.js";
7
8
  import { DEFAULT_SDS_CHARACTER_PROMPT, DEFAULT_SDS_JOB_PROMPT, DEFAULT_SDS_RUNBOOK_PROMPT, DEFAULT_SDS_TEMPLATE, } from "../../prompts/SdsPrompts.js";
9
+ import { createEmptyArtifacts, } from "./DocgenRunContext.js";
10
+ import { buildDocInventory } from "./DocInventory.js";
11
+ import { DocPatchEngine, } from "./patch/DocPatchEngine.js";
12
+ import { runApiPathConsistencyGate } from "./review/gates/ApiPathConsistencyGate.js";
13
+ import { runOpenApiCoverageGate } from "./review/gates/OpenApiCoverageGate.js";
14
+ import { runBuildReadyCompletenessGate } from "./review/gates/BuildReadyCompletenessGate.js";
15
+ import { runDeploymentBlueprintGate } from "./review/gates/DeploymentBlueprintGate.js";
16
+ import { runPlaceholderArtifactGate } from "./review/gates/PlaceholderArtifactGate.js";
17
+ import { runSqlSyntaxGate } from "./review/gates/SqlSyntaxGate.js";
18
+ import { runSqlRequiredTablesGate } from "./review/gates/SqlRequiredTablesGate.js";
19
+ import { runTerminologyNormalizationGate } from "./review/gates/TerminologyNormalizationGate.js";
20
+ import { runOpenQuestionsGate } from "./review/gates/OpenQuestionsGate.js";
21
+ import { runNoMaybesGate } from "./review/gates/NoMaybesGate.js";
22
+ import { runRfpConsentGate } from "./review/gates/RfpConsentGate.js";
23
+ import { runRfpDefinitionGate } from "./review/gates/RfpDefinitionGate.js";
24
+ import { runPdrInterfacesGate } from "./review/gates/PdrInterfacesGate.js";
25
+ import { runPdrOwnershipGate } from "./review/gates/PdrOwnershipGate.js";
26
+ import { runPdrOpenQuestionsGate } from "./review/gates/PdrOpenQuestionsGate.js";
27
+ import { runSdsDecisionsGate } from "./review/gates/SdsDecisionsGate.js";
28
+ import { runSdsPolicyTelemetryGate } from "./review/gates/SdsPolicyTelemetryGate.js";
29
+ import { runSdsOpsGate } from "./review/gates/SdsOpsGate.js";
30
+ import { runSdsAdaptersGate } from "./review/gates/SdsAdaptersGate.js";
31
+ import { runAdminOpenApiSpecGate } from "./review/gates/AdminOpenApiSpecGate.js";
32
+ import { runOpenApiSchemaSanityGate } from "./review/gates/OpenApiSchemaSanityGate.js";
33
+ import { renderReviewReport } from "./review/ReviewReportRenderer.js";
34
+ import { serializeReviewReport, } from "./review/ReviewReportSchema.js";
35
+ import { aggregateReviewOutcome, } from "./review/ReviewTypes.js";
36
+ import { findAdminSurfaceMentions, validateOpenApiSchemaContent } from "../openapi/OpenApiService.js";
8
37
  import { JobService } from "../jobs/JobService.js";
38
+ import { cleanupWorkspaceStateDirs, resolveDocgenStatePath, } from "../../workspace/WorkspaceManager.js";
9
39
  import { RoutingService } from "../agents/RoutingService.js";
40
+ import { AgentRatingService, selectBestAgentForCapabilities, } from "../agents/AgentRatingService.js";
41
+ import { DocAlignmentPatcher } from "./alignment/DocAlignmentPatcher.js";
42
+ import { ToolDenylist } from "../system/ToolDenylist.js";
10
43
  const ensureDir = async (targetPath) => {
11
44
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
12
45
  };
46
+ const parseDelimitedList = (value) => {
47
+ if (!value)
48
+ return [];
49
+ return value
50
+ .split(",")
51
+ .map((entry) => entry.trim())
52
+ .filter(Boolean);
53
+ };
54
+ const ALWAYS_BLOCKING_GATES = new Set([
55
+ "gate-placeholder-artifacts",
56
+ "gate-no-maybes",
57
+ "gate-pdr-open-questions-quality",
58
+ ]);
59
+ const BUILD_READY_ONLY_GATES = new Set([
60
+ "gate-api-path-consistency",
61
+ "gate-openapi-schema-sanity",
62
+ "gate-openapi-coverage",
63
+ "gate-sql-syntax",
64
+ "gate-sql-required-tables",
65
+ "gate-admin-openapi-spec",
66
+ "gate-terminology-normalization",
67
+ "gate-open-questions",
68
+ "gate-rfp-consent-contradictions",
69
+ "gate-rfp-definition-coverage",
70
+ "gate-pdr-interfaces-pipeline",
71
+ "gate-pdr-ownership-consent-flow",
72
+ "gate-sds-explicit-decisions",
73
+ "gate-sds-policy-telemetry-metering",
74
+ "gate-sds-ops-observability-testing",
75
+ "gate-sds-external-adapters",
76
+ "gate-deployment-blueprint-validator",
77
+ "gate-build-ready-completeness",
78
+ ]);
13
79
  const readPromptIfExists = async (workspace, relative) => {
14
80
  const candidate = path.join(workspace.mcodaDir, relative);
15
81
  try {
@@ -22,6 +88,7 @@ const readPromptIfExists = async (workspace, relative) => {
22
88
  const PDR_REQUIRED_HEADINGS = [
23
89
  ["Introduction"],
24
90
  ["Scope"],
91
+ ["Technology Stack", "Tech Stack"],
25
92
  ["Requirements", "Requirements & Constraints"],
26
93
  ["Architecture", "Architecture Overview"],
27
94
  ["Interfaces", "Interfaces / APIs"],
@@ -88,6 +155,43 @@ const extractBullets = (content, limit = 20) => {
88
155
  .filter((line) => line.length > 0)
89
156
  .slice(0, limit);
90
157
  };
158
+ const DEFAULT_TECH_STACK_FALLBACK = [
159
+ "- Frontend: React + TypeScript",
160
+ "- Backend/services: TypeScript (Node.js)",
161
+ "- Database: MySQL",
162
+ "- Cache/queues: Redis",
163
+ "- Scripting/ops: Bash",
164
+ "- Override defaults if the RFP specifies a different stack.",
165
+ ].join("\n");
166
+ const ML_TECH_STACK_FALLBACK = [
167
+ "- Language: Python",
168
+ "- ML stack: PyTorch or TensorFlow (pick based on model requirements)",
169
+ "- Services/API: Python web framework (FastAPI/Flask) as needed",
170
+ "- Database: MySQL",
171
+ "- Cache/queues: Redis",
172
+ "- Scripting/ops: Bash",
173
+ "- Override defaults if the RFP specifies a different stack.",
174
+ ].join("\n");
175
+ const contextIndicatesMlStack = (context) => {
176
+ const sources = [context.rfp?.content, ...context.related.map((doc) => doc.content ?? "")].filter(Boolean);
177
+ if (!sources.length)
178
+ return false;
179
+ const text = sources.join("\n").toLowerCase();
180
+ const patterns = [
181
+ /\bmachine learning\b/,
182
+ /\bdeep learning\b/,
183
+ /\bneural\b/,
184
+ /\bmodel training\b/,
185
+ /\bmodel inference\b/,
186
+ /\bml\b/,
187
+ /\bllm\b/,
188
+ /\bembeddings?\b/,
189
+ ];
190
+ return patterns.some((pattern) => pattern.test(text));
191
+ };
192
+ const resolveTechStackFallback = (context) => {
193
+ return contextIndicatesMlStack(context) ? ML_TECH_STACK_FALLBACK : DEFAULT_TECH_STACK_FALLBACK;
194
+ };
91
195
  class DocContextAssembler {
92
196
  constructor(docdex, workspace) {
93
197
  this.docdex = docdex;
@@ -100,9 +204,28 @@ class DocContextAssembler {
100
204
  }
101
205
  async findLatestLocalDoc(docType) {
102
206
  const candidates = [];
207
+ const lower = docType.toLowerCase();
208
+ const explicitFiles = [
209
+ path.join(this.workspace.mcodaDir, "docs", `${lower}.md`),
210
+ path.join(this.workspace.workspaceRoot, "docs", `${lower}.md`),
211
+ ];
212
+ const addCandidate = async (candidatePath) => {
213
+ try {
214
+ const stat = await fs.stat(candidatePath);
215
+ if (stat.isFile()) {
216
+ candidates.push({ path: candidatePath, mtime: stat.mtimeMs });
217
+ }
218
+ }
219
+ catch {
220
+ // ignore
221
+ }
222
+ };
223
+ for (const filePath of explicitFiles) {
224
+ await addCandidate(filePath);
225
+ }
103
226
  const dirs = [
104
- path.join(this.workspace.mcodaDir, "docs", docType.toLowerCase()),
105
- path.join(this.workspace.workspaceRoot, "docs", docType.toLowerCase()),
227
+ path.join(this.workspace.mcodaDir, "docs", lower),
228
+ path.join(this.workspace.workspaceRoot, "docs", lower),
106
229
  ];
107
230
  for (const dir of dirs) {
108
231
  try {
@@ -237,7 +360,7 @@ class DocContextAssembler {
237
360
  existingSds = [localSds];
238
361
  }
239
362
  if (!pdrs.length && !rfp) {
240
- throw new Error("No PDR or RFP content could be resolved. Ensure docdex is reachable with an sds_default profile or add local docs under .mcoda/docs/pdr and docs/rfp.");
363
+ throw new Error(`No PDR or RFP content could be resolved. Ensure docdex is reachable with an sds_default profile or add local docs under ${path.join(this.workspace.mcodaDir, "docs", "pdr")} and docs/rfp (or docs/rfp.md).`);
241
364
  }
242
365
  const blocks = [];
243
366
  if (rfp)
@@ -330,10 +453,22 @@ class DocContextAssembler {
330
453
  }
331
454
  let related = [];
332
455
  if (docdexAvailable) {
333
- related = await this.docdex.search({ projectKey: input.projectKey, docType: "PDR", profile: "rfp_default" });
334
- const sds = await this.docdex.search({ projectKey: input.projectKey, docType: "SDS", profile: "rfp_default" });
335
- openapi = await this.docdex.search({ projectKey: input.projectKey, docType: "OPENAPI", profile: "rfp_default" });
336
- related = [...related, ...sds];
456
+ try {
457
+ related = await this.docdex.search({ projectKey: input.projectKey, docType: "PDR", profile: "rfp_default" });
458
+ const sds = await this.docdex.search({ projectKey: input.projectKey, docType: "SDS", profile: "rfp_default" });
459
+ openapi = await this.docdex.search({
460
+ projectKey: input.projectKey,
461
+ docType: "OPENAPI",
462
+ profile: "rfp_default",
463
+ });
464
+ related = [...related, ...sds];
465
+ }
466
+ catch (error) {
467
+ docdexAvailable = false;
468
+ related = [];
469
+ openapi = [];
470
+ warnings.push(`Docdex unavailable; continuing without related docs (${error.message ?? "unknown error"}).`);
471
+ }
337
472
  }
338
473
  const summaryParts = [
339
474
  `RFP: ${this.summarize(rfp)}`,
@@ -383,7 +518,7 @@ const buildRunPrompt = (context, projectKey, prompts, runbook) => {
383
518
  docdexNote,
384
519
  [
385
520
  "Return markdown with exactly these sections as H2 headings, one time each:",
386
- "Introduction, Scope, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria",
521
+ "Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria",
387
522
  "Do not use bold headings; use `##` headings only. Do not repeat sections.",
388
523
  ].join("\n"),
389
524
  runbookPrompt,
@@ -421,12 +556,14 @@ const buildSdsRunPrompt = (context, projectKey, prompts, runbook, template) => {
421
556
  const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
422
557
  const canonicalTitles = PDR_REQUIRED_HEADINGS.map((variants) => variants[0]);
423
558
  const normalized = normalizeHeadingsToH2(draft, canonicalTitles);
559
+ const techStackFallback = resolveTechStackFallback(context);
424
560
  const required = [
425
561
  { title: "Introduction", fallback: `This PDR summarizes project ${projectKey ?? "N/A"} based on ${rfpSource}.` },
426
562
  {
427
563
  title: "Scope",
428
564
  fallback: "In-scope: todo CRUD (title required; optional description, due date, priority), status toggle, filters/sort/search, bulk complete/delete, keyboard shortcuts, responsive UI, offline/localStorage. Out-of-scope: multi-user/auth/sync/backends, notifications/reminders, team features, heavy UI kits.",
429
565
  },
566
+ { title: "Technology Stack", fallback: techStackFallback },
430
567
  {
431
568
  title: "Requirements & Constraints",
432
569
  fallback: context.bullets.map((b) => `- ${b}`).join("\n") ||
@@ -444,7 +581,20 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
444
581
  for (const section of required) {
445
582
  const best = getBestSectionBody(normalized, section.title);
446
583
  const cleaned = cleanBody(best ?? "");
447
- const body = cleaned && cleaned.length > 0 ? cleaned : cleanBody(section.fallback);
584
+ let body = cleaned && cleaned.length > 0 ? cleaned : cleanBody(section.fallback);
585
+ if (section.title === "Interfaces / APIs" && (context.openapi?.length ?? 0) === 0) {
586
+ const scrubbed = stripInventedEndpoints(body);
587
+ const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (authentication/identity, data access, integrations, eventing, analytics/observability).";
588
+ if (!scrubbed || scrubbed.length === 0 || /endpoint/i.test(scrubbed)) {
589
+ body = cleanBody(openApiFallback);
590
+ }
591
+ else {
592
+ body = scrubbed;
593
+ }
594
+ if (!/openapi/i.test(body)) {
595
+ body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
596
+ }
597
+ }
448
598
  parts.push(`## ${section.title}`);
449
599
  parts.push(body);
450
600
  }
@@ -458,7 +608,7 @@ const tidyPdrDraft = async (draft, agent, invoke) => {
458
608
  draft,
459
609
  "",
460
610
  "Requirements:",
461
- "- Keep exactly one instance of each H2 section: Introduction, Scope, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria, Source RFP.",
611
+ "- Keep exactly one instance of each H2 section: Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria, Source RFP.",
462
612
  "- Remove duplicate sections, bold headings posing as sections, placeholder sentences, and repeated bullet blocks. If the same idea appears twice, keep the richer/longer version and drop the restatement.",
463
613
  "- Do not add new sections or reorder the required outline.",
464
614
  "- Keep content concise and aligned to the headings. Do not alter semantics.",
@@ -468,6 +618,13 @@ const tidyPdrDraft = async (draft, agent, invoke) => {
468
618
  return output.trim();
469
619
  };
470
620
  const PDR_ENRICHMENT_SECTIONS = [
621
+ {
622
+ title: "Technology Stack",
623
+ guidance: [
624
+ "List frontend, backend/services, databases, caches/queues, infra/runtime, and scripting/tooling choices.",
625
+ "If the RFP omits stack details, state the default stack (TypeScript/React/MySQL/Redis/Bash) or a Python ML stack when neural/ML workloads are explicit.",
626
+ ],
627
+ },
471
628
  {
472
629
  title: "Architecture Overview",
473
630
  guidance: [
@@ -588,6 +745,19 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
588
745
  for (const section of sections) {
589
746
  structured = ensureSectionContent(structured, section, fallbackFor(section));
590
747
  }
748
+ if ((context.openapi?.length ?? 0) === 0) {
749
+ const interfaceTitle = sections.find((section) => /interface|contract/i.test(section)) ?? "Interfaces & Contracts";
750
+ const extracted = extractSection(structured, interfaceTitle);
751
+ if (extracted) {
752
+ const scrubbed = stripInventedEndpoints(cleanBody(extracted.body ?? ""));
753
+ const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (auth/identity, restaurant suggestions, voting cycles, results/analytics).";
754
+ let body = scrubbed.length > 0 && !/endpoint/i.test(scrubbed) ? scrubbed : cleanBody(openApiFallback);
755
+ if (!/openapi/i.test(body)) {
756
+ body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
757
+ }
758
+ structured = replaceSection(structured, interfaceTitle, body);
759
+ }
760
+ }
591
761
  return structured;
592
762
  };
593
763
  const getSdsSections = (template) => {
@@ -847,6 +1017,7 @@ const PLACEHOLDER_PATTERNS = [
847
1017
  /^[-*+.]?\s*Outstanding questions/i,
848
1018
  /^[-*+.]?\s*Performance, reliability, compliance/i,
849
1019
  /^[-*+.]?\s*Enumerate risks from the RFP/i,
1020
+ /^I (will|am going to|plan to|am)\s+(read|review|analy[sz]e|gather|start|begin|look|scan)\b/i,
850
1021
  ];
851
1022
  const cleanBody = (body) => {
852
1023
  const requiredTitles = PDR_REQUIRED_HEADINGS.flat().map((t) => t.toLowerCase());
@@ -887,6 +1058,14 @@ const cleanBody = (body) => {
887
1058
  }
888
1059
  return deduped.join("\n").trim();
889
1060
  };
1061
+ const stripInventedEndpoints = (body) => {
1062
+ const lines = body.split(/\r?\n/);
1063
+ const filtered = lines.filter((line) => {
1064
+ const normalized = line.replace(/[`*_]/g, "");
1065
+ return !/(^|\s)\b(GET|POST|PUT|PATCH|DELETE)\b[^\n]*\//i.test(normalized);
1066
+ });
1067
+ return filtered.join("\n").trim();
1068
+ };
890
1069
  const extractSection = (draft, title) => {
891
1070
  const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s+|(?![\\s\\S]))`, "im");
892
1071
  const match = draft.match(regex);
@@ -910,7 +1089,7 @@ const getBestSectionBody = (draft, title) => {
910
1089
  };
911
1090
  const replaceSection = (draft, title, newBody) => {
912
1091
  const normalizedBody = cleanBody(newBody);
913
- const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s+|$)`, "im");
1092
+ const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s+|(?![\\s\\S]))`, "im");
914
1093
  if (regex.test(draft)) {
915
1094
  return draft.replace(regex, `$1\n\n${normalizedBody}\n\n`);
916
1095
  }
@@ -978,33 +1157,42 @@ const readGitBranch = async (workspaceRoot) => {
978
1157
  export class DocsService {
979
1158
  constructor(workspace, deps) {
980
1159
  this.workspace = workspace;
981
- this.docdex = deps?.docdex ?? new DocdexClient({ workspaceRoot: workspace.workspaceRoot });
1160
+ const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
1161
+ this.docdex = deps?.docdex ?? new DocdexClient({ workspaceRoot: workspace.workspaceRoot, repoId: docdexRepoId });
982
1162
  this.jobService = deps?.jobService ?? new JobService(workspace, undefined, { noTelemetry: deps?.noTelemetry });
983
1163
  this.repo = deps.repo;
984
1164
  this.agentService = deps.agentService;
985
1165
  this.routingService = deps.routingService;
1166
+ this.workspaceRepo = deps.workspaceRepo;
1167
+ this.ratingService = deps.ratingService;
986
1168
  }
987
1169
  static async create(workspace, options = {}) {
988
1170
  const repo = await GlobalRepository.create();
989
1171
  const agentService = new AgentService(repo);
990
1172
  const routingService = await RoutingService.create();
1173
+ const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
991
1174
  const docdex = new DocdexClient({
992
1175
  workspaceRoot: workspace.workspaceRoot,
993
1176
  baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
1177
+ repoId: docdexRepoId,
994
1178
  });
995
1179
  const jobService = new JobService(workspace, undefined, { noTelemetry: options.noTelemetry });
996
1180
  return new DocsService(workspace, { repo, agentService, routingService, docdex, jobService, noTelemetry: options.noTelemetry });
997
1181
  }
998
1182
  async close() {
999
- if (this.agentService.close) {
1000
- await this.agentService.close();
1001
- }
1002
- if (this.repo.close) {
1003
- await this.repo.close();
1004
- }
1005
- if (this.jobService.close) {
1006
- await this.jobService.close();
1007
- }
1183
+ const swallow = async (fn) => {
1184
+ try {
1185
+ if (fn)
1186
+ await fn();
1187
+ }
1188
+ catch {
1189
+ // Best-effort close; ignore errors (including "database is closed").
1190
+ }
1191
+ };
1192
+ await swallow(this.agentService.close?.bind(this.agentService));
1193
+ await swallow(this.repo.close?.bind(this.repo));
1194
+ await swallow(this.jobService.close?.bind(this.jobService));
1195
+ await swallow(this.workspaceRepo?.close?.bind(this.workspaceRepo));
1008
1196
  }
1009
1197
  defaultPdrOutputPath(projectKey, rfpPath) {
1010
1198
  const slug = slugify(projectKey ?? (rfpPath ? path.basename(rfpPath, path.extname(rfpPath)) : "pdr"));
@@ -1036,14 +1224,1391 @@ export class DocsService {
1036
1224
  }
1037
1225
  return { name: templateName ?? "default", content: DEFAULT_SDS_TEMPLATE };
1038
1226
  }
1039
- async resolveAgent(agentName, commandAliases = ["docs-pdr-generate", "docs:pdr:generate", "pdr"]) {
1040
- const commandName = commandAliases[commandAliases.length - 1] ?? "pdr";
1041
- const resolved = await this.routingService.resolveAgentForCommand({
1227
+ buildDocgenCapabilityProfile(input) {
1228
+ const required = ["doc_generation", "docdex_query"];
1229
+ const preferred = [];
1230
+ if (input.commandName === "docs-pdr-generate") {
1231
+ preferred.push("rfp_creation", "spec_generation");
1232
+ }
1233
+ if (input.commandName === "docs-sds-generate") {
1234
+ preferred.push("sds_writing", "spec_generation");
1235
+ }
1236
+ if (input.iterationEnabled) {
1237
+ preferred.push("multiple_draft_generation", "quick_bug_patching", "code_review");
1238
+ }
1239
+ return {
1240
+ required: Array.from(new Set(required)),
1241
+ preferred: Array.from(new Set(preferred)),
1242
+ };
1243
+ }
1244
+ async collectDocgenCandidates(resolved) {
1245
+ const candidates = new Map();
1246
+ let agents = [];
1247
+ try {
1248
+ agents = await this.repo.listAgents();
1249
+ }
1250
+ catch {
1251
+ agents = [];
1252
+ }
1253
+ let healthRows = [];
1254
+ try {
1255
+ healthRows = await this.repo.listAgentHealthSummary();
1256
+ }
1257
+ catch {
1258
+ healthRows = [];
1259
+ }
1260
+ const healthById = new Map(healthRows.map((row) => [row.agentId, row.status]));
1261
+ for (const agent of agents) {
1262
+ let caps = [];
1263
+ try {
1264
+ caps = await this.repo.getAgentCapabilities(agent.id);
1265
+ }
1266
+ catch {
1267
+ caps = agent.capabilities ?? [];
1268
+ }
1269
+ candidates.set(agent.id, {
1270
+ agent,
1271
+ capabilities: caps,
1272
+ healthStatus: healthById.get(agent.id),
1273
+ });
1274
+ }
1275
+ if (resolved) {
1276
+ const existing = candidates.get(resolved.agent.id);
1277
+ if (!existing || existing.capabilities.length === 0) {
1278
+ candidates.set(resolved.agent.id, {
1279
+ agent: resolved.agent,
1280
+ capabilities: resolved.capabilities ?? [],
1281
+ healthStatus: resolved.healthStatus,
1282
+ });
1283
+ }
1284
+ }
1285
+ return Array.from(candidates.values());
1286
+ }
1287
+ async selectDocgenAgent(input) {
1288
+ const commandName = input.commandAliases[input.commandAliases.length - 1] ?? input.commandName;
1289
+ const profile = this.buildDocgenCapabilityProfile({
1290
+ commandName: input.commandName,
1291
+ iterationEnabled: input.iterationEnabled,
1292
+ });
1293
+ let resolved;
1294
+ let resolveError;
1295
+ try {
1296
+ resolved = await this.routingService.resolveAgentForCommand({
1297
+ workspace: this.workspace,
1298
+ commandName,
1299
+ overrideAgentSlug: input.agentName,
1300
+ requiredCapabilities: profile.required,
1301
+ });
1302
+ }
1303
+ catch (error) {
1304
+ resolveError = error instanceof Error ? error : new Error(String(error));
1305
+ resolved = undefined;
1306
+ }
1307
+ if (!resolved) {
1308
+ const candidates = await this.collectDocgenCandidates();
1309
+ const selection = selectBestAgentForCapabilities({
1310
+ candidates,
1311
+ required: profile.required,
1312
+ preferred: profile.preferred,
1313
+ });
1314
+ if (!selection) {
1315
+ throw resolveError ?? new Error("No agents available for doc generation.");
1316
+ }
1317
+ const selected = selection.agent;
1318
+ const selectedLabel = selected.slug ?? selected.id;
1319
+ const preferredLabel = input.agentName ?? "routing-default";
1320
+ const warn = (message) => {
1321
+ if (!input.warnings.includes(message)) {
1322
+ input.warnings.push(message);
1323
+ }
1324
+ };
1325
+ warn(`Docgen preflight selected fallback agent ${selectedLabel} (preferred ${preferredLabel}).`);
1326
+ if (selection.missingRequired.length > 0) {
1327
+ warn(`Docgen preflight selected agent ${selectedLabel} missing required capabilities: ${selection.missingRequired.join(", ")}.`);
1328
+ }
1329
+ const logLines = [
1330
+ `[docgen preflight] command=${input.commandName} routing=${commandName}`,
1331
+ `[docgen preflight] preferred=${preferredLabel} selected=${selectedLabel} fallback=yes`,
1332
+ `[docgen preflight] required=${profile.required.join(", ") || "none"}`,
1333
+ `[docgen preflight] preferred_caps=${profile.preferred.join(", ") || "none"}`,
1334
+ `[docgen preflight] missing_required=${selection.missingRequired.join(", ") || "none"}`,
1335
+ `[docgen preflight] missing_preferred=${selection.missingPreferred.join(", ") || "none"}`,
1336
+ `[docgen preflight] reason=${selection.reason}`,
1337
+ resolveError ? `[docgen preflight] routing_error=${resolveError.message}` : undefined,
1338
+ ].filter(Boolean);
1339
+ await this.jobService.appendLog(input.jobId, `${logLines.join("\n")}\n`);
1340
+ await this.jobService.writeCheckpoint(input.jobId, {
1341
+ stage: "agent_preflight",
1342
+ timestamp: new Date().toISOString(),
1343
+ details: {
1344
+ commandName: input.commandName,
1345
+ routingCommand: commandName,
1346
+ preferredAgent: preferredLabel,
1347
+ selectedAgent: selectedLabel,
1348
+ fallbackUsed: true,
1349
+ requiredCapabilities: profile.required,
1350
+ preferredCapabilities: profile.preferred,
1351
+ missingRequired: selection.missingRequired,
1352
+ missingPreferred: selection.missingPreferred,
1353
+ reason: selection.reason,
1354
+ routingError: resolveError?.message,
1355
+ },
1356
+ });
1357
+ return selected;
1358
+ }
1359
+ const resolvedMissing = profile.required.filter((cap) => !(resolved.capabilities ?? []).includes(cap));
1360
+ const needsFallback = resolvedMissing.length > 0 || resolved.healthStatus === "unreachable";
1361
+ const candidates = needsFallback ? await this.collectDocgenCandidates(resolved) : [];
1362
+ const selection = needsFallback
1363
+ ? selectBestAgentForCapabilities({
1364
+ candidates,
1365
+ required: profile.required,
1366
+ preferred: profile.preferred,
1367
+ })
1368
+ : undefined;
1369
+ const selected = selection?.agent ?? resolved.agent;
1370
+ const missingRequired = selection?.missingRequired ?? resolvedMissing;
1371
+ const missingPreferred = selection?.missingPreferred ??
1372
+ profile.preferred.filter((cap) => !(resolved.capabilities ?? []).includes(cap));
1373
+ const fallbackUsed = selected.id !== resolved.agent.id;
1374
+ const preferredLabel = resolved.agent.slug ?? resolved.agent.id;
1375
+ const selectedLabel = selected.slug ?? selected.id;
1376
+ const warn = (message) => {
1377
+ if (!input.warnings.includes(message)) {
1378
+ input.warnings.push(message);
1379
+ }
1380
+ };
1381
+ if (fallbackUsed) {
1382
+ warn(`Docgen preflight selected fallback agent ${selectedLabel} (preferred ${preferredLabel}).`);
1383
+ }
1384
+ if (missingRequired.length > 0) {
1385
+ warn(`Docgen preflight selected agent ${selectedLabel} missing required capabilities: ${missingRequired.join(", ")}.`);
1386
+ }
1387
+ const logLines = [
1388
+ `[docgen preflight] command=${input.commandName} routing=${commandName}`,
1389
+ `[docgen preflight] preferred=${preferredLabel} selected=${selectedLabel} fallback=${fallbackUsed ? "yes" : "no"}`,
1390
+ `[docgen preflight] required=${profile.required.join(", ") || "none"}`,
1391
+ `[docgen preflight] preferred_caps=${profile.preferred.join(", ") || "none"}`,
1392
+ `[docgen preflight] missing_required=${missingRequired.join(", ") || "none"}`,
1393
+ `[docgen preflight] missing_preferred=${missingPreferred.join(", ") || "none"}`,
1394
+ `[docgen preflight] reason=${selection?.reason ?? "routing default"}`,
1395
+ ];
1396
+ await this.jobService.appendLog(input.jobId, `${logLines.join("\n")}\n`);
1397
+ await this.jobService.writeCheckpoint(input.jobId, {
1398
+ stage: "agent_preflight",
1399
+ timestamp: new Date().toISOString(),
1400
+ details: {
1401
+ commandName: input.commandName,
1402
+ routingCommand: commandName,
1403
+ preferredAgent: preferredLabel,
1404
+ selectedAgent: selectedLabel,
1405
+ fallbackUsed,
1406
+ requiredCapabilities: profile.required,
1407
+ preferredCapabilities: profile.preferred,
1408
+ missingRequired,
1409
+ missingPreferred,
1410
+ reason: selection?.reason,
1411
+ },
1412
+ });
1413
+ return selected;
1414
+ }
1415
+ async ensureRatingService() {
1416
+ if (this.ratingService)
1417
+ return this.ratingService;
1418
+ if (process.env.MCODA_DISABLE_DB === "1") {
1419
+ throw new Error("Workspace DB disabled; agent rating requires DB access.");
1420
+ }
1421
+ if (!this.workspaceRepo) {
1422
+ this.workspaceRepo = await WorkspaceRepository.create(this.workspace.workspaceRoot);
1423
+ }
1424
+ this.ratingService = new AgentRatingService(this.workspace, {
1425
+ workspaceRepo: this.workspaceRepo,
1426
+ globalRepo: this.repo,
1427
+ agentService: this.agentService,
1428
+ routingService: this.routingService,
1429
+ });
1430
+ return this.ratingService;
1431
+ }
1432
+ createRunContext(input) {
1433
+ return {
1434
+ version: 1,
1435
+ commandName: input.commandName,
1436
+ commandRunId: input.commandRunId,
1437
+ jobId: input.jobId,
1042
1438
  workspace: this.workspace,
1043
- commandName,
1044
- overrideAgentSlug: agentName,
1439
+ projectKey: input.projectKey,
1440
+ rfpId: input.rfpId,
1441
+ rfpPath: input.rfpPath,
1442
+ templateName: input.templateName,
1443
+ outputPath: input.outputPath,
1444
+ createdAt: new Date().toISOString(),
1445
+ flags: input.flags,
1446
+ iteration: { current: 0, max: 0 },
1447
+ artifacts: createEmptyArtifacts(),
1448
+ warnings: input.warnings,
1449
+ };
1450
+ }
1451
+ async recordDocgenStage(runContext, input) {
1452
+ const iteration = runContext.iteration.max > 0 && runContext.iteration.current > 0
1453
+ ? { current: runContext.iteration.current, max: runContext.iteration.max, phase: input.phase }
1454
+ : undefined;
1455
+ await this.jobService.recordJobProgress(runContext.jobId, {
1456
+ stage: input.stage,
1457
+ message: input.message,
1458
+ iteration,
1459
+ details: input.details,
1460
+ totalItems: input.totalItems,
1461
+ processedItems: input.processedItems,
1462
+ heartbeat: input.heartbeat,
1463
+ });
1464
+ }
1465
+ async enforceToolDenylist(input) {
1466
+ const denylist = await ToolDenylist.load({
1467
+ mcodaDir: this.workspace.mcodaDir,
1468
+ env: process.env,
1469
+ });
1470
+ const identifiers = [input.agent.slug, input.agent.adapter, input.agent.id].filter((value) => Boolean(value));
1471
+ const matched = denylist.findMatch(identifiers);
1472
+ if (!matched)
1473
+ return;
1474
+ const message = denylist.formatViolation(matched);
1475
+ const artifact = input.runContext.commandName === "docs-pdr-generate" ? "pdr" : "sds";
1476
+ const issue = {
1477
+ id: `gate-tool-denylist-${matched}`,
1478
+ gateId: "gate-tool-denylist",
1479
+ severity: "blocker",
1480
+ category: "compliance",
1481
+ artifact,
1482
+ message,
1483
+ remediation: "Select a non-deprecated agent or update the tool denylist configuration.",
1484
+ location: {
1485
+ kind: "heading",
1486
+ heading: "Tooling Preflight",
1487
+ path: input.runContext.outputPath,
1488
+ },
1489
+ metadata: {
1490
+ matchedTool: matched,
1491
+ identifiers,
1492
+ agentId: input.agent.id,
1493
+ agentSlug: input.agent.slug,
1494
+ agentAdapter: input.agent.adapter,
1495
+ denylist: denylist.list(),
1496
+ },
1497
+ };
1498
+ const gateResult = {
1499
+ gateId: "gate-tool-denylist",
1500
+ gateName: "Tool Denylist",
1501
+ status: "fail",
1502
+ issues: [issue],
1503
+ notes: [message],
1504
+ metadata: {
1505
+ matchedTool: matched,
1506
+ },
1507
+ };
1508
+ if (input.runContext.iteration.max === 0) {
1509
+ input.runContext.iteration.max = 1;
1510
+ }
1511
+ const report = this.buildReviewReport({
1512
+ runContext: input.runContext,
1513
+ gateResults: [gateResult],
1514
+ remainingOpenItems: [issue],
1515
+ fixesApplied: [],
1516
+ iterationStatus: "completed",
1517
+ });
1518
+ await this.persistReviewReport(input.runContext, "review-final", report);
1519
+ await this.jobService.appendLog(input.runContext.jobId, `${message}\n`);
1520
+ await this.jobService.writeCheckpoint(input.runContext.jobId, {
1521
+ stage: "tool_denylist_blocked",
1522
+ timestamp: new Date().toISOString(),
1523
+ details: {
1524
+ matchedTool: matched,
1525
+ identifiers,
1526
+ denylist: denylist.list(),
1527
+ },
1528
+ });
1529
+ throw new Error(message);
1530
+ }
1531
+ async applyDocPatches(runContext, patches, options) {
1532
+ const engine = new DocPatchEngine();
1533
+ return engine.apply({
1534
+ runContext,
1535
+ patches,
1536
+ dryRun: options?.dryRun ?? runContext.flags.dryRun,
1537
+ });
1538
+ }
1539
+ resolveMaxIterations() {
1540
+ const raw = process.env.MCODA_DOCS_MAX_ITERATIONS;
1541
+ if (!raw)
1542
+ return 2;
1543
+ const parsed = Number.parseInt(raw, 10);
1544
+ if (!Number.isFinite(parsed) || parsed <= 0)
1545
+ return 2;
1546
+ return Math.max(1, parsed);
1547
+ }
1548
+ isIterationEnabled(runContext) {
1549
+ if (runContext.flags.dryRun)
1550
+ return false;
1551
+ if (runContext.flags.iterate)
1552
+ return true;
1553
+ if (runContext.flags.fast)
1554
+ return false;
1555
+ return true;
1556
+ }
1557
+ shouldBlockGate(gateId, runContext) {
1558
+ if (ALWAYS_BLOCKING_GATES.has(gateId))
1559
+ return true;
1560
+ const resolveOpenQuestions = runContext.flags.resolveOpenQuestions ||
1561
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
1562
+ if (gateId === "gate-open-questions" && resolveOpenQuestions) {
1563
+ return true;
1564
+ }
1565
+ if (BUILD_READY_ONLY_GATES.has(gateId))
1566
+ return runContext.flags.buildReady;
1567
+ return false;
1568
+ }
1569
+ appendUniqueWarnings(runContext, values) {
1570
+ for (const value of values) {
1571
+ if (!value)
1572
+ continue;
1573
+ if (runContext.warnings.includes(value))
1574
+ continue;
1575
+ runContext.warnings.push(value);
1576
+ }
1577
+ }
1578
+ summarizeIssues(issues) {
1579
+ const summary = issues
1580
+ .slice(0, 3)
1581
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
1582
+ .join(" ");
1583
+ const suffix = issues.length > 3 ? ` (+${issues.length - 3} more)` : "";
1584
+ return `${summary}${suffix}`;
1585
+ }
1586
+ appendGateWarnings(runContext, gate, blocking) {
1587
+ if (gate.notes?.length) {
1588
+ this.appendUniqueWarnings(runContext, gate.notes);
1589
+ }
1590
+ if (blocking)
1591
+ return;
1592
+ if (gate.issues.length === 0)
1593
+ return;
1594
+ const summary = this.summarizeIssues(gate.issues);
1595
+ this.appendUniqueWarnings(runContext, [
1596
+ `${gate.gateName} issues (${gate.issues.length}). ${summary}`,
1597
+ ]);
1598
+ }
1599
+ async runReviewGates(runContext, phase = "review") {
1600
+ const gateResults = [];
1601
+ const blockingIssues = [];
1602
+ const phaseLabel = phase === "recheck" ? "Re-check" : "Review";
1603
+ const runGate = async (gateId, gateName, runner) => {
1604
+ await this.recordDocgenStage(runContext, {
1605
+ stage: `${phase}:${gateId}`,
1606
+ message: `${phaseLabel}: ${gateName}`,
1607
+ phase,
1608
+ heartbeat: true,
1609
+ details: { gateId, gateName },
1610
+ });
1611
+ return await runner();
1612
+ };
1613
+ const addResult = (result) => {
1614
+ const blocking = this.shouldBlockGate(result.gateId, runContext);
1615
+ if (result.status === "fail" && blocking) {
1616
+ blockingIssues.push(...result.issues);
1617
+ }
1618
+ const normalized = result.status === "fail" && !blocking ? { ...result, status: "warn" } : result;
1619
+ this.appendGateWarnings(runContext, normalized, blocking);
1620
+ gateResults.push(normalized);
1621
+ };
1622
+ const stateWarnings = (runContext.stateWarnings ?? []).filter((warning) => typeof warning === "string" && warning.trim().length > 0);
1623
+ const stateArtifact = runContext.commandName === "docs-pdr-generate" ? "pdr" : "sds";
1624
+ const stateIssues = stateWarnings.map((warning, index) => ({
1625
+ id: `gate-state-dir-cleanup-${index + 1}`,
1626
+ gateId: "gate-state-dir-cleanup",
1627
+ severity: "info",
1628
+ category: "compliance",
1629
+ artifact: stateArtifact,
1630
+ message: warning,
1631
+ remediation: "Keep docgen intermediate state under .mcoda or OS temp directories and relocate legacy state directories into .mcoda.",
1632
+ location: { kind: "heading", heading: "State Directory Cleanup", path: runContext.outputPath },
1633
+ }));
1634
+ addResult({
1635
+ gateId: "gate-state-dir-cleanup",
1636
+ gateName: "State Directory Cleanup",
1637
+ status: stateIssues.length > 0 ? "warn" : "pass",
1638
+ issues: stateIssues,
1639
+ });
1640
+ if (runContext.flags.noPlaceholders || runContext.flags.buildReady) {
1641
+ const allowlist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_ALLOWLIST);
1642
+ const denylist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_DENYLIST);
1643
+ addResult(await runGate("gate-placeholder-artifacts", "Placeholder Artifacts", () => runPlaceholderArtifactGate({
1644
+ artifacts: runContext.artifacts,
1645
+ allowlist: allowlist.length > 0 ? allowlist : undefined,
1646
+ denylist: denylist.length > 0 ? denylist : undefined,
1647
+ })));
1648
+ }
1649
+ else {
1650
+ const skippedGate = {
1651
+ gateId: "gate-placeholder-artifacts",
1652
+ gateName: "Placeholder Artifacts",
1653
+ status: "skipped",
1654
+ issues: [],
1655
+ notes: ["Placeholder gate disabled (noPlaceholders/buildReady not set)."],
1656
+ };
1657
+ addResult(skippedGate);
1658
+ }
1659
+ addResult(await runGate("gate-api-path-consistency", "API Path Consistency", () => runApiPathConsistencyGate({ artifacts: runContext.artifacts })));
1660
+ addResult(await runGate("gate-openapi-schema-sanity", "OpenAPI Schema Sanity", () => runOpenApiSchemaSanityGate({ artifacts: runContext.artifacts })));
1661
+ addResult(await runGate("gate-openapi-coverage", "OpenAPI Coverage", () => runOpenApiCoverageGate({ artifacts: runContext.artifacts })));
1662
+ addResult(await runGate("gate-sql-syntax", "SQL Syntax", () => runSqlSyntaxGate({ artifacts: runContext.artifacts })));
1663
+ addResult(await runGate("gate-sql-required-tables", "SQL Required Tables", () => runSqlRequiredTablesGate({ artifacts: runContext.artifacts })));
1664
+ addResult(await runGate("gate-admin-openapi-spec", "Admin OpenAPI Spec", () => runAdminOpenApiSpecGate({ artifacts: runContext.artifacts })));
1665
+ addResult(await runGate("gate-terminology-normalization", "Terminology Normalization", () => runTerminologyNormalizationGate({ artifacts: runContext.artifacts })));
1666
+ addResult(await runGate("gate-open-questions", "Open Questions", () => runOpenQuestionsGate({ artifacts: runContext.artifacts })));
1667
+ const resolveOpenQuestions = runContext.flags.resolveOpenQuestions ||
1668
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
1669
+ const noMaybesEnabled = runContext.flags.noMaybes ||
1670
+ process.env.MCODA_DOCS_NO_MAYBES === "1" ||
1671
+ resolveOpenQuestions;
1672
+ addResult(await runGate("gate-no-maybes", "No Maybes", () => runNoMaybesGate({
1673
+ artifacts: runContext.artifacts,
1674
+ enabled: noMaybesEnabled,
1675
+ })));
1676
+ addResult(await runGate("gate-rfp-consent", "RFP Consent", () => runRfpConsentGate({ rfpPath: runContext.rfpPath })));
1677
+ const definitionAllowlist = parseDelimitedList(process.env.MCODA_DOCS_RFP_DEFINITION_ALLOWLIST);
1678
+ addResult(await runGate("gate-rfp-definition", "RFP Definition Coverage", () => runRfpDefinitionGate({
1679
+ rfpPath: runContext.rfpPath,
1680
+ allowlist: definitionAllowlist.length > 0 ? definitionAllowlist : undefined,
1681
+ })));
1682
+ addResult(await runGate("gate-pdr-interfaces", "PDR Interfaces", () => runPdrInterfacesGate({ artifacts: runContext.artifacts })));
1683
+ addResult(await runGate("gate-pdr-ownership", "PDR Ownership", () => runPdrOwnershipGate({ artifacts: runContext.artifacts })));
1684
+ const openQuestionsEnabled = resolveOpenQuestions;
1685
+ addResult(await runGate("gate-pdr-open-questions", "PDR Open Questions", () => runPdrOpenQuestionsGate({
1686
+ artifacts: runContext.artifacts,
1687
+ enabled: openQuestionsEnabled,
1688
+ })));
1689
+ addResult(await runGate("gate-sds-explicit-decisions", "SDS Explicit Decisions", () => runSdsDecisionsGate({ artifacts: runContext.artifacts })));
1690
+ addResult(await runGate("gate-sds-policy-telemetry", "SDS Policy Telemetry", () => runSdsPolicyTelemetryGate({ artifacts: runContext.artifacts })));
1691
+ addResult(await runGate("gate-sds-ops-observability-testing", "SDS Ops/Observability/Testing", () => runSdsOpsGate({ artifacts: runContext.artifacts })));
1692
+ addResult(await runGate("gate-sds-external-adapters", "SDS External Adapters", () => runSdsAdaptersGate({ artifacts: runContext.artifacts })));
1693
+ addResult(await runGate("gate-deployment-blueprint", "Deployment Blueprint", () => runDeploymentBlueprintGate({
1694
+ artifacts: runContext.artifacts,
1695
+ buildReady: runContext.flags.buildReady,
1696
+ })));
1697
+ addResult(await runGate("gate-build-ready-completeness", "Build Ready Completeness", () => runBuildReadyCompletenessGate({
1698
+ artifacts: runContext.artifacts,
1699
+ buildReady: runContext.flags.buildReady,
1700
+ })));
1701
+ return { gateResults, blockingIssues };
1702
+ }
1703
+ buildPatchPlanFromIssues(inputIssues) {
1704
+ const patchesByPath = new Map();
1705
+ const fixes = [];
1706
+ const seenRanges = new Set();
1707
+ for (const issue of inputIssues) {
1708
+ if (issue.gateId !== "gate-placeholder-artifacts")
1709
+ continue;
1710
+ if (issue.location.kind !== "line_range")
1711
+ continue;
1712
+ const key = `${issue.location.path}:${issue.location.lineStart}-${issue.location.lineEnd}`;
1713
+ if (seenRanges.has(key))
1714
+ continue;
1715
+ seenRanges.add(key);
1716
+ const patch = patchesByPath.get(issue.location.path) ??
1717
+ {
1718
+ path: issue.location.path,
1719
+ operations: [],
1720
+ };
1721
+ patch.operations.push({
1722
+ type: "remove_block",
1723
+ location: issue.location,
1724
+ });
1725
+ patchesByPath.set(issue.location.path, patch);
1726
+ fixes.push({
1727
+ issueId: issue.id,
1728
+ summary: `Removed placeholder content in ${path.basename(issue.location.path)}`,
1729
+ appliedAt: new Date().toISOString(),
1730
+ metadata: {
1731
+ gateId: issue.gateId,
1732
+ path: issue.location.path,
1733
+ lineStart: issue.location.lineStart,
1734
+ lineEnd: issue.location.lineEnd,
1735
+ },
1736
+ });
1737
+ }
1738
+ return { patches: Array.from(patchesByPath.values()), fixes };
1739
+ }
1740
+ resolveQuestionDecision(question) {
1741
+ const trimmed = question.trim().replace(/\?+$/, "").trim();
1742
+ if (!trimmed)
1743
+ return undefined;
1744
+ const patterns = [
1745
+ { pattern: /should we use\s+(.+)$/i, verb: "Use" },
1746
+ { pattern: /^use\s+(.+)$/i, verb: "Use" },
1747
+ { pattern: /^choose\s+(.+)$/i, verb: "Choose" },
1748
+ { pattern: /^select\s+(.+)$/i, verb: "Select" },
1749
+ { pattern: /decide on\s+(.+)$/i, verb: "Use" },
1750
+ ];
1751
+ for (const entry of patterns) {
1752
+ const match = trimmed.match(entry.pattern);
1753
+ if (!match)
1754
+ continue;
1755
+ const choice = match[1]?.trim() ?? "";
1756
+ if (!choice)
1757
+ continue;
1758
+ if (/\bor\b|\/|either/i.test(choice))
1759
+ continue;
1760
+ const suffix = choice.endsWith(".") ? "" : ".";
1761
+ return `${entry.verb} ${choice}${suffix}`;
1762
+ }
1763
+ return undefined;
1764
+ }
1765
+ sanitizeIndecisiveLine(line, patternId) {
1766
+ const patterns = {
1767
+ maybe: /\bmaybe\b/i,
1768
+ optional: /\boptional\b/i,
1769
+ could: /\bcould\b/i,
1770
+ might: /\bmight\b/i,
1771
+ possibly: /\bpossibly\b/i,
1772
+ either: /\beither\b/i,
1773
+ tbd: /\btbd\b/i,
1774
+ };
1775
+ const pattern = patterns[patternId];
1776
+ if (!pattern)
1777
+ return undefined;
1778
+ const match = line.match(/^(\s*[-*+]\s+|\s*)(.*)$/);
1779
+ const prefix = match?.[1] ?? "";
1780
+ const body = match?.[2] ?? line;
1781
+ const cleaned = body
1782
+ .replace(pattern, "")
1783
+ .replace(/\s{2,}/g, " ")
1784
+ .replace(/\s+([,.;:])/g, "$1")
1785
+ .trim();
1786
+ if (!cleaned || cleaned === body.trim())
1787
+ return undefined;
1788
+ return `${prefix}${cleaned}`;
1789
+ }
1790
+ formatResolvedDecisions(decisions) {
1791
+ const lines = [];
1792
+ for (const decision of decisions) {
1793
+ const metadata = (decision.metadata ?? {});
1794
+ const question = typeof metadata.question === "string" && metadata.question.trim().length > 0
1795
+ ? metadata.question.trim()
1796
+ : undefined;
1797
+ const suffix = question ? ` (question: ${question})` : "";
1798
+ lines.push(`- ${decision.summary}${suffix}`);
1799
+ }
1800
+ return lines.join("\n");
1801
+ }
1802
+ async resolveOpenQuestions(runContext, gateResults) {
1803
+ const enabled = runContext.flags.resolveOpenQuestions ||
1804
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
1805
+ if (!enabled)
1806
+ return { decisions: [], warnings: [] };
1807
+ const openQuestions = gateResults.find((gate) => gate.gateId === "gate-open-questions");
1808
+ if (!openQuestions || openQuestions.issues.length === 0) {
1809
+ return { decisions: [], warnings: [] };
1810
+ }
1811
+ const decisions = [];
1812
+ const warnings = [];
1813
+ const decisionTargets = new Map();
1814
+ const lineReplacements = new Map();
1815
+ const replacedLines = new Set();
1816
+ for (const issue of openQuestions.issues) {
1817
+ if (issue.location.kind !== "line_range")
1818
+ continue;
1819
+ const metadata = (issue.metadata ?? {});
1820
+ const question = typeof metadata.question === "string" && metadata.question.trim().length > 0
1821
+ ? metadata.question.trim()
1822
+ : undefined;
1823
+ const normalized = typeof metadata.normalized === "string" && metadata.normalized.trim().length > 0
1824
+ ? metadata.normalized.trim()
1825
+ : undefined;
1826
+ const required = metadata.required === true;
1827
+ if (!question)
1828
+ continue;
1829
+ const decisionSummary = this.resolveQuestionDecision(question);
1830
+ if (!decisionSummary) {
1831
+ if (required) {
1832
+ warnings.push(`Open question unresolved: ${question}`);
1833
+ }
1834
+ continue;
1835
+ }
1836
+ const fallbackId = question
1837
+ .toLowerCase()
1838
+ .replace(/[^a-z0-9]+/g, "-")
1839
+ .replace(/^-+|-+$/g, "");
1840
+ const decisionIdBase = normalized ?? (fallbackId || issue.id);
1841
+ const decisionId = `decision-${decisionIdBase}`;
1842
+ const decision = {
1843
+ id: decisionId,
1844
+ summary: decisionSummary,
1845
+ rationale: `Resolved from open question: "${question}"`,
1846
+ decidedAt: new Date().toISOString(),
1847
+ relatedIssueIds: [issue.id],
1848
+ metadata: {
1849
+ question,
1850
+ normalized,
1851
+ target: metadata.target,
1852
+ },
1853
+ };
1854
+ decisions.push(decision);
1855
+ const target = typeof metadata.target === "string" && metadata.target.trim().length > 0
1856
+ ? metadata.target.trim()
1857
+ : issue.artifact;
1858
+ let targetPath;
1859
+ if (target === "pdr" || issue.artifact === "pdr") {
1860
+ targetPath = runContext.artifacts.pdr?.path;
1861
+ }
1862
+ else if (target === "sds" || issue.artifact === "sds") {
1863
+ targetPath = runContext.artifacts.sds?.path;
1864
+ }
1865
+ else {
1866
+ targetPath = runContext.artifacts.sds?.path ?? runContext.artifacts.pdr?.path;
1867
+ }
1868
+ if (!targetPath) {
1869
+ warnings.push(`Resolved decision could not be inserted (missing target doc): ${question}`);
1870
+ }
1871
+ else {
1872
+ const existing = decisionTargets.get(targetPath);
1873
+ if (existing) {
1874
+ existing.push(decision);
1875
+ }
1876
+ else {
1877
+ decisionTargets.set(targetPath, [decision]);
1878
+ }
1879
+ }
1880
+ const lineIndex = issue.location.lineStart - 1;
1881
+ const key = `${issue.location.path}:${lineIndex}`;
1882
+ replacedLines.add(key);
1883
+ if (!lineReplacements.has(issue.location.path)) {
1884
+ lineReplacements.set(issue.location.path, new Map());
1885
+ }
1886
+ lineReplacements.get(issue.location.path)?.set(lineIndex, `Resolved: ${decision.summary}`);
1887
+ }
1888
+ const noMaybesGate = gateResults.find((gate) => gate.gateId === "gate-no-maybes");
1889
+ if (noMaybesGate && noMaybesGate.issues.length > 0) {
1890
+ const contentCache = new Map();
1891
+ const loadContent = async (filePath) => {
1892
+ if (contentCache.has(filePath))
1893
+ return contentCache.get(filePath);
1894
+ try {
1895
+ const content = await fs.readFile(filePath, "utf8");
1896
+ contentCache.set(filePath, content);
1897
+ return content;
1898
+ }
1899
+ catch (error) {
1900
+ warnings.push(`Unable to read ${filePath} for indecisive cleanup: ${error.message ?? String(error)}`);
1901
+ return undefined;
1902
+ }
1903
+ };
1904
+ for (const issue of noMaybesGate.issues) {
1905
+ if (issue.location.kind !== "line_range")
1906
+ continue;
1907
+ const lineIndex = issue.location.lineStart - 1;
1908
+ const key = `${issue.location.path}:${lineIndex}`;
1909
+ if (replacedLines.has(key))
1910
+ continue;
1911
+ const content = await loadContent(issue.location.path);
1912
+ if (!content)
1913
+ continue;
1914
+ const lines = content.split(/\r?\n/);
1915
+ if (lineIndex < 0 || lineIndex >= lines.length)
1916
+ continue;
1917
+ const line = lines[lineIndex] ?? "";
1918
+ const metadata = (issue.metadata ?? {});
1919
+ const patternId = typeof metadata.patternId === "string" ? metadata.patternId : "";
1920
+ const sanitized = this.sanitizeIndecisiveLine(line, patternId);
1921
+ if (!sanitized)
1922
+ continue;
1923
+ if (!lineReplacements.has(issue.location.path)) {
1924
+ lineReplacements.set(issue.location.path, new Map());
1925
+ }
1926
+ lineReplacements.get(issue.location.path)?.set(lineIndex, sanitized);
1927
+ }
1928
+ }
1929
+ if (lineReplacements.size === 0 && decisionTargets.size === 0) {
1930
+ return { decisions, warnings };
1931
+ }
1932
+ const patches = [];
1933
+ for (const [filePath, replacements] of lineReplacements.entries()) {
1934
+ const operations = [];
1935
+ const sorted = Array.from(replacements.entries()).sort((a, b) => a[0] - b[0]);
1936
+ for (const [lineIndex, content] of sorted) {
1937
+ const lineNumber = lineIndex + 1;
1938
+ operations.push({
1939
+ type: "replace_section",
1940
+ location: {
1941
+ kind: "line_range",
1942
+ path: filePath,
1943
+ lineStart: lineNumber,
1944
+ lineEnd: lineNumber,
1945
+ },
1946
+ content,
1947
+ });
1948
+ }
1949
+ const decisionSet = decisionTargets.get(filePath);
1950
+ if (decisionSet && decisionSet.length > 0) {
1951
+ operations.push({
1952
+ type: "insert_section",
1953
+ heading: "Resolved Decisions",
1954
+ content: this.formatResolvedDecisions(decisionSet),
1955
+ position: "append",
1956
+ headingLevel: 2,
1957
+ });
1958
+ }
1959
+ if (operations.length > 0) {
1960
+ patches.push({ path: filePath, operations });
1961
+ }
1962
+ }
1963
+ for (const [filePath, decisionSet] of decisionTargets.entries()) {
1964
+ if (lineReplacements.has(filePath))
1965
+ continue;
1966
+ if (!decisionSet.length)
1967
+ continue;
1968
+ patches.push({
1969
+ path: filePath,
1970
+ operations: [
1971
+ {
1972
+ type: "insert_section",
1973
+ heading: "Resolved Decisions",
1974
+ content: this.formatResolvedDecisions(decisionSet),
1975
+ position: "append",
1976
+ headingLevel: 2,
1977
+ },
1978
+ ],
1979
+ });
1980
+ }
1981
+ if (patches.length > 0) {
1982
+ const patchResult = await this.applyDocPatches(runContext, patches);
1983
+ if (patchResult.warnings.length > 0) {
1984
+ warnings.push(...patchResult.warnings);
1985
+ }
1986
+ }
1987
+ return { decisions, warnings };
1988
+ }
1989
+ reviewReportDir(runContext) {
1990
+ return path.join(this.workspace.mcodaDir, "jobs", runContext.jobId, "review");
1991
+ }
1992
+ reviewReportPaths(runContext, label) {
1993
+ const reportDir = this.reviewReportDir(runContext);
1994
+ const jobDir = path.join(this.workspace.mcodaDir, "jobs", runContext.jobId);
1995
+ const jsonPath = path.join(reportDir, `${label}.json`);
1996
+ const markdownPath = path.join(reportDir, `${label}.md`);
1997
+ const markdownRelative = path.relative(jobDir, markdownPath) || path.basename(markdownPath);
1998
+ return { jsonPath, markdownPath, markdownRelative };
1999
+ }
2000
+ buildReviewReport(input) {
2001
+ const outcome = aggregateReviewOutcome({
2002
+ gateResults: input.gateResults,
2003
+ remainingOpenItems: input.remainingOpenItems,
2004
+ fixesApplied: input.fixesApplied,
2005
+ decisions: input.decisions ?? [],
2006
+ generatedAt: new Date().toISOString(),
1045
2007
  });
1046
- return resolved.agent;
2008
+ const iterationReports = input.iterationReports && input.iterationReports.length > 0
2009
+ ? input.iterationReports
2010
+ : undefined;
2011
+ return {
2012
+ version: 1,
2013
+ generatedAt: outcome.generatedAt,
2014
+ iteration: {
2015
+ current: input.runContext.iteration.current,
2016
+ max: input.runContext.iteration.max,
2017
+ status: input.iterationStatus,
2018
+ },
2019
+ status: outcome.summary.status,
2020
+ summary: outcome.summary,
2021
+ gateResults: outcome.gateResults,
2022
+ issues: outcome.issues,
2023
+ remainingOpenItems: outcome.remainingOpenItems,
2024
+ fixesApplied: outcome.fixesApplied,
2025
+ decisions: outcome.decisions,
2026
+ deltas: input.deltas ?? [],
2027
+ metadata: {
2028
+ commandName: input.runContext.commandName,
2029
+ commandRunId: input.runContext.commandRunId,
2030
+ jobId: input.runContext.jobId,
2031
+ projectKey: input.runContext.projectKey,
2032
+ iterationReports,
2033
+ },
2034
+ };
2035
+ }
2036
+ async persistReviewReport(runContext, label, report) {
2037
+ const paths = this.reviewReportPaths(runContext, label);
2038
+ await ensureDir(paths.jsonPath);
2039
+ await fs.writeFile(paths.jsonPath, serializeReviewReport(report), "utf8");
2040
+ await fs.writeFile(paths.markdownPath, renderReviewReport(report), "utf8");
2041
+ return { markdownRelative: paths.markdownRelative };
2042
+ }
2043
+ async runIterationLoop(runContext) {
2044
+ const iterationEnabled = this.isIterationEnabled(runContext);
2045
+ const maxIterations = runContext.iteration.max > 0
2046
+ ? runContext.iteration.max
2047
+ : iterationEnabled
2048
+ ? this.resolveMaxIterations()
2049
+ : 1;
2050
+ runContext.iteration.max = maxIterations;
2051
+ const fixesApplied = [];
2052
+ const alignmentDeltas = [];
2053
+ const decisions = [];
2054
+ const iterationReports = [];
2055
+ let lastGateResults = [];
2056
+ let lastBlockingIssues = [];
2057
+ const alignmentPatcher = new DocAlignmentPatcher();
2058
+ let finalReportRelative;
2059
+ const persistIterationReport = async (status) => {
2060
+ const report = this.buildReviewReport({
2061
+ runContext,
2062
+ gateResults: lastGateResults,
2063
+ remainingOpenItems: lastBlockingIssues,
2064
+ fixesApplied,
2065
+ deltas: alignmentDeltas,
2066
+ decisions,
2067
+ iterationStatus: status,
2068
+ });
2069
+ const { markdownRelative } = await this.persistReviewReport(runContext, `review-iteration-${runContext.iteration.current}`, report);
2070
+ iterationReports.push(markdownRelative);
2071
+ };
2072
+ const persistFinalReport = async (status) => {
2073
+ const report = this.buildReviewReport({
2074
+ runContext,
2075
+ gateResults: lastGateResults,
2076
+ remainingOpenItems: lastBlockingIssues,
2077
+ fixesApplied,
2078
+ deltas: alignmentDeltas,
2079
+ decisions,
2080
+ iterationStatus: status,
2081
+ iterationReports,
2082
+ });
2083
+ const { markdownRelative } = await this.persistReviewReport(runContext, "review-final", report);
2084
+ finalReportRelative = markdownRelative;
2085
+ };
2086
+ for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
2087
+ runContext.iteration.current = iteration;
2088
+ await this.jobService.recordIterationProgress(runContext.jobId, {
2089
+ current: iteration,
2090
+ max: maxIterations,
2091
+ phase: "review",
2092
+ });
2093
+ const review = await this.runReviewGates(runContext, "review");
2094
+ lastGateResults = review.gateResults;
2095
+ lastBlockingIssues = review.blockingIssues;
2096
+ if (review.blockingIssues.length === 0) {
2097
+ await persistIterationReport("completed");
2098
+ await persistFinalReport("completed");
2099
+ return { gateResults: review.gateResults, fixesApplied, reviewReportPath: finalReportRelative };
2100
+ }
2101
+ if (!iterationEnabled) {
2102
+ await persistIterationReport("max_iterations");
2103
+ await persistFinalReport("max_iterations");
2104
+ const summary = this.summarizeIssues(review.blockingIssues);
2105
+ throw new Error(`Doc generation review failed. ${summary}`);
2106
+ }
2107
+ const questionResolution = await this.resolveOpenQuestions(runContext, review.gateResults);
2108
+ if (questionResolution.decisions.length > 0) {
2109
+ decisions.push(...questionResolution.decisions);
2110
+ }
2111
+ if (questionResolution.warnings.length > 0) {
2112
+ this.appendUniqueWarnings(runContext, questionResolution.warnings);
2113
+ }
2114
+ if (runContext.flags.crossAlign) {
2115
+ const alignmentResult = await alignmentPatcher.apply({
2116
+ runContext,
2117
+ gateResults: review.gateResults,
2118
+ });
2119
+ if (alignmentResult.warnings.length > 0) {
2120
+ this.appendUniqueWarnings(runContext, alignmentResult.warnings);
2121
+ }
2122
+ if (alignmentResult.deltas.length > 0) {
2123
+ alignmentDeltas.push(...alignmentResult.deltas);
2124
+ }
2125
+ }
2126
+ const patchPlan = this.buildPatchPlanFromIssues(review.blockingIssues);
2127
+ await this.jobService.recordIterationProgress(runContext.jobId, {
2128
+ current: iteration,
2129
+ max: maxIterations,
2130
+ phase: "patch",
2131
+ details: { patches: patchPlan.patches.length, fixes: patchPlan.fixes.length },
2132
+ });
2133
+ if (patchPlan.patches.length > 0) {
2134
+ fixesApplied.push(...patchPlan.fixes);
2135
+ const patchResult = await this.applyDocPatches(runContext, patchPlan.patches);
2136
+ if (patchResult.warnings.length > 0) {
2137
+ this.appendUniqueWarnings(runContext, patchResult.warnings);
2138
+ }
2139
+ await this.jobService.recordIterationProgress(runContext.jobId, {
2140
+ current: iteration,
2141
+ max: maxIterations,
2142
+ phase: "recheck",
2143
+ });
2144
+ const recheck = await this.runReviewGates(runContext, "recheck");
2145
+ lastGateResults = recheck.gateResults;
2146
+ lastBlockingIssues = recheck.blockingIssues;
2147
+ if (recheck.blockingIssues.length === 0) {
2148
+ await persistIterationReport("completed");
2149
+ await persistFinalReport("completed");
2150
+ return { gateResults: recheck.gateResults, fixesApplied, reviewReportPath: finalReportRelative };
2151
+ }
2152
+ }
2153
+ const iterationStatus = lastBlockingIssues.length === 0
2154
+ ? "completed"
2155
+ : iteration === maxIterations
2156
+ ? "max_iterations"
2157
+ : "in_progress";
2158
+ await persistIterationReport(iterationStatus);
2159
+ }
2160
+ const summary = this.summarizeIssues(lastBlockingIssues);
2161
+ await persistFinalReport("max_iterations");
2162
+ throw new Error(`Doc generation review failed after ${maxIterations} iteration(s). ${summary}`);
2163
+ }
2164
+ async enforcePlaceholderArtifacts(runContext) {
2165
+ if (!runContext.flags.noPlaceholders)
2166
+ return;
2167
+ const allowlist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_ALLOWLIST);
2168
+ const denylist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_DENYLIST);
2169
+ const result = await runPlaceholderArtifactGate({
2170
+ artifacts: runContext.artifacts,
2171
+ allowlist: allowlist.length > 0 ? allowlist : undefined,
2172
+ denylist: denylist.length > 0 ? denylist : undefined,
2173
+ });
2174
+ if (result.notes?.length) {
2175
+ runContext.warnings.push(...result.notes);
2176
+ }
2177
+ if (result.status !== "fail")
2178
+ return;
2179
+ const summary = result.issues
2180
+ .slice(0, 3)
2181
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2182
+ .join(" ");
2183
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2184
+ throw new Error(`Placeholder artifacts detected (${result.issues.length}). ${summary}${suffix}`);
2185
+ }
2186
+ async enforceApiPathConsistency(runContext) {
2187
+ const result = await runApiPathConsistencyGate({ artifacts: runContext.artifacts });
2188
+ if (result.notes?.length) {
2189
+ runContext.warnings.push(...result.notes);
2190
+ }
2191
+ if (result.status !== "fail")
2192
+ return;
2193
+ const summary = result.issues
2194
+ .slice(0, 3)
2195
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2196
+ .join(" ");
2197
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2198
+ const message = `API path consistency check failed (${result.issues.length}). ${summary}${suffix}`;
2199
+ if (runContext.flags.buildReady) {
2200
+ throw new Error(message);
2201
+ }
2202
+ runContext.warnings.push(message);
2203
+ }
2204
+ async enforceOpenApiSchemaSanity(runContext) {
2205
+ if (!runContext.artifacts.openapi?.length)
2206
+ return;
2207
+ const issues = [];
2208
+ for (const record of runContext.artifacts.openapi) {
2209
+ try {
2210
+ const content = await fs.readFile(record.path, "utf8");
2211
+ const result = validateOpenApiSchemaContent(content);
2212
+ if (result.errors.length > 0) {
2213
+ issues.push(...result.errors.map((error) => `${record.path}: ${error}`));
2214
+ }
2215
+ }
2216
+ catch (error) {
2217
+ runContext.warnings.push(`Unable to read OpenAPI spec ${record.path}: ${error.message ?? String(error)}`);
2218
+ }
2219
+ }
2220
+ if (issues.length === 0)
2221
+ return;
2222
+ const summary = issues.slice(0, 3).join(" ");
2223
+ const suffix = issues.length > 3 ? ` (+${issues.length - 3} more)` : "";
2224
+ const message = `OpenAPI schema sanity issues (${issues.length}). ${summary}${suffix}`;
2225
+ if (runContext.flags.buildReady) {
2226
+ throw new Error(message);
2227
+ }
2228
+ runContext.warnings.push(message);
2229
+ }
2230
+ async enforceOpenApiCoverage(runContext) {
2231
+ const result = await runOpenApiCoverageGate({ artifacts: runContext.artifacts });
2232
+ if (result.notes?.length) {
2233
+ runContext.warnings.push(...result.notes);
2234
+ }
2235
+ if (result.status !== "fail")
2236
+ return;
2237
+ const summary = result.issues
2238
+ .slice(0, 3)
2239
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2240
+ .join(" ");
2241
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2242
+ const message = `OpenAPI endpoint coverage check failed (${result.issues.length}). ${summary}${suffix}`;
2243
+ if (runContext.flags.buildReady) {
2244
+ throw new Error(message);
2245
+ }
2246
+ runContext.warnings.push(message);
2247
+ }
2248
+ async enforceSqlSyntax(runContext) {
2249
+ const result = await runSqlSyntaxGate({ artifacts: runContext.artifacts });
2250
+ if (result.notes?.length) {
2251
+ runContext.warnings.push(...result.notes);
2252
+ }
2253
+ if (result.status !== "fail")
2254
+ return;
2255
+ const summary = result.issues
2256
+ .slice(0, 3)
2257
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2258
+ .join(" ");
2259
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2260
+ const message = `SQL syntax validation failed (${result.issues.length}). ${summary}${suffix}`;
2261
+ if (runContext.flags.buildReady) {
2262
+ throw new Error(message);
2263
+ }
2264
+ runContext.warnings.push(message);
2265
+ }
2266
+ async enforceSqlRequiredTables(runContext) {
2267
+ const result = await runSqlRequiredTablesGate({ artifacts: runContext.artifacts });
2268
+ if (result.notes?.length) {
2269
+ runContext.warnings.push(...result.notes);
2270
+ }
2271
+ if (result.status !== "fail")
2272
+ return;
2273
+ const summary = result.issues
2274
+ .slice(0, 3)
2275
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2276
+ .join(" ");
2277
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2278
+ const message = `SQL required tables check failed (${result.issues.length}). ${summary}${suffix}`;
2279
+ if (runContext.flags.buildReady) {
2280
+ throw new Error(message);
2281
+ }
2282
+ runContext.warnings.push(message);
2283
+ }
2284
+ async generateDeploymentBlueprint(runContext, sdsContent, projectKey) {
2285
+ if (runContext.flags.dryRun) {
2286
+ runContext.warnings.push("Dry run enabled; deployment blueprint generation skipped.");
2287
+ return false;
2288
+ }
2289
+ const scaffolder = new DocsScaffolder();
2290
+ const openapiRecords = runContext.artifacts.openapi ?? [];
2291
+ const primaryOpenApi = openapiRecords.find((record) => record.variant !== "admin") ?? openapiRecords[0];
2292
+ let openapiContent;
2293
+ if (primaryOpenApi) {
2294
+ try {
2295
+ openapiContent = await fs.readFile(primaryOpenApi.path, "utf8");
2296
+ }
2297
+ catch (error) {
2298
+ runContext.warnings.push(`Unable to read OpenAPI spec ${primaryOpenApi.path} for deployment blueprint: ${error.message ?? String(error)}`);
2299
+ }
2300
+ }
2301
+ try {
2302
+ await scaffolder.generateDeploymentBlueprintFiles({
2303
+ sdsContent,
2304
+ openapiContent,
2305
+ outputDir: path.join(this.workspace.workspaceRoot, "deploy"),
2306
+ serviceName: projectKey ?? "app",
2307
+ });
2308
+ return true;
2309
+ }
2310
+ catch (error) {
2311
+ runContext.warnings.push(`Deployment blueprint generation failed: ${error.message ?? String(error)}`);
2312
+ return false;
2313
+ }
2314
+ }
2315
+ async enforceAdminOpenApiSpec(runContext) {
2316
+ const docRecords = [runContext.artifacts.pdr, runContext.artifacts.sds].filter((record) => Boolean(record));
2317
+ if (docRecords.length === 0)
2318
+ return;
2319
+ const mentions = [];
2320
+ for (const record of docRecords) {
2321
+ try {
2322
+ const content = await fs.readFile(record.path, "utf8");
2323
+ const found = findAdminSurfaceMentions(content);
2324
+ for (const mention of found) {
2325
+ mentions.push({
2326
+ record,
2327
+ line: mention.line,
2328
+ excerpt: mention.heading ? `${mention.heading}: ${mention.excerpt}` : mention.excerpt,
2329
+ });
2330
+ }
2331
+ }
2332
+ catch (error) {
2333
+ runContext.warnings.push(`Unable to scan ${record.path} for admin surface mentions: ${error.message ?? String(error)}`);
2334
+ }
2335
+ }
2336
+ if (mentions.length === 0)
2337
+ return;
2338
+ const openapiRecords = runContext.artifacts.openapi ?? [];
2339
+ const hasAdminSpec = openapiRecords.some((record) => record.variant === "admin" || /admin/i.test(path.basename(record.path)));
2340
+ if (hasAdminSpec)
2341
+ return;
2342
+ const summary = mentions
2343
+ .slice(0, 2)
2344
+ .map((entry) => `${path.basename(entry.record.path)}:${entry.line} ${entry.excerpt}`)
2345
+ .join(" | ");
2346
+ const suffix = mentions.length > 2 ? ` (+${mentions.length - 2} more)` : "";
2347
+ const message = `Admin OpenAPI spec required (admin surfaces referenced): ${summary}${suffix}`;
2348
+ if (runContext.flags.buildReady) {
2349
+ throw new Error(message);
2350
+ }
2351
+ runContext.warnings.push(message);
2352
+ }
2353
+ async enforceTerminologyNormalization(runContext) {
2354
+ const result = await runTerminologyNormalizationGate({ artifacts: runContext.artifacts });
2355
+ if (result.notes?.length) {
2356
+ runContext.warnings.push(...result.notes);
2357
+ }
2358
+ if (result.status === "pass")
2359
+ return;
2360
+ const summary = result.issues
2361
+ .slice(0, 3)
2362
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2363
+ .join(" ");
2364
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2365
+ const message = `Terminology normalization findings (${result.issues.length}). ${summary}${suffix}`;
2366
+ if (result.status === "fail" && runContext.flags.buildReady) {
2367
+ throw new Error(message);
2368
+ }
2369
+ runContext.warnings.push(message);
2370
+ }
2371
+ async enforceOpenQuestions(runContext) {
2372
+ const result = await runOpenQuestionsGate({ artifacts: runContext.artifacts });
2373
+ if (result.notes?.length) {
2374
+ runContext.warnings.push(...result.notes);
2375
+ }
2376
+ if (result.status === "pass")
2377
+ return;
2378
+ const summary = result.issues
2379
+ .slice(0, 3)
2380
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2381
+ .join(" ");
2382
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2383
+ const message = `Open questions detected (${result.issues.length}). ${summary}${suffix}`;
2384
+ if (result.status === "fail" && runContext.flags.buildReady) {
2385
+ throw new Error(message);
2386
+ }
2387
+ runContext.warnings.push(message);
2388
+ }
2389
+ async enforceNoMaybes(runContext) {
2390
+ const enabled = runContext.flags.noMaybes ||
2391
+ runContext.flags.resolveOpenQuestions ||
2392
+ process.env.MCODA_DOCS_NO_MAYBES === "1" ||
2393
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
2394
+ const result = await runNoMaybesGate({ artifacts: runContext.artifacts, enabled });
2395
+ if (result.notes?.length) {
2396
+ runContext.warnings.push(...result.notes);
2397
+ }
2398
+ if (result.status !== "fail")
2399
+ return;
2400
+ const summary = result.issues
2401
+ .slice(0, 3)
2402
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2403
+ .join(" ");
2404
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2405
+ throw new Error(`Indecisive language detected (${result.issues.length}). ${summary}${suffix}`);
2406
+ }
2407
+ async enforceRfpConsent(runContext) {
2408
+ const result = await runRfpConsentGate({ rfpPath: runContext.rfpPath });
2409
+ if (result.notes?.length) {
2410
+ runContext.warnings.push(...result.notes);
2411
+ }
2412
+ if (result.status === "pass" || result.status === "skipped")
2413
+ return;
2414
+ const summary = result.issues
2415
+ .slice(0, 3)
2416
+ .map((issue) => `${issue.message}`)
2417
+ .join(" ");
2418
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2419
+ const message = `RFP consent contradictions detected (${result.issues.length}). ${summary}${suffix}`;
2420
+ if (runContext.flags.buildReady) {
2421
+ throw new Error(message);
2422
+ }
2423
+ runContext.warnings.push(message);
2424
+ }
2425
+ async enforceRfpDefinitionCoverage(runContext) {
2426
+ const allowlist = parseDelimitedList(process.env.MCODA_DOCS_RFP_DEFINITION_ALLOWLIST);
2427
+ const result = await runRfpDefinitionGate({
2428
+ rfpPath: runContext.rfpPath,
2429
+ allowlist: allowlist.length > 0 ? allowlist : undefined,
2430
+ });
2431
+ if (result.notes?.length) {
2432
+ runContext.warnings.push(...result.notes);
2433
+ }
2434
+ if (result.status === "pass" || result.status === "skipped")
2435
+ return;
2436
+ const summary = result.issues
2437
+ .slice(0, 3)
2438
+ .map((issue) => issue.message)
2439
+ .join(" ");
2440
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2441
+ const message = `RFP definition coverage issues (${result.issues.length}). ${summary}${suffix}`;
2442
+ if (runContext.flags.buildReady) {
2443
+ throw new Error(message);
2444
+ }
2445
+ runContext.warnings.push(message);
2446
+ }
2447
+ async enforcePdrInterfaces(runContext) {
2448
+ const result = await runPdrInterfacesGate({ artifacts: runContext.artifacts });
2449
+ if (result.notes?.length) {
2450
+ runContext.warnings.push(...result.notes);
2451
+ }
2452
+ if (result.status === "pass" || result.status === "skipped")
2453
+ return;
2454
+ const summary = result.issues
2455
+ .slice(0, 3)
2456
+ .map((issue) => issue.message)
2457
+ .join(" ");
2458
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2459
+ const message = `PDR interface/pipeline issues (${result.issues.length}). ${summary}${suffix}`;
2460
+ if (runContext.flags.buildReady) {
2461
+ throw new Error(message);
2462
+ }
2463
+ runContext.warnings.push(message);
2464
+ }
2465
+ async enforcePdrOwnership(runContext) {
2466
+ const result = await runPdrOwnershipGate({ artifacts: runContext.artifacts });
2467
+ if (result.notes?.length) {
2468
+ runContext.warnings.push(...result.notes);
2469
+ }
2470
+ if (result.status === "pass" || result.status === "skipped")
2471
+ return;
2472
+ const summary = result.issues
2473
+ .slice(0, 3)
2474
+ .map((issue) => issue.message)
2475
+ .join(" ");
2476
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2477
+ const message = `PDR ownership/consent flow issues (${result.issues.length}). ${summary}${suffix}`;
2478
+ if (runContext.flags.buildReady) {
2479
+ throw new Error(message);
2480
+ }
2481
+ runContext.warnings.push(message);
2482
+ }
2483
+ async enforcePdrOpenQuestionsQuality(runContext) {
2484
+ const enabled = process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
2485
+ const result = await runPdrOpenQuestionsGate({ artifacts: runContext.artifacts, enabled });
2486
+ if (result.notes?.length) {
2487
+ runContext.warnings.push(...result.notes);
2488
+ }
2489
+ if (result.status === "pass" || result.status === "skipped")
2490
+ return;
2491
+ const summary = result.issues
2492
+ .slice(0, 3)
2493
+ .map((issue) => issue.message)
2494
+ .join(" ");
2495
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2496
+ const message = `PDR open question quality issues (${result.issues.length}). ${summary}${suffix}`;
2497
+ throw new Error(message);
2498
+ }
2499
+ async enforceSdsExplicitDecisions(runContext) {
2500
+ const result = await runSdsDecisionsGate({ artifacts: runContext.artifacts });
2501
+ if (result.notes?.length) {
2502
+ runContext.warnings.push(...result.notes);
2503
+ }
2504
+ if (result.status === "pass" || result.status === "skipped")
2505
+ return;
2506
+ const summary = result.issues
2507
+ .slice(0, 3)
2508
+ .map((issue) => issue.message)
2509
+ .join(" ");
2510
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2511
+ const message = `SDS explicit decision issues (${result.issues.length}). ${summary}${suffix}`;
2512
+ if (runContext.flags.buildReady) {
2513
+ throw new Error(message);
2514
+ }
2515
+ runContext.warnings.push(message);
2516
+ }
2517
+ async enforceSdsPolicyTelemetry(runContext) {
2518
+ const result = await runSdsPolicyTelemetryGate({ artifacts: runContext.artifacts });
2519
+ if (result.notes?.length) {
2520
+ runContext.warnings.push(...result.notes);
2521
+ }
2522
+ if (result.status === "pass" || result.status === "skipped")
2523
+ return;
2524
+ const summary = result.issues
2525
+ .slice(0, 3)
2526
+ .map((issue) => issue.message)
2527
+ .join(" ");
2528
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2529
+ const message = `SDS policy/telemetry/metering issues (${result.issues.length}). ${summary}${suffix}`;
2530
+ if (runContext.flags.buildReady) {
2531
+ throw new Error(message);
2532
+ }
2533
+ runContext.warnings.push(message);
2534
+ }
2535
+ async enforceSdsOpsObservabilityTesting(runContext) {
2536
+ const result = await runSdsOpsGate({ artifacts: runContext.artifacts });
2537
+ if (result.notes?.length) {
2538
+ runContext.warnings.push(...result.notes);
2539
+ }
2540
+ if (result.status === "pass" || result.status === "skipped")
2541
+ return;
2542
+ const summary = result.issues
2543
+ .slice(0, 3)
2544
+ .map((issue) => issue.message)
2545
+ .join(" ");
2546
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2547
+ const message = `SDS ops/observability/testing issues (${result.issues.length}). ${summary}${suffix}`;
2548
+ if (runContext.flags.buildReady) {
2549
+ throw new Error(message);
2550
+ }
2551
+ runContext.warnings.push(message);
2552
+ }
2553
+ async enforceSdsExternalAdapters(runContext) {
2554
+ const result = await runSdsAdaptersGate({ artifacts: runContext.artifacts });
2555
+ if (result.notes?.length) {
2556
+ runContext.warnings.push(...result.notes);
2557
+ }
2558
+ if (result.status === "pass" || result.status === "skipped")
2559
+ return;
2560
+ const summary = result.issues
2561
+ .slice(0, 3)
2562
+ .map((issue) => issue.message)
2563
+ .join(" ");
2564
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2565
+ const message = `SDS external adapter issues (${result.issues.length}). ${summary}${suffix}`;
2566
+ if (runContext.flags.buildReady) {
2567
+ throw new Error(message);
2568
+ }
2569
+ runContext.warnings.push(message);
2570
+ }
2571
+ async enforceDeploymentBlueprint(runContext) {
2572
+ const result = await runDeploymentBlueprintGate({
2573
+ artifacts: runContext.artifacts,
2574
+ buildReady: runContext.flags.buildReady,
2575
+ });
2576
+ if (result.notes?.length) {
2577
+ runContext.warnings.push(...result.notes);
2578
+ }
2579
+ if (result.status === "pass" || result.status === "skipped")
2580
+ return;
2581
+ const summary = result.issues
2582
+ .slice(0, 3)
2583
+ .map((issue) => `${issue.message}`)
2584
+ .join(" ");
2585
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2586
+ const message = `Deployment blueprint validation issues (${result.issues.length}). ${summary}${suffix}`;
2587
+ if (result.status === "fail") {
2588
+ throw new Error(message);
2589
+ }
2590
+ runContext.warnings.push(message);
2591
+ }
2592
+ async enforceBuildReadyCompleteness(runContext) {
2593
+ const result = await runBuildReadyCompletenessGate({
2594
+ artifacts: runContext.artifacts,
2595
+ buildReady: runContext.flags.buildReady,
2596
+ });
2597
+ if (result.notes?.length) {
2598
+ runContext.warnings.push(...result.notes);
2599
+ }
2600
+ if (result.status === "pass" || result.status === "skipped")
2601
+ return;
2602
+ const summary = result.issues
2603
+ .slice(0, 3)
2604
+ .map((issue) => `${issue.artifact}: ${issue.message}`)
2605
+ .join(" ");
2606
+ const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
2607
+ const message = `Build-ready completeness check failed (${result.issues.length}). ${summary}${suffix}`;
2608
+ if (result.status === "fail") {
2609
+ throw new Error(message);
2610
+ }
2611
+ runContext.warnings.push(message);
1047
2612
  }
1048
2613
  async writePdrFile(outPath, content) {
1049
2614
  await ensureDir(outPath);
@@ -1067,16 +2632,18 @@ export class DocsService {
1067
2632
  await ensureDir(outPath);
1068
2633
  await fs.writeFile(outPath, content, "utf8");
1069
2634
  }
1070
- async assertSdsDocdexProfile() {
2635
+ async checkSdsDocdexProfile(warnings) {
1071
2636
  const base = this.workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL;
1072
2637
  if (base)
1073
2638
  return;
1074
- const localStore = path.join(this.workspace.workspaceRoot, ".mcoda", "docdex", "documents.json");
2639
+ const localStore = path.join(this.workspace.mcodaDir, "docdex", "documents.json");
1075
2640
  try {
1076
2641
  await fs.access(localStore);
2642
+ return;
1077
2643
  }
1078
2644
  catch {
1079
- throw new Error("Docdex is not configured for SDS retrieval (missing docdexUrl and no local store). Configure docdexUrl or index docs with an sds_default profile.");
2645
+ // No docdex URL or local store; continue with local docs if present.
2646
+ warnings.push("Docdex is not configured for SDS retrieval; attempting local docs (no local docdex store found).");
1080
2647
  }
1081
2648
  }
1082
2649
  async registerSds(outPath, content, projectKey) {
@@ -1163,24 +2730,95 @@ export class DocsService {
1163
2730
  workspaceId: this.workspace.workspaceId,
1164
2731
  commandName: "docs-pdr-generate",
1165
2732
  jobId: job.id,
2733
+ commandRunId: commandRun.id,
1166
2734
  action: "docdex_context",
1167
2735
  promptTokens: 0,
1168
2736
  completionTokens: 0,
1169
2737
  metadata: { docdexAvailable: context.docdexAvailable },
1170
2738
  });
1171
- const agent = await this.resolveAgent(options.agentName);
2739
+ const stream = options.agentStream ?? true;
2740
+ const iterate = options.iterate === true;
2741
+ const fastMode = iterate ? false : options.fast === true || process.env.MCODA_DOCS_FAST === "1";
2742
+ const skipValidation = process.env.MCODA_SKIP_PDR_VALIDATION === "1";
2743
+ const buildReady = options.buildReady === true || process.env.MCODA_DOCS_BUILD_READY === "1";
2744
+ const resolveOpenQuestions = options.resolveOpenQuestions === true ||
2745
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
2746
+ const noMaybes = options.noMaybes === true ||
2747
+ process.env.MCODA_DOCS_NO_MAYBES === "1" ||
2748
+ resolveOpenQuestions;
2749
+ const noPlaceholders = options.noPlaceholders === true ||
2750
+ process.env.MCODA_DOCS_NO_PLACEHOLDERS === "1" ||
2751
+ buildReady;
2752
+ const crossAlign = options.crossAlign !== false;
2753
+ const iterationEnabled = !options.dryRun && !fastMode;
2754
+ const maxIterations = iterationEnabled ? this.resolveMaxIterations() : 1;
2755
+ const agent = await this.selectDocgenAgent({
2756
+ agentName: options.agentName,
2757
+ commandName: "docs-pdr-generate",
2758
+ commandAliases: ["docs-pdr-generate", "docs:pdr:generate", "pdr"],
2759
+ jobId: job.id,
2760
+ warnings: context.warnings,
2761
+ iterationEnabled,
2762
+ });
2763
+ const outputPath = options.outPath ?? this.defaultPdrOutputPath(options.projectKey, context.rfp.path);
2764
+ const runContext = this.createRunContext({
2765
+ commandName: "docs-pdr-generate",
2766
+ commandRunId: commandRun.id,
2767
+ jobId: job.id,
2768
+ projectKey: options.projectKey,
2769
+ rfpId: options.rfpId,
2770
+ rfpPath: context.rfp.path ?? options.rfpPath,
2771
+ outputPath,
2772
+ flags: {
2773
+ dryRun: options.dryRun === true,
2774
+ fast: fastMode,
2775
+ iterate,
2776
+ json: options.json === true,
2777
+ stream,
2778
+ buildReady,
2779
+ noPlaceholders,
2780
+ resolveOpenQuestions,
2781
+ noMaybes,
2782
+ crossAlign,
2783
+ },
2784
+ warnings: context.warnings,
2785
+ });
2786
+ runContext.iteration.max = maxIterations;
2787
+ const stateCleanupWarnings = await cleanupWorkspaceStateDirs({
2788
+ workspaceRoot: this.workspace.workspaceRoot,
2789
+ mcodaDir: this.workspace.mcodaDir,
2790
+ });
2791
+ const { statePath: iterativeOutputPath, warnings: statePathWarnings } = resolveDocgenStatePath({
2792
+ outputPath: runContext.outputPath,
2793
+ mcodaDir: this.workspace.mcodaDir,
2794
+ jobId: runContext.jobId,
2795
+ commandName: runContext.commandName,
2796
+ });
2797
+ const stateWarnings = [...stateCleanupWarnings, ...statePathWarnings];
2798
+ if (stateWarnings.length > 0) {
2799
+ runContext.stateWarnings = stateWarnings;
2800
+ this.appendUniqueWarnings(runContext, stateWarnings);
2801
+ }
2802
+ runContext.artifacts.pdr = { kind: "pdr", path: runContext.outputPath, meta: {} };
2803
+ await this.enforceToolDenylist({ runContext, agent });
2804
+ await this.recordDocgenStage(runContext, {
2805
+ stage: "generation",
2806
+ message: "Generating PDR draft",
2807
+ totalItems: maxIterations,
2808
+ processedItems: 0,
2809
+ });
1172
2810
  const prompts = await this.agentService.getPrompts(agent.id);
1173
2811
  const runbook = (await readPromptIfExists(this.workspace, path.join("prompts", "commands", "pdr-generate.md"))) ||
1174
2812
  DEFAULT_PDR_RUNBOOK_PROMPT;
1175
2813
  let draft = "";
2814
+ let agentUsed = false;
1176
2815
  let agentMetadata;
1177
2816
  let adapter = agent.adapter;
1178
- const stream = options.agentStream ?? true;
1179
- const skipValidation = process.env.MCODA_SKIP_PDR_VALIDATION === "1";
1180
2817
  let lastInvoke;
1181
2818
  for (let attempt = 0; attempt < 2; attempt += 1) {
1182
2819
  const prompt = buildRunPrompt(context, options.projectKey, prompts, attempt === 0 ? runbook : `${runbook}\n\nRETRY: The previous attempt failed validation. Ensure all required sections are present and non-empty. Do not leave placeholders.`);
1183
2820
  const invoke = async (input) => {
2821
+ agentUsed = true;
1184
2822
  const { output: out, adapter: usedAdapter, metadata } = await this.invokeAgent(agent, input, stream, job.id, options.onToken);
1185
2823
  adapter = usedAdapter;
1186
2824
  agentMetadata = metadata;
@@ -1195,12 +2833,18 @@ export class DocsService {
1195
2833
  workspaceId: this.workspace.workspaceId,
1196
2834
  commandName: "docs-pdr-generate",
1197
2835
  jobId: job.id,
2836
+ commandRunId: commandRun.id,
1198
2837
  agentId: agent.id,
1199
2838
  modelName: agent.defaultModel,
1200
2839
  action: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
1201
2840
  promptTokens: estimateTokens(prompt),
1202
2841
  completionTokens: estimateTokens(agentOutput),
1203
- metadata: { adapter, docdexAvailable: context.docdexAvailable, attempt },
2842
+ metadata: {
2843
+ adapter,
2844
+ docdexAvailable: context.docdexAvailable,
2845
+ attempt: attempt + 1,
2846
+ phase: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
2847
+ },
1204
2848
  });
1205
2849
  if (valid) {
1206
2850
  draft = structured;
@@ -1218,11 +2862,14 @@ export class DocsService {
1218
2862
  if (!draft) {
1219
2863
  throw new Error("PDR draft generation failed; no valid draft produced.");
1220
2864
  }
1221
- if (lastInvoke) {
2865
+ if (fastMode) {
2866
+ context.warnings.push("Fast mode enabled; skipping PDR enrichment and tidy passes.");
2867
+ }
2868
+ else if (lastInvoke) {
1222
2869
  draft = await enrichPdrDraft(draft, agent, context, options.projectKey, lastInvoke);
1223
2870
  draft = ensureStructuredDraft(draft, options.projectKey, context, context.rfp.path ?? context.rfp.id ?? "RFP");
1224
2871
  }
1225
- if (lastInvoke) {
2872
+ if (!fastMode && lastInvoke) {
1226
2873
  try {
1227
2874
  const tidiedRaw = await tidyPdrDraft(draft, agent, lastInvoke);
1228
2875
  const tidied = ensureStructuredDraft(tidiedRaw, options.projectKey, context, context.rfp.path ?? context.rfp.id ?? "RFP");
@@ -1236,17 +2883,21 @@ export class DocsService {
1236
2883
  context.warnings.push(`Tidy pass skipped: ${error.message ?? "unknown error"}`);
1237
2884
  }
1238
2885
  }
1239
- const outputPath = options.outPath ?? this.defaultPdrOutputPath(options.projectKey, context.rfp.path);
1240
2886
  if (!options.dryRun) {
1241
- const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "pdr", `${path.basename(outputPath, path.extname(outputPath))}-first-draft.md`);
2887
+ const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "pdr", `${path.basename(runContext.outputPath, path.extname(runContext.outputPath))}-first-draft.md`);
1242
2888
  await ensureDir(firstDraftPath);
1243
2889
  await fs.writeFile(firstDraftPath, draft, "utf8");
1244
- try {
1245
- const iterativeDraft = await buildIterativePdr(options.projectKey, context, draft, outputPath, lastInvoke ?? (async (input) => this.invokeAgent(agent, input, stream, job.id, options.onToken)));
1246
- draft = iterativeDraft;
2890
+ if (fastMode) {
2891
+ context.warnings.push("Fast mode enabled; skipping iterative PDR refinement.");
1247
2892
  }
1248
- catch (error) {
1249
- context.warnings.push(`Iterative PDR refinement failed; keeping first draft. ${String(error)}`);
2893
+ else {
2894
+ try {
2895
+ const iterativeDraft = await buildIterativePdr(options.projectKey, context, draft, iterativeOutputPath, lastInvoke ?? (async (input) => this.invokeAgent(agent, input, stream, job.id, options.onToken)));
2896
+ draft = iterativeDraft;
2897
+ }
2898
+ catch (error) {
2899
+ context.warnings.push(`Iterative PDR refinement failed; keeping first draft. ${String(error)}`);
2900
+ }
1250
2901
  }
1251
2902
  }
1252
2903
  await this.jobService.writeCheckpoint(job.id, {
@@ -1257,23 +2908,29 @@ export class DocsService {
1257
2908
  let docdexId;
1258
2909
  let segments;
1259
2910
  let mirrorStatus = "skipped";
2911
+ let reviewReportPath;
1260
2912
  if (options.dryRun) {
1261
2913
  context.warnings.push("Dry run enabled; PDR was not written to disk or registered in docdex.");
1262
2914
  }
1263
2915
  if (!options.dryRun) {
1264
- await this.writePdrFile(outputPath, draft);
2916
+ await this.writePdrFile(runContext.outputPath, draft);
1265
2917
  if (context.docdexAvailable) {
1266
- const registered = await this.registerPdr(outputPath, draft, options.projectKey);
1267
- docdexId = registered.id;
1268
- segments = (registered.segments ?? []).map((s) => s.id);
1269
- await fs.writeFile(`${outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
2918
+ try {
2919
+ const registered = await this.registerPdr(runContext.outputPath, draft, options.projectKey);
2920
+ docdexId = registered.id;
2921
+ segments = (registered.segments ?? []).map((s) => s.id);
2922
+ await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
2923
+ }
2924
+ catch (error) {
2925
+ context.warnings.push(`Docdex registration skipped: ${error.message}`);
2926
+ }
1270
2927
  }
1271
2928
  const publicDocsDir = path.join(this.workspace.workspaceRoot, "docs", "pdr");
1272
2929
  const shouldMirror = this.workspace.config?.mirrorDocs !== false;
1273
2930
  if (shouldMirror) {
1274
2931
  try {
1275
2932
  await ensureDir(path.join(publicDocsDir, "placeholder"));
1276
- const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
2933
+ const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
1277
2934
  await ensureDir(mirrorPath);
1278
2935
  await fs.writeFile(mirrorPath, draft, "utf8");
1279
2936
  mirrorStatus = "mirrored";
@@ -1283,18 +2940,54 @@ export class DocsService {
1283
2940
  mirrorStatus = "failed";
1284
2941
  }
1285
2942
  }
2943
+ try {
2944
+ await this.recordDocgenStage(runContext, {
2945
+ stage: "inventory",
2946
+ message: "Building doc inventory",
2947
+ });
2948
+ runContext.artifacts = await buildDocInventory({
2949
+ workspace: this.workspace,
2950
+ preferred: { pdrPath: runContext.outputPath },
2951
+ });
2952
+ }
2953
+ catch (error) {
2954
+ runContext.warnings.push(`Doc inventory build failed: ${error.message ?? String(error)}`);
2955
+ }
2956
+ const iterationResult = await this.runIterationLoop(runContext);
2957
+ reviewReportPath = iterationResult.reviewReportPath;
1286
2958
  }
1287
2959
  await this.jobService.updateJobStatus(job.id, "completed", {
1288
- payload: { outputPath, docdexId, segments, mirrorStatus },
2960
+ payload: {
2961
+ outputPath: runContext.outputPath,
2962
+ docdexId,
2963
+ segments,
2964
+ mirrorStatus,
2965
+ ...(reviewReportPath ? { reviewReportPath } : {}),
2966
+ },
1289
2967
  });
1290
2968
  await this.jobService.finishCommandRun(commandRun.id, "succeeded");
2969
+ if (options.rateAgents && agentUsed) {
2970
+ try {
2971
+ const ratingService = await this.ensureRatingService();
2972
+ await ratingService.rate({
2973
+ workspace: this.workspace,
2974
+ agentId: agent.id,
2975
+ commandName: "docs-pdr-generate",
2976
+ jobId: job.id,
2977
+ commandRunId: commandRun.id,
2978
+ });
2979
+ }
2980
+ catch (error) {
2981
+ context.warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
2982
+ }
2983
+ }
1291
2984
  return {
1292
2985
  jobId: job.id,
1293
2986
  commandRunId: commandRun.id,
1294
- outputPath,
2987
+ outputPath: runContext.outputPath,
1295
2988
  draft,
1296
2989
  docdexId,
1297
- warnings: context.warnings,
2990
+ warnings: runContext.warnings,
1298
2991
  };
1299
2992
  }
1300
2993
  catch (error) {
@@ -1363,9 +3056,9 @@ export class DocsService {
1363
3056
  }
1364
3057
  }
1365
3058
  async generateSds(options) {
1366
- await this.assertSdsDocdexProfile();
1367
- const assembler = new DocContextAssembler(this.docdex, this.workspace);
1368
3059
  const warnings = [];
3060
+ await this.checkSdsDocdexProfile(warnings);
3061
+ const assembler = new DocContextAssembler(this.docdex, this.workspace);
1369
3062
  const commandRun = await this.jobService.startCommandRun("docs-sds-generate", options.projectKey);
1370
3063
  let job;
1371
3064
  let resumeDraft;
@@ -1423,6 +3116,7 @@ export class DocsService {
1423
3116
  workspaceId: this.workspace.workspaceId,
1424
3117
  commandName: "docs-sds-generate",
1425
3118
  jobId: job.id,
3119
+ commandRunId: commandRun.id,
1426
3120
  action: "docdex_context",
1427
3121
  promptTokens: 0,
1428
3122
  completionTokens: 0,
@@ -1444,7 +3138,76 @@ export class DocsService {
1444
3138
  }
1445
3139
  }
1446
3140
  }
1447
- const agent = await this.resolveAgent(options.agentName, ["docs-sds-generate", "docs:sds:generate", "sds"]);
3141
+ const stream = options.agentStream ?? true;
3142
+ const iterate = options.iterate === true;
3143
+ const fastMode = iterate ? false : options.fast === true || process.env.MCODA_DOCS_FAST === "1";
3144
+ const skipValidation = process.env.MCODA_SKIP_SDS_VALIDATION === "1";
3145
+ const buildReady = options.buildReady === true || process.env.MCODA_DOCS_BUILD_READY === "1";
3146
+ const resolveOpenQuestions = options.resolveOpenQuestions === true ||
3147
+ process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
3148
+ const noMaybes = options.noMaybes === true ||
3149
+ process.env.MCODA_DOCS_NO_MAYBES === "1" ||
3150
+ resolveOpenQuestions;
3151
+ const noPlaceholders = options.noPlaceholders === true ||
3152
+ process.env.MCODA_DOCS_NO_PLACEHOLDERS === "1" ||
3153
+ buildReady;
3154
+ const crossAlign = options.crossAlign !== false;
3155
+ const iterationEnabled = !options.dryRun && !fastMode;
3156
+ const maxIterations = iterationEnabled ? this.resolveMaxIterations() : 1;
3157
+ const agent = await this.selectDocgenAgent({
3158
+ agentName: options.agentName,
3159
+ commandName: "docs-sds-generate",
3160
+ commandAliases: ["docs-sds-generate", "docs:sds:generate", "sds"],
3161
+ jobId: job.id,
3162
+ warnings,
3163
+ iterationEnabled,
3164
+ });
3165
+ const runContext = this.createRunContext({
3166
+ commandName: "docs-sds-generate",
3167
+ commandRunId: commandRun.id,
3168
+ jobId: job.id,
3169
+ projectKey: options.projectKey,
3170
+ rfpPath: context.rfp?.path,
3171
+ templateName: options.templateName,
3172
+ outputPath,
3173
+ flags: {
3174
+ dryRun: options.dryRun === true,
3175
+ fast: fastMode,
3176
+ iterate,
3177
+ json: options.json === true,
3178
+ stream,
3179
+ buildReady,
3180
+ noPlaceholders,
3181
+ resolveOpenQuestions,
3182
+ noMaybes,
3183
+ crossAlign,
3184
+ },
3185
+ warnings,
3186
+ });
3187
+ runContext.iteration.max = maxIterations;
3188
+ const stateCleanupWarnings = await cleanupWorkspaceStateDirs({
3189
+ workspaceRoot: this.workspace.workspaceRoot,
3190
+ mcodaDir: this.workspace.mcodaDir,
3191
+ });
3192
+ const { statePath: iterativeOutputPath, warnings: statePathWarnings } = resolveDocgenStatePath({
3193
+ outputPath: runContext.outputPath,
3194
+ mcodaDir: this.workspace.mcodaDir,
3195
+ jobId: runContext.jobId,
3196
+ commandName: runContext.commandName,
3197
+ });
3198
+ const stateWarnings = [...stateCleanupWarnings, ...statePathWarnings];
3199
+ if (stateWarnings.length > 0) {
3200
+ runContext.stateWarnings = stateWarnings;
3201
+ this.appendUniqueWarnings(runContext, stateWarnings);
3202
+ }
3203
+ runContext.artifacts.sds = { kind: "sds", path: runContext.outputPath, meta: {} };
3204
+ await this.enforceToolDenylist({ runContext, agent });
3205
+ await this.recordDocgenStage(runContext, {
3206
+ stage: "generation",
3207
+ message: "Generating SDS draft",
3208
+ totalItems: maxIterations,
3209
+ processedItems: 0,
3210
+ });
1448
3211
  const prompts = await this.agentService.getPrompts(agent.id);
1449
3212
  const template = await this.loadSdsTemplate(options.templateName);
1450
3213
  const sdsSections = getSdsSections(template.content);
@@ -1452,11 +3215,11 @@ export class DocsService {
1452
3215
  (await readPromptIfExists(this.workspace, path.join("prompts", "sds", "generate.md"))) ||
1453
3216
  DEFAULT_SDS_RUNBOOK_PROMPT;
1454
3217
  let draft = resumeDraft ?? "";
3218
+ let agentUsed = false;
1455
3219
  let agentMetadata;
1456
3220
  let adapter = agent.adapter;
1457
- const stream = options.agentStream ?? true;
1458
- const skipValidation = process.env.MCODA_SKIP_SDS_VALIDATION === "1";
1459
3221
  const invoke = async (input) => {
3222
+ agentUsed = true;
1460
3223
  const { output: out, adapter: usedAdapter, metadata } = await this.invokeAgent(agent, input, stream, job.id, options.onToken);
1461
3224
  adapter = usedAdapter;
1462
3225
  if (metadata)
@@ -1476,6 +3239,7 @@ export class DocsService {
1476
3239
  workspaceId: this.workspace.workspaceId,
1477
3240
  commandName: "docs-sds-generate",
1478
3241
  jobId: job.id,
3242
+ commandRunId: commandRun.id,
1479
3243
  agentId: agent.id,
1480
3244
  modelName: agent.defaultModel,
1481
3245
  action: attempt === 0 ? "draft_sds" : "draft_sds_retry",
@@ -1486,7 +3250,8 @@ export class DocsService {
1486
3250
  provider: adapter,
1487
3251
  docdexAvailable: context.docdexAvailable,
1488
3252
  template: template.name,
1489
- attempt,
3253
+ attempt: attempt + 1,
3254
+ phase: attempt === 0 ? "draft_sds" : "draft_sds_retry",
1490
3255
  },
1491
3256
  });
1492
3257
  if (valid)
@@ -1508,6 +3273,7 @@ export class DocsService {
1508
3273
  workspaceId: this.workspace.workspaceId,
1509
3274
  commandName: "docs-sds-generate",
1510
3275
  jobId: job.id,
3276
+ commandRunId: commandRun.id,
1511
3277
  action: "draft_sds_resume",
1512
3278
  promptTokens: 0,
1513
3279
  completionTokens: estimateTokens(draft),
@@ -1531,42 +3297,60 @@ export class DocsService {
1531
3297
  workspaceId: this.workspace.workspaceId,
1532
3298
  commandName: "docs-sds-generate",
1533
3299
  jobId: job.id,
3300
+ commandRunId: commandRun.id,
1534
3301
  agentId: agent.id,
1535
3302
  modelName: agent.defaultModel,
1536
3303
  action: "draft_sds_resume_regenerate",
1537
3304
  promptTokens: estimateTokens(prompt),
1538
3305
  completionTokens: estimateTokens(agentOutput),
1539
- metadata: { adapter, provider: adapter, docdexAvailable: context.docdexAvailable, template: template.name },
3306
+ metadata: {
3307
+ adapter,
3308
+ provider: adapter,
3309
+ docdexAvailable: context.docdexAvailable,
3310
+ template: template.name,
3311
+ phase: "draft_sds_resume_regenerate",
3312
+ attempt: 1,
3313
+ },
1540
3314
  });
1541
3315
  }
1542
- // Enrich each section sequentially after a valid base draft exists.
1543
- draft = await enrichSdsDraft(draft, sdsSections, agent, context, options.projectKey, invoke);
1544
- draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
1545
- if (!skipValidation && !(validateSdsDraft(draft) && headingHasContent(draft, "Architecture"))) {
1546
- warnings.push("Enriched SDS draft failed validation; using structured fallback.");
1547
- draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
3316
+ if (fastMode) {
3317
+ warnings.push("Fast mode enabled; skipping SDS enrichment and tidy passes.");
1548
3318
  }
1549
- try {
1550
- const tidiedRaw = await tidySdsDraft(draft, sdsSections, agent, invoke);
1551
- const tidied = ensureSdsStructuredDraft(tidiedRaw, options.projectKey, context, template.content);
1552
- if (skipValidation || (validateSdsDraft(tidied) && headingHasContent(tidied, "Architecture"))) {
1553
- draft = tidied;
3319
+ else {
3320
+ // Enrich each section sequentially after a valid base draft exists.
3321
+ draft = await enrichSdsDraft(draft, sdsSections, agent, context, options.projectKey, invoke);
3322
+ draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
3323
+ if (!skipValidation && !(validateSdsDraft(draft) && headingHasContent(draft, "Architecture"))) {
3324
+ warnings.push("Enriched SDS draft failed validation; using structured fallback.");
3325
+ draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
3326
+ }
3327
+ try {
3328
+ const tidiedRaw = await tidySdsDraft(draft, sdsSections, agent, invoke);
3329
+ const tidied = ensureSdsStructuredDraft(tidiedRaw, options.projectKey, context, template.content);
3330
+ if (skipValidation || (validateSdsDraft(tidied) && headingHasContent(tidied, "Architecture"))) {
3331
+ draft = tidied;
3332
+ }
3333
+ }
3334
+ catch (error) {
3335
+ warnings.push(`SDS tidy pass skipped: ${error.message ?? "unknown error"}`);
1554
3336
  }
1555
- }
1556
- catch (error) {
1557
- warnings.push(`SDS tidy pass skipped: ${error.message ?? "unknown error"}`);
1558
3337
  }
1559
3338
  await fs.mkdir(path.dirname(draftPath), { recursive: true });
1560
3339
  await fs.writeFile(draftPath, draft, "utf8");
1561
- const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "sds", `${path.basename(outputPath, path.extname(outputPath))}-first-draft.md`);
3340
+ const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "sds", `${path.basename(runContext.outputPath, path.extname(runContext.outputPath))}-first-draft.md`);
1562
3341
  await ensureDir(firstDraftPath);
1563
3342
  await fs.writeFile(firstDraftPath, draft, "utf8");
1564
- try {
1565
- const iterativeDraft = await buildIterativeSds(options.projectKey, context, draft, sdsSections, outputPath, invoke);
1566
- draft = iterativeDraft;
3343
+ if (fastMode) {
3344
+ warnings.push("Fast mode enabled; skipping iterative SDS refinement.");
1567
3345
  }
1568
- catch (error) {
1569
- warnings.push(`Iterative SDS refinement failed; keeping first draft. ${String(error)}`);
3346
+ else {
3347
+ try {
3348
+ const iterativeDraft = await buildIterativeSds(options.projectKey, context, draft, sdsSections, iterativeOutputPath, invoke);
3349
+ draft = iterativeDraft;
3350
+ }
3351
+ catch (error) {
3352
+ warnings.push(`Iterative SDS refinement failed; keeping first draft. ${String(error)}`);
3353
+ }
1570
3354
  }
1571
3355
  await this.jobService.writeCheckpoint(job.id, {
1572
3356
  stage: "draft_completed",
@@ -1576,23 +3360,29 @@ export class DocsService {
1576
3360
  let docdexId;
1577
3361
  let segments;
1578
3362
  let mirrorStatus = "skipped";
3363
+ let reviewReportPath;
1579
3364
  if (options.dryRun) {
1580
3365
  warnings.push("Dry run enabled; SDS was not written to disk or registered in docdex.");
1581
3366
  }
1582
3367
  if (!options.dryRun) {
1583
- await this.writeSdsFile(outputPath, draft);
3368
+ await this.writeSdsFile(runContext.outputPath, draft);
1584
3369
  if (context.docdexAvailable) {
1585
- const registered = await this.registerSds(outputPath, draft, options.projectKey);
1586
- docdexId = registered.id;
1587
- segments = (registered.segments ?? []).map((s) => s.id);
1588
- await fs.writeFile(`${outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
3370
+ try {
3371
+ const registered = await this.registerSds(runContext.outputPath, draft, options.projectKey);
3372
+ docdexId = registered.id;
3373
+ segments = (registered.segments ?? []).map((s) => s.id);
3374
+ await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
3375
+ }
3376
+ catch (error) {
3377
+ warnings.push(`Docdex registration skipped: ${error.message}`);
3378
+ }
1589
3379
  }
1590
3380
  const publicDocsDir = path.join(this.workspace.workspaceRoot, "docs", "sds");
1591
3381
  const shouldMirror = this.workspace.config?.mirrorDocs !== false;
1592
3382
  if (shouldMirror) {
1593
3383
  try {
1594
3384
  await ensureDir(path.join(publicDocsDir, "placeholder"));
1595
- const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
3385
+ const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
1596
3386
  await ensureDir(mirrorPath);
1597
3387
  await fs.writeFile(mirrorPath, draft, "utf8");
1598
3388
  mirrorStatus = "mirrored";
@@ -1601,25 +3391,76 @@ export class DocsService {
1601
3391
  mirrorStatus = "failed";
1602
3392
  }
1603
3393
  }
3394
+ try {
3395
+ await this.recordDocgenStage(runContext, {
3396
+ stage: "inventory",
3397
+ message: "Building doc inventory",
3398
+ });
3399
+ runContext.artifacts = await buildDocInventory({
3400
+ workspace: this.workspace,
3401
+ preferred: { sdsPath: runContext.outputPath },
3402
+ });
3403
+ }
3404
+ catch (error) {
3405
+ runContext.warnings.push(`Doc inventory build failed: ${error.message ?? String(error)}`);
3406
+ }
3407
+ await this.recordDocgenStage(runContext, {
3408
+ stage: "blueprint",
3409
+ message: "Generating deployment blueprint",
3410
+ });
3411
+ const blueprintGenerated = await this.generateDeploymentBlueprint(runContext, draft, options.projectKey);
3412
+ if (blueprintGenerated) {
3413
+ try {
3414
+ await this.recordDocgenStage(runContext, {
3415
+ stage: "inventory",
3416
+ message: "Rebuilding doc inventory",
3417
+ });
3418
+ runContext.artifacts = await buildDocInventory({
3419
+ workspace: this.workspace,
3420
+ preferred: { sdsPath: runContext.outputPath },
3421
+ });
3422
+ }
3423
+ catch (error) {
3424
+ runContext.warnings.push(`Doc inventory rebuild after blueprint failed: ${error.message ?? String(error)}`);
3425
+ }
3426
+ }
3427
+ const iterationResult = await this.runIterationLoop(runContext);
3428
+ reviewReportPath = iterationResult.reviewReportPath;
1604
3429
  }
1605
3430
  await this.jobService.updateJobStatus(job.id, "completed", {
1606
3431
  payload: {
1607
- outputPath,
3432
+ outputPath: runContext.outputPath,
1608
3433
  docdexId,
1609
3434
  segments,
1610
3435
  template: template.name,
1611
3436
  mirrorStatus,
1612
3437
  agentMetadata,
3438
+ ...(reviewReportPath ? { reviewReportPath } : {}),
1613
3439
  },
1614
3440
  });
1615
3441
  await this.jobService.finishCommandRun(commandRun.id, "succeeded");
3442
+ if (options.rateAgents && agentUsed) {
3443
+ try {
3444
+ const ratingService = await this.ensureRatingService();
3445
+ await ratingService.rate({
3446
+ workspace: this.workspace,
3447
+ agentId: agent.id,
3448
+ commandName: "docs-sds-generate",
3449
+ jobId: job.id,
3450
+ commandRunId: commandRun.id,
3451
+ });
3452
+ }
3453
+ catch (error) {
3454
+ warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
3455
+ }
3456
+ }
1616
3457
  return {
1617
3458
  jobId: job.id,
1618
3459
  commandRunId: commandRun.id,
1619
- outputPath,
3460
+ outputPath: runContext.outputPath,
1620
3461
  draft,
1621
3462
  docdexId,
1622
- warnings,
3463
+ warnings: runContext.warnings,
1623
3464
  };
1624
3465
  }
1625
3466
  catch (error) {