@skyramp/mcp 0.2.6 → 0.2.8-rc.1

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 (248) hide show
  1. package/build/commands/commandLibrary.d.ts +12 -0
  2. package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
  3. package/build/commands/testThisEndpointCommand.d.ts +2 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +6 -4
  6. package/build/playwright/PlaywrightTraceService.d.ts +32 -0
  7. package/build/playwright/index.d.ts +3 -0
  8. package/build/playwright/registerPlaywrightTools.d.ts +8 -0
  9. package/build/playwright/registerPlaywrightTools.js +12 -11
  10. package/build/playwright/traceRecordingPrompt.d.ts +13 -0
  11. package/build/playwright/traceRecordingPrompt.js +3 -11
  12. package/build/prompts/code-reuse.d.ts +1 -0
  13. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
  14. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
  16. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
  17. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  18. package/build/prompts/fix-error-prompt.d.ts +1 -0
  19. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
  20. package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
  21. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  22. package/build/prompts/personas.d.ts +21 -0
  23. package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
  24. package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
  25. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  26. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  27. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  28. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  29. package/build/prompts/sut-setup/shared.d.ts +39 -0
  30. package/build/prompts/sut-setup/shared.js +132 -0
  31. package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
  32. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
  33. package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
  34. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  35. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
  36. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  37. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
  38. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
  39. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
  40. package/build/prompts/test-recommendation/diffExecutionPlan.js +83 -18
  41. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
  42. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
  43. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
  44. package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
  45. package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
  46. package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
  47. package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
  48. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
  49. package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
  50. package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
  51. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
  52. package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
  53. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
  54. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
  55. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
  56. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +97 -99
  57. package/build/prompts/testbot/testbot-prompts.d.ts +26 -0
  58. package/build/prompts/testbot/testbot-prompts.js +301 -46
  59. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  60. package/build/prompts/testbot/testbot-prompts.test.js +109 -117
  61. package/build/resources/analysisResources.d.ts +17 -0
  62. package/build/resources/progressResource.d.ts +2 -0
  63. package/build/resources/sutSetupResource.d.ts +2 -0
  64. package/build/resources/sutSetupResource.js +45 -0
  65. package/build/resources/testbotResource.d.ts +2 -0
  66. package/build/resources/testbotResource.js +2 -2
  67. package/build/services/AnalyticsService.d.ts +13 -0
  68. package/build/services/AnalyticsService.test.d.ts +1 -0
  69. package/build/services/AnalyticsService.test.js +86 -0
  70. package/build/services/ModularizationService.d.ts +11 -0
  71. package/build/services/ScenarioGenerationService.d.ts +37 -0
  72. package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
  73. package/build/services/ScenarioGenerationService.integration.test.js +4 -0
  74. package/build/services/ScenarioGenerationService.test.d.ts +1 -0
  75. package/build/services/TestDiscoveryService.d.ts +128 -0
  76. package/build/services/TestDiscoveryService.js +22 -12
  77. package/build/services/TestDiscoveryService.test.d.ts +1 -0
  78. package/build/services/TestDiscoveryService.test.js +4 -3
  79. package/build/services/TestExecutionService.d.ts +55 -0
  80. package/build/services/TestExecutionService.js +158 -26
  81. package/build/services/TestExecutionService.test.d.ts +1 -0
  82. package/build/services/TestExecutionService.test.js +339 -25
  83. package/build/services/TestGenerationService.d.ts +56 -0
  84. package/build/services/TestGenerationService.test.d.ts +1 -0
  85. package/build/services/TestGenerationService.test.js +10 -8
  86. package/build/services/containerEnv.d.ts +14 -0
  87. package/build/tool-phase-coverage.test.d.ts +1 -0
  88. package/build/tool-phase-coverage.test.js +2 -0
  89. package/build/tool-phases.d.ts +38 -0
  90. package/build/tools/auth/loginTool.d.ts +2 -0
  91. package/build/tools/auth/logoutTool.d.ts +2 -0
  92. package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
  93. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
  94. package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
  95. package/build/tools/executeSkyrampTestTool.d.ts +2 -0
  96. package/build/tools/executeSkyrampTestTool.js +26 -7
  97. package/build/tools/fixErrorTool.d.ts +2 -0
  98. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
  99. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  100. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
  101. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
  102. package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
  103. package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
  104. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
  105. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
  106. package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
  107. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
  108. package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
  109. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
  110. package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
  111. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  112. package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
  113. package/build/tools/one-click/oneClickTool.d.ts +11 -0
  114. package/build/tools/submitReportTool.d.ts +394 -0
  115. package/build/tools/submitReportTool.js +250 -41
  116. package/build/tools/submitReportTool.test.d.ts +1 -0
  117. package/build/tools/submitReportTool.test.js +511 -55
  118. package/build/tools/test-management/actionsTool.d.ts +10 -0
  119. package/build/tools/test-management/actionsTool.js +21 -7
  120. package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
  121. package/build/tools/test-management/analyzeChangesTool.js +150 -48
  122. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  123. package/build/tools/test-management/analyzeChangesTool.test.js +95 -78
  124. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  125. package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
  126. package/build/tools/test-management/index.d.ts +3 -0
  127. package/build/tools/test-management/index.js +0 -1
  128. package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
  129. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
  130. package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
  131. package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
  132. package/build/tools/trace/sessionState.d.ts +2 -0
  133. package/build/tools/trace/sessionState.test.d.ts +1 -0
  134. package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
  135. package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
  136. package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
  137. package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
  138. package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
  139. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
  140. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  141. package/build/tools/workspace/serviceUpsert.d.ts +12 -0
  142. package/build/tools/workspace/serviceUpsert.js +23 -0
  143. package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
  144. package/build/tools/workspace/serviceUpsert.test.js +50 -0
  145. package/build/types/OneClickCommands.d.ts +68 -0
  146. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  147. package/build/types/TestAnalysis.d.ts +97 -0
  148. package/build/types/TestExecution.d.ts +63 -0
  149. package/build/types/TestExecution.js +11 -1
  150. package/build/types/TestRecommendation.d.ts +12 -0
  151. package/build/types/TestTypes.d.ts +234 -0
  152. package/build/types/TestbotReport.d.ts +68 -0
  153. package/build/types/TestbotReport.js +1 -0
  154. package/build/types/index.d.ts +4 -0
  155. package/build/types/index.js +3 -0
  156. package/build/utils/AnalysisStateManager.d.ts +242 -0
  157. package/build/utils/AnalysisStateManager.js +144 -3
  158. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  159. package/build/utils/AnalysisStateManager.test.js +98 -0
  160. package/build/utils/analyze-openapi.d.ts +1 -0
  161. package/build/utils/branchDiff.d.ts +18 -0
  162. package/build/utils/dartRouteExtractor.d.ts +45 -0
  163. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  164. package/build/utils/docker.d.ts +19 -0
  165. package/build/utils/docker.test.d.ts +1 -0
  166. package/build/utils/docker.test.js +10 -9
  167. package/build/utils/featureFlags.d.ts +37 -0
  168. package/build/utils/featureFlags.test.d.ts +1 -0
  169. package/build/utils/gitStaging.d.ts +24 -0
  170. package/build/utils/gitStaging.test.d.ts +1 -0
  171. package/build/utils/gitStaging.test.js +13 -7
  172. package/build/utils/httpDefaults.d.ts +10 -0
  173. package/build/utils/httpDefaults.test.d.ts +1 -0
  174. package/build/utils/initAgent.d.ts +36 -0
  175. package/build/utils/language-helper.d.ts +6 -0
  176. package/build/utils/logger.d.ts +11 -0
  177. package/build/utils/normalizeParams.d.ts +14 -0
  178. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  179. package/build/utils/pr-comment-parser.d.ts +31 -0
  180. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  181. package/build/utils/pr-comment-parser.test.js +6 -5
  182. package/build/utils/projectMetadata.d.ts +15 -0
  183. package/build/utils/projectMetadata.test.d.ts +1 -0
  184. package/build/utils/proxy-terminal.d.ts +2 -0
  185. package/build/utils/repoScanner.d.ts +27 -0
  186. package/build/utils/repoScanner.test.d.ts +1 -0
  187. package/build/utils/routeParsers.d.ts +53 -0
  188. package/build/utils/routeParsers.js +31 -38
  189. package/build/utils/routeParsers.test.d.ts +1 -0
  190. package/build/utils/scenarioDrafting.d.ts +71 -0
  191. package/build/utils/scenarioDrafting.js +23 -1
  192. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  193. package/build/utils/skyrampMdContent.d.ts +5 -0
  194. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  195. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  196. package/build/utils/telemetry.d.ts +32 -0
  197. package/build/utils/telemetry.test.d.ts +1 -0
  198. package/build/utils/trace-parser.d.ts +32 -0
  199. package/build/utils/trace-parser.test.d.ts +6 -0
  200. package/build/utils/uiPageEnumerator.d.ts +146 -0
  201. package/build/utils/uiPageEnumerator.js +58 -14
  202. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  203. package/build/utils/uiPageEnumerator.test.js +223 -43
  204. package/build/utils/utils.d.ts +45 -0
  205. package/build/utils/utils.js +10 -0
  206. package/build/utils/utils.test.d.ts +1 -0
  207. package/build/utils/versions.d.ts +3 -0
  208. package/build/utils/versions.js +1 -1
  209. package/build/utils/workspaceAuth.d.ts +127 -0
  210. package/build/utils/workspaceAuth.test.d.ts +1 -0
  211. package/build/utils/workspaceAuth.test.js +25 -25
  212. package/build/workspace/index.d.ts +1 -0
  213. package/build/workspace/workspace.d.ts +390 -0
  214. package/build/workspace/workspace.js +7 -7
  215. package/build/workspace/workspace.test.d.ts +1 -0
  216. package/build/workspace/workspace.test.js +6 -6
  217. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
  218. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
  219. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  220. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  221. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  222. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
  223. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
  224. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
  225. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
  226. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
  227. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  228. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
  229. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
  230. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
  231. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  232. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  233. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
  234. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  235. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  236. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
  237. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
  238. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
  239. package/node_modules/playwright/package.json +1 -1
  240. package/package.json +16 -4
  241. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  242. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
  243. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  244. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  245. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  246. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  247. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
  248. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -1,70 +1,101 @@
1
- // Mock all heavy dependencies so the module can be loaded in isolation
2
- jest.mock("@skyramp/skyramp", () => ({}));
3
- jest.mock("simple-git", () => ({ simpleGit: jest.fn() }));
4
- jest.mock("../../services/AnalyticsService.js", () => ({
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest } from "@jest/globals";
3
+ import { z } from "zod";
4
+ // Mock all heavy dependencies so the module can be loaded in isolation.
5
+ // Under native ESM, jest.mock is not hoisted — every mock the subject's import
6
+ // graph touches must use unstable_mockModule before the dynamic import below.
7
+ jest.unstable_mockModule("simple-git", () => ({ simpleGit: jest.fn() }));
8
+ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
5
9
  AnalyticsService: { pushMCPToolEvent: jest.fn() },
6
10
  }));
7
- jest.mock("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
11
+ jest.unstable_mockModule("../../prompts/test-recommendation/test-recommendation-prompt.js", () => ({
8
12
  buildRecommendationPrompt: jest.fn(),
9
13
  }));
10
- jest.mock("../../prompts/test-recommendation/recommendationSections.js", () => ({
14
+ jest.unstable_mockModule("../../prompts/test-recommendation/recommendationSections.js", () => ({
11
15
  MAX_RECOMMENDATIONS: 10,
12
16
  MAX_TESTS_TO_GENERATE: 3,
13
17
  }));
14
- jest.mock("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
18
+ jest.unstable_mockModule("../../prompts/test-recommendation/analysisOutputPrompt.js", () => ({
15
19
  buildAnalysisOutputText: jest.fn(),
16
20
  }));
17
- jest.mock("../../services/TestDiscoveryService.js", () => ({
21
+ jest.unstable_mockModule("../../services/TestDiscoveryService.js", () => ({
18
22
  TestDiscoveryService: jest.fn(),
19
23
  }));
20
- jest.mock("../../utils/branchDiff.js", () => ({
24
+ jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
21
25
  computeBranchDiff: jest.fn(),
22
26
  }));
23
- jest.mock("../../utils/routeParsers.js", () => ({
27
+ jest.unstable_mockModule("../../utils/routeParsers.js", () => ({
24
28
  classifyEndpointsByChangedFiles: jest.fn(),
25
29
  extractResourceFromPath: jest.fn(),
26
30
  parseFileEndpoints: jest.fn(),
27
31
  }));
28
- jest.mock("../../utils/repoScanner.js", () => ({
32
+ jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
29
33
  scanAllRepoEndpoints: jest.fn(),
30
34
  scanRelatedEndpoints: jest.fn(),
31
35
  grepRouterMountingContext: jest.fn(),
32
36
  findCandidateRouteFiles: jest.fn(),
33
37
  }));
34
- jest.mock("../../utils/projectMetadata.js", () => ({
38
+ jest.unstable_mockModule("../../utils/projectMetadata.js", () => ({
35
39
  detectProjectMetadata: jest.fn(),
36
40
  }));
37
- jest.mock("../../utils/scenarioDrafting.js", () => ({
41
+ jest.unstable_mockModule("../../utils/scenarioDrafting.js", () => ({
38
42
  draftScenariosFromEndpoints: jest.fn(),
39
43
  }));
40
- jest.mock("../../utils/trace-parser.js", () => ({
44
+ jest.unstable_mockModule("../../utils/trace-parser.js", () => ({
41
45
  parseTraceFile: jest.fn(),
42
46
  discoverTraceFiles: jest.fn(),
43
47
  discoverPlaywrightZips: jest.fn(),
44
48
  }));
45
- jest.mock("../../utils/pr-comment-parser.js", () => ({
49
+ jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
46
50
  parsePRComments: jest.fn(),
47
51
  }));
48
- jest.mock("../../utils/AnalysisStateManager.js", () => ({
49
- StateManager: jest.fn().mockImplementation(() => ({
52
+ jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => {
53
+ const makeInstance = () => ({
50
54
  writeData: jest.fn(),
51
- })),
52
- registerSession: jest.fn(),
53
- storeSessionData: jest.fn(),
54
- }));
55
- jest.mock("../../utils/workspaceAuth.js", () => ({
55
+ writeRepoData: jest.fn(),
56
+ getStatePath: jest.fn().mockReturnValue("/tmp/skyramp/analysis-state.json"),
57
+ });
58
+ const StateManager = jest.fn().mockImplementation(makeInstance);
59
+ StateManager.fromStatePath = jest.fn().mockImplementation(makeInstance);
60
+ return {
61
+ StateManager,
62
+ registerSession: jest.fn(),
63
+ storeSessionData: jest.fn(),
64
+ setTestsRepoDir: jest.fn(),
65
+ getActiveRunStatePath: jest.fn(),
66
+ setActiveRunStatePath: jest.fn(),
67
+ clearActiveRunStatePath: jest.fn(),
68
+ };
69
+ });
70
+ // Keep every real workspaceAuth export the subject's import graph needs
71
+ // (readWorkspaceConfigRaw, getDefaultAuthHeader, WorkspaceAuthType, …) and
72
+ // override only parseWorkspaceAuthType. requireActual can't load ESM
73
+ // synchronously, so import the real module first (mock is not hoisted).
74
+ const realWorkspaceAuth = await import("../../utils/workspaceAuth.js");
75
+ jest.unstable_mockModule("../../utils/workspaceAuth.js", () => ({
76
+ ...realWorkspaceAuth,
56
77
  parseWorkspaceAuthType: jest.fn(),
57
78
  }));
58
- jest.mock("../../utils/logger.js", () => ({
79
+ jest.unstable_mockModule("../../utils/logger.js", () => ({
59
80
  logger: { info: jest.fn(), debug: jest.fn(), error: jest.fn(), warn: jest.fn(), warning: jest.fn() },
60
81
  }));
61
- jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
82
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
62
83
  McpServer: jest.fn(),
63
84
  }));
64
- jest.mock("@modelcontextprotocol/sdk/types.js", () => ({}));
65
- jest.mock("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
66
- import { z } from "zod";
67
- import { analyzeChangesInputSchema, isSecurityRelevantDiff } from "./analyzeChangesTool.js";
85
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/types.js", () => ({}));
86
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/shared/protocol.js", () => ({}));
87
+ // All subject + mocked-module imports must be dynamic and live AFTER the
88
+ // unstable_mockModule calls above static imports hoist above the mocks under
89
+ // native ESM, loading the real (heavy) modules unmocked.
90
+ const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
91
+ const { computeBranchDiff } = await import("../../utils/branchDiff.js");
92
+ const { classifyEndpointsByChangedFiles } = await import("../../utils/routeParsers.js");
93
+ const { scanRelatedEndpoints, scanAllRepoEndpoints } = await import("../../utils/repoScanner.js");
94
+ const { StateManager } = await import("../../utils/AnalysisStateManager.js");
95
+ const os = await import("os");
96
+ const path = await import("path");
97
+ const fsSync = await import("fs");
98
+ const fsModule = fsSync;
68
99
  const schema = z.object(analyzeChangesInputSchema);
69
100
  describe("isSecurityRelevantDiff", () => {
70
101
  it("matches auth-specific middleware and admin-key signals", () => {
@@ -78,68 +109,35 @@ describe("isSecurityRelevantDiff", () => {
78
109
  expect(isSecurityRelevantDiff("move compression middleware to server setup")).toBe(false);
79
110
  });
80
111
  });
81
- describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
82
- it("accepts a valid absolute path", () => {
83
- const result = schema.safeParse({
84
- repositoryPath: "/repo",
85
- stateOutputFile: "/tmp/analyze-changes-state.json",
86
- });
87
- expect(result.success).toBe(true);
88
- });
89
- it("rejects a relative path for stateOutputFile", () => {
90
- // stateOutputFile must be absolute so the caller can guarantee the file location.
91
- // Relative paths are silently ambiguous and should be rejected.
92
- const result = schema.safeParse({
93
- repositoryPath: "/repo",
94
- stateOutputFile: "relative/path/state.json",
95
- });
96
- expect(result.success).toBe(false);
97
- });
98
- it("accepts absence of stateOutputFile (optional field)", () => {
99
- const result = schema.safeParse({ repositoryPath: "/repo" });
100
- expect(result.success).toBe(true);
101
- });
102
- });
103
112
  describe("automatic old files cleanup", () => {
104
- let StateManager;
105
113
  let cleanupOldFilesSpy;
106
114
  beforeEach(() => {
107
115
  jest.clearAllMocks();
108
- StateManager = require("../../utils/AnalysisStateManager.js").StateManager;
109
116
  cleanupOldFilesSpy = jest.fn().mockResolvedValue(0);
110
117
  StateManager.cleanupOldFiles = cleanupOldFilesSpy;
111
118
  });
112
- it("calls cleanupOldFiles with default temp dir when no stateOutputFile provided", async () => {
113
- // This test verifies the cleanup call is made without stateDir when using default location
119
+ it("calls cleanupOldFiles with default temp dir", async () => {
114
120
  expect(StateManager.cleanupOldFiles).toBeDefined();
115
121
  await StateManager.cleanupOldFiles(24, undefined);
116
122
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined);
117
123
  });
118
- it("calls cleanupOldFiles with custom dir when stateOutputFile is provided", async () => {
119
- // This test verifies the cleanup call is made with the directory of stateOutputFile
124
+ it("calls cleanupOldFiles with custom dir", async () => {
120
125
  const customPath = "/custom/dir";
121
126
  await StateManager.cleanupOldFiles(24, customPath);
122
127
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, customPath);
123
128
  });
124
- it("calls cleanupOldFiles with empty stateTypes to restrict to diff files only", async () => {
125
- // analyzeTestHealthTool passes [] so state files (skyramp-analysis-*, skyramp-recommendation-*)
126
- // are never deleted — the caller still needs args.stateFile for skyramp_actions.
129
+ it("calls cleanupOldFiles with empty stateTypes", async () => {
127
130
  await StateManager.cleanupOldFiles(24, undefined, []);
128
131
  expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined, []);
129
132
  });
130
133
  it("continues execution if cleanup fails", async () => {
131
- // Cleanup failures should not crash the analyze flow
132
134
  cleanupOldFilesSpy.mockRejectedValue(new Error("permission denied"));
133
- await expect(StateManager.cleanupOldFiles(24).catch(() => {
134
- // In the real code, this is caught and logged as a warning
135
- return Promise.resolve();
136
- })).resolves.toBeUndefined();
135
+ await expect(StateManager.cleanupOldFiles(24).catch(() => Promise.resolve())).resolves.toBeUndefined();
137
136
  });
138
137
  });
139
138
  // ─────────────────────────────────────────────────────────────────────────────
140
139
  // filterEndpointsBySpec — spec filtering + merge logic (Step 4c / 4c-merge)
141
140
  // ─────────────────────────────────────────────────────────────────────────────
142
- import { filterEndpointsBySpec } from "./analyzeChangesTool.js";
143
141
  describe("filterEndpointsBySpec", () => {
144
142
  const makeEp = (path, methods = ["GET"]) => ({ path, methods, sourceFile: "" });
145
143
  it("removes scanned endpoints that are not in the spec and not diff-changed", () => {
@@ -224,10 +222,6 @@ describe("filterEndpointsBySpec", () => {
224
222
  // ─────────────────────────────────────────────────────────────────────────────
225
223
  // isGraphQLFile — unit tests
226
224
  // ─────────────────────────────────────────────────────────────────────────────
227
- import { filterUnsupportedGraphQLEndpoints, isGraphQLFile } from "./analyzeChangesTool.js";
228
- import * as os from "os";
229
- import * as path from "path";
230
- import * as fsSync from "fs";
231
225
  describe("isGraphQLFile", () => {
232
226
  const tmpDir = os.tmpdir();
233
227
  it("returns true for .graphql extension without reading content", async () => {
@@ -303,14 +297,6 @@ describe("filterUnsupportedGraphQLEndpoints", () => {
303
297
  readSpy.mockRestore();
304
298
  });
305
299
  });
306
- // ─────────────────────────────────────────────────────────────────────────────
307
- // analyzeChangesTool handler — GraphQL-only early return (handler-level)
308
- // ─────────────────────────────────────────────────────────────────────────────
309
- import { registerAnalyzeChangesTool, buildTraceFileEntry } from "./analyzeChangesTool.js";
310
- import { computeBranchDiff } from "../../utils/branchDiff.js";
311
- import { classifyEndpointsByChangedFiles } from "../../utils/routeParsers.js";
312
- import { scanRelatedEndpoints } from "../../utils/repoScanner.js";
313
- import * as fsModule from "fs";
314
300
  /** Register the tool against a minimal mock server and return the captured handler.
315
301
  * The handler takes (params, extra) — extra is pre-filled with a no-op sendNotification. */
316
302
  function captureAnalyzeHandler() {
@@ -399,7 +385,6 @@ describe("analyzeChangesTool handler — GraphQL-only early return", () => {
399
385
  readSpy.mockRestore();
400
386
  });
401
387
  it("does NOT early-return when scope is full_repo (only fires for PR diffs)", async () => {
402
- const { scanAllRepoEndpoints } = require("../../utils/repoScanner.js");
403
388
  scanAllRepoEndpoints.mockReturnValue([]);
404
389
  const handler = captureAnalyzeHandler();
405
390
  const result = await handler({ ...baseParams, scope: "full_repo" });
@@ -437,3 +422,35 @@ describe("buildTraceFileEntry", () => {
437
422
  expect(entry.userFlows).toEqual([]);
438
423
  });
439
424
  });
425
+ describe("UI blueprint-capture instructions (merged from ui_analyze_changes)", () => {
426
+ it("NO_UI_INSTRUCTIONS signals no capture is needed and references no follow-up tool", () => {
427
+ expect(NO_UI_INSTRUCTIONS).toMatch(/no blueprint capture needed/i);
428
+ // The former pre-flight tool told the agent to "proceed to
429
+ // skyramp_analyze_changes" — that's redundant now we're inside it.
430
+ expect(NO_UI_INSTRUCTIONS).not.toMatch(/skyramp_analyze_changes/);
431
+ });
432
+ it("NO_RESOLVABLE_URLS_INSTRUCTIONS falls back to source-grounded", () => {
433
+ expect(NO_RESOLVABLE_URLS_INSTRUCTIONS).toMatch(/source-grounded/i);
434
+ });
435
+ it("buildCaptureInstructions numbers each candidate URL with its provenance", () => {
436
+ const pages = [
437
+ { url: "http://localhost:3000/notifications", sourcedFrom: ["app/routes.tsx"], strategy: "framework-route-grep" },
438
+ { url: "http://localhost:3000/settings", sourcedFrom: [], strategy: "root-fallback" },
439
+ ];
440
+ const text = buildCaptureInstructions(pages);
441
+ expect(text).toContain("1. http://localhost:3000/notifications");
442
+ expect(text).toContain("2. http://localhost:3000/settings");
443
+ expect(text).toContain("sourcedFrom: app/routes.tsx");
444
+ expect(text).toContain("sourcedFrom: (none)");
445
+ expect(text).toContain("browser_navigate");
446
+ expect(text).toContain("browser_blueprint");
447
+ });
448
+ it("buildCaptureInstructions does NOT emit a login step (uiCredentials dropped)", () => {
449
+ const pages = [
450
+ { url: "http://localhost:3000/x", sourcedFrom: [], strategy: "root-fallback" },
451
+ ];
452
+ const text = buildCaptureInstructions(pages);
453
+ expect(text).not.toMatch(/log in first/i);
454
+ expect(text).not.toMatch(/ui-credentials/i);
455
+ });
456
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAnalyzeTestHealthTool(server: McpServer): void;
@@ -15,11 +15,15 @@ export function registerAnalyzeTestHealthTool(server) {
15
15
  idempotentHint: true,
16
16
  openWorldHint: false,
17
17
  },
18
- description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow. Optionally execute tests with skyramp_execute_test before calling skyramp_actions to capture beforeStatus.`,
18
+ description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow.`,
19
19
  inputSchema: {
20
20
  stateFile: z
21
21
  .string()
22
- .describe("Path to state file from skyramp_analyze_changes. Assessment results exist only in the LLM's reasoning context this tool does not write back to the stateFile."),
22
+ .describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass summaryOutputFile or testbot-result.txt here."),
23
+ repository: z
24
+ .string()
25
+ .optional()
26
+ .describe("The owner/repo whose analysis section to assess (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for the primary repo, or a related repo's owner/repo for that repo's section within the run-scoped stateFile. Omit only for a single-repo run."),
23
27
  },
24
28
  outputSchema: {
25
29
  prompt: z.string().describe("LLM drift analysis prompt and assessment instructions."),
@@ -27,17 +31,20 @@ export function registerAnalyzeTestHealthTool(server) {
27
31
  }, async (args) => {
28
32
  let errorResult;
29
33
  try {
30
- logger.info(`Analyzing test health from state file: ${args.stateFile}`);
31
- // Load UnifiedAnalysisState from state file
34
+ logger.info(`Analyzing test health from state file: ${args.stateFile}${args.repository ? ` (repository: ${args.repository})` : ""}`);
35
+ // Load UnifiedAnalysisState for the requested repo section (primary when
36
+ // repository omitted/matches the root) from the run-scoped state file.
32
37
  const stateManager = StateManager.fromStatePath(args.stateFile);
33
- const stateData = await stateManager.readData();
34
- const fullState = await stateManager.readFullState();
35
- const repositoryPath = fullState?.metadata.repositoryPath || "";
38
+ const stateData = await stateManager.readRepoData(args.repository);
39
+ const repositoryPath = (await stateManager.getRepoRepositoryPath(args.repository)) || "";
36
40
  if (!stateData) {
37
- return toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
41
+ return toolError(`State file is empty or invalid: ${args.stateFile}${args.repository ? ` (no section for repository ${args.repository})` : ""}. Call skyramp_analyze_changes first to generate a valid state file.`);
38
42
  }
39
43
  if (!repositoryPath || typeof repositoryPath !== "string") {
40
- return toolError(`repositoryPath not found in state file metadata. The state file was likely created by an older versionre-run skyramp_analyze_changes to regenerate it.`);
44
+ return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes not summaryOutputFile or testbot-result.txt. Re-run skyramp_analyze_changes to regenerate it.`);
45
+ }
46
+ if (!Array.isArray(stateData.existingTests)) {
47
+ return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt.`);
41
48
  }
42
49
  // Skyramp tests: full drift analysis + all actions permitted.
43
50
  // Relevant external tests (user-written, relevant to this PR's endpoints): drift analysis
@@ -0,0 +1,3 @@
1
+ export { registerAnalyzeChangesTool } from "./analyzeChangesTool.js";
2
+ export { registerAnalyzeTestHealthTool } from "./analyzeTestHealthTool.js";
3
+ export { registerActionsTool } from "./actionsTool.js";
@@ -1,4 +1,3 @@
1
1
  export { registerAnalyzeChangesTool } from "./analyzeChangesTool.js";
2
- export { registerUiAnalyzeChangesTool } from "./uiAnalyzeChangesTool.js";
3
2
  export { registerAnalyzeTestHealthTool } from "./analyzeTestHealthTool.js";
4
3
  export { registerActionsTool } from "./actionsTool.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolve the user-supplied `playwrightSaveStoragePath` against `outputDir`:
3
+ * - A bare filename (no separators, not absolute) is joined with `outputDir`.
4
+ * - Anything else (absolute path, or a relative path containing separators)
5
+ * is returned unchanged.
6
+ */
7
+ export declare function resolveSaveStoragePath(input: string, outputDir: string | undefined): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SKYR-3321: pick which Playwright session-storage paths to pass to the
3
+ * recorder based on a single, explicit `sessionMode` signal.
4
+ *
5
+ * The original start-trace tool had an asymmetric default: it always
6
+ * resolved a save path (so every recording overwrote the session file on
7
+ * stop) but never auto-loaded. That was the root cause of the "load →
8
+ * 31-min idle → stop → session destroyed" regression that fills SKYR-3321.
9
+ * The mode vocabulary replaced that with the three real intents — capture a
10
+ * new session, reuse an existing one, or ignore session storage entirely.
11
+ *
12
+ * SKYR-3793: `auto` must never store credentials the user did not ask for.
13
+ * It loads an existing session if one is present in `outputDir` (reuse), but
14
+ * when none exists it does NOTHING — it does not auto-capture. Storing is
15
+ * opt-in only: `sessionMode=capture` or an explicit `saveOverride`.
16
+ *
17
+ * Both `loadOverride` and `saveOverride` are plain optional strings: when
18
+ * the LLM omits them they arrive as `undefined`, which means "use the
19
+ * mode's default". No sentinel values; explicit overrides win when given.
20
+ */
21
+ export type SessionMode = "auto" | "capture" | "reuse" | "ignore";
22
+ export interface ResolvedSessionPaths {
23
+ loadPath?: string;
24
+ savePath?: string;
25
+ }
26
+ export interface ResolveSessionPathsArgs {
27
+ mode: SessionMode;
28
+ /** Raw value of `playwrightStoragePath` (LLM may omit). */
29
+ loadOverride: string | undefined;
30
+ /** Raw value of `playwrightSaveStoragePath` (LLM may omit). */
31
+ saveOverride: string | undefined;
32
+ /** Workspace directory used to resolve relative paths and locate the default session file. */
33
+ outputDir: string;
34
+ /**
35
+ * Whether `<outputDir>/<SESSION_STORAGE_FILENAME>` exists on disk. Injected
36
+ * rather than read in-function so the resolver stays pure and testable.
37
+ */
38
+ sessionExists: boolean;
39
+ }
40
+ export declare function resolveSessionPaths(args: ResolveSessionPathsArgs): ResolvedSessionPaths;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function setSavedSessionPath(p: string | undefined): void;
2
+ export declare function consumeSavedSessionPath(): string | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTraceTool(server: McpServer): void;
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTraceStopTool(server: McpServer): void;
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function issueScanToken(): string;
3
+ export declare function validateAndConsumeScanToken(token: string): boolean;
4
+ export declare function registerInitScanWorkspaceTool(server: McpServer): void;
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerInitializeWorkspaceTool(server: McpServer): void;
@@ -7,6 +7,7 @@ import { logger } from "../../utils/logger.js";
7
7
  import { AnalyticsService } from "../../services/AnalyticsService.js";
8
8
  import { SKYRAMP_IMAGE_VERSION } from "../../utils/versions.js";
9
9
  import { validateAndConsumeScanToken } from "./initScanWorkspaceTool.js";
10
+ import { upsertServicesByRepo } from "./serviceUpsert.js";
10
11
  const require = createRequire(import.meta.url);
11
12
  const MCP_VERSION = require("../../../package.json").version || "";
12
13
  function getExecutorVersion() {
@@ -16,8 +17,8 @@ const TOOL_NAME = "skyramp_init_workspace";
16
17
  // writeWorkspaceConfig() writes the YAML directly (rather than via
17
18
  // WorkspaceConfigManager.addService) so all services are written in a single
18
19
  // pass without N+1 reads. The workspace serviceSchema accepts the full authType
19
- // set (incl. cookie/session/token), so input services are validated against it
20
- // directly — no local schema extension needed.
20
+ // set (incl. cookie/session/token) and the multi-repo `repository` field, so input
21
+ // services are validated against it directly — no local schema extension needed.
21
22
  const initializeWorkspaceSchema = z.object({
22
23
  workspacePath: z
23
24
  .string()
@@ -32,6 +33,10 @@ const initializeWorkspaceSchema = z.object({
32
33
  .boolean()
33
34
  .default(false)
34
35
  .describe("Set to true to overwrite an existing workspace file. Use when the user explicitly requests it, or when recovering from a workspace validation failure (schema mismatch, unknown fields). Default is false."),
36
+ merge: z
37
+ .boolean()
38
+ .default(false)
39
+ .describe("Set to true to MERGE the provided services into an existing workspace.yml instead of overwriting it. The primary use is registering a related repository's services into one unified workspace file (each service carrying its `repository`), but it applies whenever you want to add services without discarding existing ones. Services are upserted by the composite (repository, serviceName), so same-named services in different repos coexist. Ignored on first init (no existing file). Default is false."),
35
40
  });
36
41
  /**
37
42
  * Write a YAML workspace config to disk, bypassing the library's restricted
@@ -72,13 +77,17 @@ export function registerInitializeWorkspaceTool(server) {
72
77
  try {
73
78
  const workspacePath = params.workspacePath;
74
79
  const manager = new WorkspaceConfigManager(workspacePath);
75
- // Check if already initialized
76
- if ((await manager.exists()) && !params.force) {
80
+ const alreadyExists = await manager.exists();
81
+ // Check if already initialized. In merge mode we proceed (read the
82
+ // existing file and upsert into it) instead of refusing — this is how a
83
+ // multi-repo run registers each additional repo's services into one
84
+ // unified workspace.yml without a per-repo file.
85
+ if (alreadyExists && !params.force && !params.merge) {
77
86
  return {
78
87
  content: [
79
88
  {
80
89
  type: "text",
81
- text: `Workspace already initialized at ${manager.getConfigPath()}. Set force: true to overwrite.`,
90
+ text: `Workspace already initialized at ${manager.getConfigPath()}. Set force: true to overwrite, or merge: true to add services to it.`,
82
91
  },
83
92
  ],
84
93
  isError: false,
@@ -107,24 +116,30 @@ export function registerInitializeWorkspaceTool(server) {
107
116
  isError: true,
108
117
  };
109
118
  }
110
- // Initialize workspace
111
- let config = await manager.initialize();
112
- config = await manager.updateMetadata({
113
- mcpVersion: MCP_VERSION,
114
- executorVersion: getExecutorVersion(),
115
- });
116
- // Batch all service upserts: update in memory, write once (direct YAML
117
- // write avoids N+1 reads and a mid-loop validation error).
118
- const services = config.services ?? [];
119
- for (const svc of params.services) {
120
- const idx = services.findIndex((s) => s.serviceName === svc.serviceName);
121
- if (idx >= 0) {
122
- services[idx] = svc;
123
- }
124
- else {
125
- services.push(svc);
126
- }
119
+ // Initialize (fresh) or read (merge into existing). Merge mode preserves
120
+ // the existing primary repo identity + previously-registered services;
121
+ // initialize() would reset both, dropping other repos' services.
122
+ let config;
123
+ if (params.merge && alreadyExists) {
124
+ config = await manager.read();
125
+ config = await manager.updateMetadata({
126
+ mcpVersion: MCP_VERSION,
127
+ executorVersion: getExecutorVersion(),
128
+ });
129
+ }
130
+ else {
131
+ config = await manager.initialize();
132
+ config = await manager.updateMetadata({
133
+ mcpVersion: MCP_VERSION,
134
+ executorVersion: getExecutorVersion(),
135
+ });
127
136
  }
137
+ // Batch all service upserts: update in memory, write once (direct YAML
138
+ // write avoids N+1 reads and a mid-loop validation error). Upsert is keyed
139
+ // on the composite (repo, serviceName) — see upsertServicesByRepo — so a
140
+ // same-named service from an additional repo is a distinct entry, not an
141
+ // overwrite of the primary's.
142
+ const services = upsertServicesByRepo(config.services ?? [], params.services);
128
143
  config.services = services;
129
144
  if (config.metadata) {
130
145
  config.metadata.updatedAt = new Date().toISOString();
@@ -142,6 +157,8 @@ export function registerInitializeWorkspaceTool(server) {
142
157
  lines.push(`\nServices (${config.services.length}):`);
143
158
  for (const svc of config.services) {
144
159
  const parts = [svc.serviceName];
160
+ if (svc.repository)
161
+ parts.push(svc.repository);
145
162
  if (svc.language)
146
163
  parts.push(svc.language);
147
164
  if (svc.framework)