@skyramp/mcp 0.4.3 → 0.4.4-rc.2

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 (168) hide show
  1. package/build/index.js +239 -210
  2. package/build/playwright/PlaywrightTraceService.d.ts +1 -1
  3. package/build/playwright/blueprintDigestStore.d.ts +1 -1
  4. package/build/playwright/registerPlaywrightTools.d.ts +1 -1
  5. package/build/playwright/traceRecordingPrompt.d.ts +1 -1
  6. package/build/playwright/traceRecordingPrompt.js +4 -3
  7. package/build/prompts/local-dev/local-dev-prompts.d.ts +1 -1
  8. package/build/prompts/local-dev/local-dev-prompts.js +2 -2
  9. package/build/prompts/orderAssertionRule.d.ts +9 -0
  10. package/build/prompts/orderAssertionRule.js +9 -0
  11. package/build/prompts/startTraceCollectionPrompts.d.ts +1 -1
  12. package/build/prompts/startTraceCollectionPrompts.js +2 -3
  13. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  14. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +1 -1
  15. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +2 -2
  16. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  17. package/build/prompts/testbot/testbot-prompts.js +9 -9
  18. package/build/recommendation/answers.d.ts +3 -3
  19. package/build/recommendation/answers.js +3 -2
  20. package/build/recommendation/verifierContracts.d.ts +4 -4
  21. package/build/recommendation/verifierContracts.js +6 -5
  22. package/build/recommendation/verifiers/coverage.js +25 -25
  23. package/build/recommendation/verifiers/deliveredMatchesPlan.js +3 -3
  24. package/build/resources/analysisResources.d.ts +1 -1
  25. package/build/resources/analysisResources.js +1 -1
  26. package/build/resources/progressResource.d.ts +1 -1
  27. package/build/resources/sutSetupResource.d.ts +1 -1
  28. package/build/resources/sutSetupResource.js +1 -1
  29. package/build/resources/testbotResource.d.ts +1 -1
  30. package/build/resources/testbotResource.js +1 -1
  31. package/build/services/AnalyticsService.d.ts +1 -1
  32. package/build/services/FleetExecutionService.d.ts +130 -0
  33. package/build/services/FleetExecutionService.js +391 -0
  34. package/build/services/ModularizationService.d.ts +1 -1
  35. package/build/services/TestExecutionService.d.ts +7 -0
  36. package/build/services/TestExecutionService.js +21 -0
  37. package/build/services/TestGenerationService.d.ts +1 -1
  38. package/build/tool-phases.js +2 -0
  39. package/build/tools/auth/loginTool.d.ts +1 -1
  40. package/build/tools/auth/loginTool.js +2 -2
  41. package/build/tools/auth/logoutTool.d.ts +1 -1
  42. package/build/tools/auth/logoutTool.js +2 -2
  43. package/build/tools/code-refactor/alignAssertionsTool.d.ts +1 -1
  44. package/build/tools/code-refactor/alignAssertionsTool.js +1 -1
  45. package/build/tools/code-refactor/assertionSkillTools.d.ts +1 -1
  46. package/build/tools/code-refactor/assertionSkillTools.js +2 -2
  47. package/build/tools/code-refactor/codeReuseTool.d.ts +1 -1
  48. package/build/tools/code-refactor/codeReuseTool.js +2 -2
  49. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +1 -1
  50. package/build/tools/code-refactor/enhanceAssertionsTool.js +1 -1
  51. package/build/tools/code-refactor/modularizationTool.d.ts +1 -1
  52. package/build/tools/code-refactor/modularizationTool.js +2 -2
  53. package/build/tools/enrichTestWithMocksTool.d.ts +1 -1
  54. package/build/tools/enrichTestWithMocksTool.js +2 -2
  55. package/build/tools/executeFleetTool.d.ts +28 -0
  56. package/build/tools/executeFleetTool.js +107 -0
  57. package/build/tools/executeSkyrampTestTool.d.ts +8 -5
  58. package/build/tools/executeSkyrampTestTool.js +13 -5
  59. package/build/tools/fixErrorTool.d.ts +1 -1
  60. package/build/tools/fixErrorTool.js +1 -1
  61. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +6 -57
  62. package/build/tools/generate-tests/batchMockGenerationTool.js +3 -3
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +9 -45
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +29 -11
  65. package/build/tools/generate-tests/generateContractRestTool.d.ts +1 -2
  66. package/build/tools/generate-tests/generateContractRestTool.js +1 -1
  67. package/build/tools/generate-tests/generateE2ERestTool.d.ts +2 -2
  68. package/build/tools/generate-tests/generateE2ERestTool.js +10 -2
  69. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +1 -1
  70. package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
  71. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +1 -2
  72. package/build/tools/generate-tests/generateIntegrationRestTool.js +1 -1
  73. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -2
  74. package/build/tools/generate-tests/generateLoadRestTool.js +2 -1
  75. package/build/tools/generate-tests/generateMockRestTool.d.ts +1 -2
  76. package/build/tools/generate-tests/generateMockRestTool.js +3 -3
  77. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +1 -1
  78. package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
  79. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +64 -0
  80. package/build/tools/generate-tests/generateUILoadRestTool.js +512 -0
  81. package/build/tools/generate-tests/generateUIRestTool.d.ts +8 -7
  82. package/build/tools/generate-tests/generateUIRestTool.js +10 -2
  83. package/build/tools/generate-tests/loadTestSchema.d.ts +15 -15
  84. package/build/tools/generate-tests/trace-reuse-guard.d.ts +1 -1
  85. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +1 -2
  86. package/build/tools/generateEnrichedIntegrationTestTool.js +1 -1
  87. package/build/tools/localDevWorkerComposeTool.d.ts +1 -1
  88. package/build/tools/localDevWorkerComposeTool.js +2 -2
  89. package/build/tools/one-click/oneClickTool.d.ts +1 -1
  90. package/build/tools/one-click/oneClickTool.js +1 -1
  91. package/build/tools/preflightMockCheckTool.d.ts +1 -1
  92. package/build/tools/preflightMockCheckTool.js +3 -3
  93. package/build/tools/queryProxyMocksTool.d.ts +1 -1
  94. package/build/tools/queryProxyMocksTool.js +3 -3
  95. package/build/tools/submitReportTool.d.ts +111 -414
  96. package/build/tools/submitReportTool.js +88 -38
  97. package/build/tools/test-management/actionsTool.d.ts +1 -1
  98. package/build/tools/test-management/actionsTool.js +7 -7
  99. package/build/tools/test-management/analyzeChangesTool.d.ts +22 -118
  100. package/build/tools/test-management/analyzeChangesTool.js +4 -4
  101. package/build/tools/test-management/analyzeTestHealthTool.d.ts +1 -1
  102. package/build/tools/test-management/analyzeTestHealthTool.js +4 -4
  103. package/build/tools/test-management/registerTestPlanTool.d.ts +107 -733
  104. package/build/tools/test-management/registerTestPlanTool.js +12 -12
  105. package/build/tools/test-management/resolveScreenTool.d.ts +1 -1
  106. package/build/tools/test-management/resolveScreenTool.js +6 -2
  107. package/build/tools/trace/startTraceCollectionTool.d.ts +1 -1
  108. package/build/tools/trace/startTraceCollectionTool.js +4 -4
  109. package/build/tools/trace/stopTraceCollectionTool.d.ts +1 -1
  110. package/build/tools/trace/stopTraceCollectionTool.js +2 -2
  111. package/build/tools/verifyTestDependenciesTool.d.ts +1 -1
  112. package/build/tools/verifyTestDependenciesTool.js +1 -1
  113. package/build/tools/workspace/initScanWorkspaceTool.d.ts +1 -1
  114. package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
  115. package/build/tools/workspace/initializeWorkspaceTool.d.ts +1 -1
  116. package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
  117. package/build/types/RepositoryAnalysis.d.ts +164 -1170
  118. package/build/types/RepositoryAnalysis.js +12 -10
  119. package/build/types/StepMethod.d.ts +24 -1
  120. package/build/types/StepMethod.js +1 -1
  121. package/build/types/TestTypes.d.ts +41 -133
  122. package/build/types/TestTypes.js +25 -3
  123. package/build/types/TestbotReport.d.ts +4 -1
  124. package/build/utils/assertion-verify/metrics.js +12 -0
  125. package/build/utils/docker.d.ts +29 -7
  126. package/build/utils/docker.js +83 -27
  127. package/build/utils/findUp.d.ts +20 -0
  128. package/build/utils/findUp.js +59 -0
  129. package/build/utils/initAgent.d.ts +3 -3
  130. package/build/utils/initAgent.js +58 -26
  131. package/build/utils/language-helper.js +1 -1
  132. package/build/utils/progress.d.ts +3 -4
  133. package/build/utils/progress.js +4 -4
  134. package/build/utils/repoSdkVersion.d.ts +16 -0
  135. package/build/utils/repoSdkVersion.js +88 -0
  136. package/build/utils/testDependencyPolicy.d.ts +20 -0
  137. package/build/utils/testDependencyPolicy.js +64 -4
  138. package/build/utils/utils-verify/typecheck.js +3 -18
  139. package/build/utils/utils.d.ts +1 -1
  140. package/build/utils/versions.d.ts +3 -3
  141. package/build/utils/versions.js +1 -1
  142. package/build/workspace/workspace.d.ts +117 -538
  143. package/build/workspace/workspace.js +2 -2
  144. package/node_modules/playwright/lib/mcp/skyramp/assertOrder.js +66 -0
  145. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +12 -3
  146. package/node_modules/playwright/lib/mcp/skyramp/common/liveReadError.js +40 -0
  147. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +21 -0
  148. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
  149. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +96 -16
  150. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +10 -0
  151. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  152. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +22 -0
  153. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/elementOrder.js +57 -0
  154. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  155. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.uiPTFncY.js → index.0MJW0uG7.js} +1 -1
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  157. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  158. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  159. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +26 -1
  160. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/elementOrder.ts +83 -0
  161. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  162. package/node_modules/playwright/package.json +1 -1
  163. package/package.json +9 -6
  164. package/plugin/prompts/plan-tests.md +9 -17
  165. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -1
  166. package/plugin/skills/enhance-assertions/reference/ui.md +2 -2
  167. package/plugin/skills/fix-test-import-errors/SKILL.md +1 -1
  168. package/plugin/skills/repair-staged-files/SKILL.md +17 -0
@@ -1,2 +1,2 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare function registerStartTraceCollectionPrompt(mcpServer: McpServer): void;
@@ -1,4 +1,3 @@
1
- // src/prompts/skyrampPrompt.ts
2
1
  import { z } from "zod";
3
2
  import { logger } from "../utils/logger.js";
4
3
  import { SESSION_STORAGE_FILENAME } from "../types/TestTypes.js";
@@ -6,12 +5,12 @@ export function registerStartTraceCollectionPrompt(mcpServer) {
6
5
  logger.info("registering start trace collection prompt");
7
6
  mcpServer.registerPrompt("skyramp_trace_prompt", {
8
7
  description: "Skyramp trace collection prompt",
9
- argsSchema: {
8
+ argsSchema: z.object({
10
9
  outputDir: z
11
10
  .string()
12
11
  .optional()
13
12
  .describe("Workspace directory where the session file will be saved. Pass the configured testDirectory from .skyramp/workspace.yml so the prompt can show the resolved save path instead of an abstract <outputDir> placeholder."),
14
- },
13
+ }),
15
14
  }, (args) => {
16
15
  const outputDir = args?.outputDir;
17
16
  const sessionPathDisplay = outputDir
@@ -55,6 +55,8 @@ ${opts.domainNotes}
55
55
 
56
56
  **When uncertain, use VERIFY not IGNORE:** If the diff touches code this test could exercise but you cannot confirm impact without reading more context, assign VERIFY — do not default to IGNORE.
57
57
 
58
+ **If your rationale concludes the test is unaffected, assign IGNORE, not VERIFY.** Assign VERIFY only for a doubt that the code you read does not settle. A rule that forbids IGNORE still applies.
59
+
58
60
  **A stated requirement wins over the code.** Before you write a new value or refresh a screenshot in an existing test, compare it with what the pull request title, the description and any requirements file they name state. If the new value breaks a stated requirement, do not write it: keep that assertion on the requirement's value and make only the other edits. If no other edit remains, mark the test VERIFY. The test stays red; do not relax it. If the requirements file is unchanged and the description presents the new behaviour as intended, the file is out of date: update the test.
59
61
 
60
62
  **Pre-commit verification — confirm before finalizing UPDATE/REGENERATE/DELETE:**
@@ -1,2 +1,2 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare function registerRecommendTestsPrompt(server: McpServer): void;
@@ -12,11 +12,11 @@ export function registerRecommendTestsPrompt(server) {
12
12
  "Parameters for generation tools must derive from the repository analysis, enrichedScenarios, " +
13
13
  "or source code you read — no invented field names, no guessed URLs. Provide a stateFile path " +
14
14
  "from skyramp_analyze_changes.",
15
- argsSchema: {
15
+ argsSchema: z.object({
16
16
  stateFile: z
17
17
  .string()
18
18
  .describe("State file path returned by skyramp_analyze_changes"),
19
- },
19
+ }),
20
20
  }, async (args) => {
21
21
  const stateFile = args.stateFile;
22
22
  if (!stateFile) {
@@ -1,4 +1,4 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { RelatedRepository, TestbotPromptOptions } from "../../types/TestbotPromptOptions.js";
3
3
  /** "After generation, complete these steps" list at the end of Task 2. Exported
4
4
  * for `referenceIntegrity.test.ts`, which compares it against the ordinals the
@@ -332,12 +332,12 @@ export function getTestbotPrompt(opts) {
332
332
  let maintenanceBeforeExecStep;
333
333
  let verifyExternalStep;
334
334
  if (planOnly) {
335
- verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Plan-only run: do not run the external tests you edited.`;
335
+ verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Plan-only run: do not run the external tests you edited or the VERIFY files.`;
336
336
  maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Plan-only run: skip the pre-edit baseline execution — the application is not running. Record every maintenance verdict from static drift analysis alone; execution statuses simply remain unrecorded.`;
337
337
  }
338
338
  else {
339
- verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Run the external tests you edited again. Do this step right after ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)}. It applies to each \`[external]\` test file that you edited in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)} and that ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)} ran. Run each such file with \`skyramp_execute_test\`, \`phase: "after"\` and \`stateFile\`, and use the same command as in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)}. Record each result as the file's \`afterStatus\`. The final execution excludes \`[external]\` files, so this step is where they get their after-run. If no file applies, say so in your report. If a file still fails, report it — do not run it again.`;
340
- maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every Skyramp test you will change, and for every test the repository owns that you will mark UPDATE: ${runTestCall()}. Run them one at a time, not in parallel. Run the command even when you expect the runner or a package to be missing: the failure output is what names what is missing, and a check you make before the run reaches no repair. On this run repair the environment only — install what is missing and run the file again — and never edit the test file, because the result of the file as the repository wrote it IS the baseline. This captures the pre-edit baseline — do not skip it even when you expect the test to fail. \`skyramp_actions\` refuses an UPDATE of the repository's own test until that test has this run. Nothing else owes one: a VERIFY changes no file, and a REGENERATE or a DELETE of the repository's own test is a recommendation you report rather than apply. Never pass \`rebaselineSnapshots\` on this run: a \`Screenshot comparison failed\` here is the evidence that a visual baseline is stale, and the refresh belongs to the final execution.`;
339
+ verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Run the external tests you edited again, and run each VERIFY file once. Do this step right after ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)}, before you plan new tests. It applies to each \`[external]\` test file that you edited in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)} and that ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)} ran, and to each file you marked VERIFY, except a test in a related repository. Run each such file with \`skyramp_execute_test\`, \`phase: "after"\` and \`stateFile\`. For an edited file, use the same command as in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)}. Record each result as the file's \`afterStatus\`. The final execution excludes \`[external]\` files and VERIFY files, so this step is where they get their after-run. If no file applies, say so in your report. If a file still fails, report it — do not run it again.`;
340
+ maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every Skyramp test you will change, and for every test the repository owns that you will mark UPDATE: ${runTestCall()}. Run them one at a time, not in parallel. Run the command even when you expect the runner or a package to be missing: the failure output is what names what is missing, and a check you make before the run reaches no repair. On this run repair the environment only — install what is missing and run the file again — and never edit the test file, because the result of the file as the repository wrote it IS the baseline. This captures the pre-edit baseline — do not skip it even when you expect the test to fail. \`skyramp_actions\` refuses an UPDATE of the repository's own test until that test has this run. Nothing else owes one: a VERIFY changes no file, so its one run comes in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, and a REGENERATE or a DELETE of the repository's own test is a recommendation you report rather than apply. Never pass \`rebaselineSnapshots\` on this run: a \`Screenshot comparison failed\` here is the evidence that a visual baseline is stale, and the refresh belongs to the final execution.`;
341
341
  }
342
342
  // For follow-up requests: emit the @skyramp-testbot header + guardrails, both stop-early.
343
343
  // For first-run prompts: emit the full Task 1 analysis + maintenance section.
@@ -477,7 +477,7 @@ ${userPrompt ? "Follow-up requests never reach this step — Task 1's guardrails
477
477
  **How to generate each type (for ADD):**
478
478
  - **Integration**: call \`skyramp_batch_scenario_test_generation\` with ALL steps in a single call (pass the \`steps\` array with method, path, requestBody, statusCode for each step). Then call \`skyramp_integration_test_generation\` with the returned scenario file${INTEGRATION_CODE_REUSE_GEN_CLAUSE}.
479
479
  **Reuse the steps you declared in your own plan** — pass them straight through. Read a source model only for a field your plan does not already carry.
480
- Scenario JSON and test files go in ${testDirInstruction}
480
+ Test files go in ${testDirInstruction} Pass that same directory as \`outputDir\` to \`skyramp_batch_scenario_test_generation\`. The tool writes the scenario JSON outside the repository, so it is not committed. Do NOT copy it into the test directory.
481
481
  **Pipeline for speed**: Call ALL \`skyramp_batch_scenario_test_generation\` calls in one batch. When they return, call ALL \`skyramp_integration_test_generation\` calls in the next batch. Do NOT serialize per-scenario (batch→integration→batch→integration) — batch ALL scenarios first, then generate ALL integration tests.
482
482
  - **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`, and \`requestData\` for POST/PUT/PATCH.
483
483
  Pass \`apiSchema\` if an OpenAPI spec exists.
@@ -536,7 +536,7 @@ ${CONTRACT_MODE_GUIDANCE}
536
536
  - **Removal guard — when \`uiContext.removedElements\` is non-empty**: the server lists which elements this PR removed — each identified by a \`data-*\` attribute and value, with the file it came from (renames, moves and deleted files are excluded). Read that file and the route or page files that render it to decide which page each appeared on. Per PAGE: \`browser_navigate\` to it, wait for the app to render, then call \`browser_assert_hidden\` once per listed element using the target the list shows — \`testId: "<value>"\` when the attribute is \`data-testid\`, otherwise \`attribute: "<name>", value: "<value>"\` (e.g. \`attribute: "data-cy"\`); no \`ref\` — the element no longer exists, so no snapshot has one; the tool checks the live page and refuses to record on a blank or un-rendered page. Then at least one \`browser_assert\` on a RETAINED element of the same page verifying its text, value or state. If a UI trace for that page is already planned, record the guard assertions inside that trace; otherwise this is its own trace and spec, \`verify-removed-ui-<page-slug>\`. The generated test asserts \`toBeHidden()\` on \`getByTestId(...)\` or \`page.locator('[data-cy="..."]')\` accordingly. A removed element the server did not list still gets its test (the removal check requires it) — ground that one in the diff and prefix its \`reasoning\` with \`[removed-element diff-grounded]\`; prefix \`reasoning\` with \`[removed-element server-listed]\` when the element came from \`uiContext.removedElements\`. When the removed element lived inside a control the page has to open — an \`option\` of a \`select\`, an item of a closed menu — open that control before asserting: a closed control's contents are never visible, so a hidden assertion recorded against a shut one passes whether the element was removed or not. \`browser_assert_hidden\` stays the tool either way, because \`browser_assert\` needs a snapshot \`ref\` and a removed element has none; where the control cannot be opened, say so in \`reasoning\` so the assertion step turns that guard into a count. **Deleted routes or pages are deleted features, not removed elements**: test the removal through the link or entry point that survives. A trace may still open the retired URL — to assert the app's fallback there, or that the old page's elements are gone — after it has recorded a positive \`browser_assert\` on a surviving page; a trace whose only visited page is the dead URL has no positive companion and is not a guard spec.
537
537
  - **Renames are not removals — on every PR, whatever \`uiContext.removedElements\` holds**: when a test id, \`aria-label\` or other accessible name changed, assert the element by its new identifier and leave the retired identifier alone (no \`browser_assert_hidden\` on it). A rename PR removes nothing, so this rule applies when the removed list is empty.
538
538
  - **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. **EXCEPTION:** when the look in question IS the change under test (a colour, class, attribute or element-order the diff makes), assert the property directly instead — see the next bullet. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
539
- - **A colour, class or attribute change the diff makes**: record it directly — \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children). If that owner has no stable ref, say so in \`reasoning\` as a recording gap — do not assert the wrong element or screenshot it. Element order has no assert kind: use \`flex-direction\` when the diff flipped it, else a \`boundingBox()\` comparison at the enhance step. \`browser_visual_snapshot\` only for a look no property expresses (image, gradient, chart).
539
+ - **A colour, class or attribute change the diff makes**: record it directly — \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children). If that owner has no stable ref, say so in \`reasoning\` as a recording gap — do not assert the wrong element or screenshot it. \`browser_visual_snapshot\` only for a look no property expresses (image, gradient, chart).
540
540
  - **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
541
541
  If \`browser_navigate\` fails (app not running / connection refused), apply skip condition (a) above: leave the planned test in your plan, say in \`businessCaseAnalysis\` why it was not written, AND record the outage in \`issuesFound\`.
542
542
  Each trace costs browser tool calls, so record the ones your plan holds and no more. One trace that asserts a real outcome is worth more than three that assert a heading.
@@ -641,9 +641,9 @@ Do not make any changes other than the code-reuse refactoring (step ${POSTGEN.UI
641
641
  ${FINISH_CHECKS_BLOCK}
642
642
 
643
643
  **Execution timing:**
644
- - **beforeStatus** (maintained tests only): execute each maintained test file **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
644
+ - **beforeStatus** (maintained tests only): execute each maintained test file you will edit **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
645
645
  - **Probe residue**: before the final execution, delete through the API the records your own probes and recordings created. Find each one by the id its create response returned, or by the exact value you typed for it during a recording. Do not delete anything else, and do not delete by a pattern. Do not use the database or a container. Do not add the deletion to a test. List in the report the records the API could not remove.
646
- - **Final execution**: Apart from ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together — passing \`stateFile\` and \`repository\` on every call: the attempt cap and the \`afterStatus\` the report reads both live in that file, and \`repository\` names the section the result lands in. Omitting \`stateFile\` does not skip the cap — the tool falls back to this run's own state file — but a wrong or missing \`repository\` fails the call. For a maintained spec whose \`skyramp_actions\` entry carried \`rebaseline_snapshots\`, pass that exact list as \`rebaselineSnapshots\` on this run and only this run. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\`.
646
+ - **Final execution**: Apart from ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together — passing \`stateFile\` and \`repository\` on every call: the attempt cap and the \`afterStatus\` the report reads both live in that file, and \`repository\` names the section the result lands in. Omitting \`stateFile\` does not skip the cap — the tool falls back to this run's own state file — but a wrong or missing \`repository\` fails the call. For a maintained spec whose \`skyramp_actions\` entry carried \`rebaseline_snapshots\`, pass that exact list as \`rebaselineSnapshots\` on this run and only this run. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\` and VERIFY files: both ran in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}.
647
647
  - Only report test results for files you actually ran.
648
648
 
649
649
  ### Post-Execution: Assertion Alignment
@@ -708,7 +708,7 @@ export function registerTestbotPrompt(server) {
708
708
  logger.info("Registering testbot prompt");
709
709
  server.registerPrompt("skyramp_testbot", {
710
710
  description: "Run Skyramp Testbot to generate test recommendations and perform test maintenance for a pull request.",
711
- argsSchema: {
711
+ argsSchema: z.object({
712
712
  prTitle: z.string().describe("Pull request title"),
713
713
  prDescription: z.string().describe("Pull request description/body"),
714
714
  repositoryPath: z
@@ -768,7 +768,7 @@ export function registerTestbotPrompt(server) {
768
768
  // like the resource does, instead of rejecting the call.
769
769
  .preprocess((v) => parseMaxFixAttemptsArg(v), z.number().int().min(1).max(10).optional())
770
770
  .describe("Per-TEST-FILE cap on skyramp_execute_test calls in the final phase: the first run plus the fix-and-rerun iterations the agent may spend on an infrastructure failure for that one file (SKYR-4460). 1..10; falls back to SKYRAMP_MAX_FIX_ATTEMPTS, then 3."),
771
- },
771
+ }),
772
772
  }, async (args) => {
773
773
  const services = await readWorkspaceServices(args.repositoryPath);
774
774
  let prompt = getTestbotPrompt({
@@ -20,8 +20,8 @@ export declare function unknownAnswerObjections(objections: Objection[], acknowl
20
20
  export declare const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
21
21
  export declare function isNonAnswerable(objectionId: unknown): boolean;
22
22
  /** The objections a sentence alone does not close either. A change with no test
23
- * needs the test, a value a page and API change refuses needs a UI test or a
24
- * maintained spec that names it (the XR02 evidence is at `surfaceReject` in
23
+ * needs the test, a value a page and API change accepts or refuses needs a UI step
24
+ * that names it (the XR02 evidence is at `surfaceCase` and `surfaceReject` in
25
25
  * `verifierContracts.ts`), and a defect with no test needs the test — a defect
26
26
  * the code already had before this pull request included. Each closes only when the
27
27
  * answer also names what stopped the test: a service that is not running, a
@@ -31,7 +31,7 @@ export declare function isNonAnswerable(objectionId: unknown): boolean;
31
31
  * Prose closed every uncovered change, including changes nothing prevented a test
32
32
  * from reaching. Runs 34423214322 and 34423217796 then lost three defects the
33
33
  * baseline reported: each was listed and closed by a sentence. */
34
- export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "coverage:surfaceReject:", "defects:untested:"];
34
+ export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "coverage:surfaceCase:", "coverage:surfaceReject:", "defects:untested:"];
35
35
  export declare function needsBlocker(objectionId: unknown): boolean;
36
36
  /** The objection raised below carries this prefix and the id it objects to. */
37
37
  export declare const REFUSED_ANSWER_PREFIX = "answers:refused:";
@@ -53,8 +53,8 @@ export function isNonAnswerable(objectionId) {
53
53
  objectionId.startsWith(NON_ANSWERABLE_PREFIX));
54
54
  }
55
55
  /** The objections a sentence alone does not close either. A change with no test
56
- * needs the test, a value a page and API change refuses needs a UI test or a
57
- * maintained spec that names it (the XR02 evidence is at `surfaceReject` in
56
+ * needs the test, a value a page and API change accepts or refuses needs a UI step
57
+ * that names it (the XR02 evidence is at `surfaceCase` and `surfaceReject` in
58
58
  * `verifierContracts.ts`), and a defect with no test needs the test — a defect
59
59
  * the code already had before this pull request included. Each closes only when the
60
60
  * answer also names what stopped the test: a service that is not running, a
@@ -66,6 +66,7 @@ export function isNonAnswerable(objectionId) {
66
66
  * baseline reported: each was listed and closed by a sentence. */
67
67
  export const BLOCKER_ONLY_PREFIXES = [
68
68
  "coverage:change:",
69
+ "coverage:surfaceCase:",
69
70
  "coverage:surfaceReject:",
70
71
  "defects:untested:",
71
72
  ];
@@ -158,7 +158,7 @@ export declare const COVERAGE_CONTRACT: {
158
158
  readonly objections: {
159
159
  readonly change: {
160
160
  readonly message: "This declared change has no test in the plan.";
161
- readonly suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.";
161
+ readonly suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.";
162
162
  };
163
163
  readonly noChanges: {
164
164
  readonly message: "The plan declares no changes, so nothing says what the pull request must make different.";
@@ -190,11 +190,11 @@ export declare const COVERAGE_CONTRACT: {
190
190
  };
191
191
  readonly surfaceCase: {
192
192
  readonly message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.";
193
- readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.";
193
+ readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
194
194
  };
195
195
  readonly surfaceReject: {
196
196
  readonly message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.";
197
- readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
197
+ readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step: try it and assert the page refuses it, or assert the page no longer offers it. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
198
198
  };
199
199
  };
200
200
  readonly suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?";
@@ -302,7 +302,7 @@ export declare const DEFECTS_CONTRACT: {
302
302
  };
303
303
  readonly untested: {
304
304
  readonly message: "This declared defect has no test in the plan.";
305
- readonly suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. If something stopped this run from writing the test, answer it and set `blocker` to what stopped it.";
305
+ readonly suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. If something stopped this run from writing the test, answer it and set `blocker` to what stopped it.";
306
306
  };
307
307
  readonly expectedPass: {
308
308
  readonly message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.";
@@ -149,7 +149,7 @@ export const COVERAGE_CONTRACT = {
149
149
  objections: {
150
150
  change: {
151
151
  message: "This declared change has no test in the plan.",
152
- suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.",
152
+ suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.",
153
153
  },
154
154
  noChanges: {
155
155
  message: "The plan declares no changes, so nothing says what the pull request must make different.",
@@ -203,17 +203,18 @@ export const COVERAGE_CONTRACT = {
203
203
  suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.",
204
204
  },
205
205
  // `{case}` is the rendered case, as in `level="Principal" (accept)`. SKYR-4529: a form offered Lead and Principal, and
206
- // only an API test sent Principal.
206
+ // only an API test sent Principal. SKYR-4554: a later XR02 run closed this with "a third red for one defect adds
207
+ // nothing", and no UI test saved Principal.
207
208
  surfaceCase: {
208
209
  message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.",
209
- suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.",
210
+ suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
210
211
  },
211
212
  // `{case}` is the rendered case, as in `level="Principal" (accept)`. The XR02 run removed Intern from a form; the page
212
213
  // objection closed by a sentence, and no generated test checked the option was
213
214
  // gone.
214
215
  surfaceReject: {
215
216
  message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.",
216
- suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
217
+ suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step: try it and assert the page refuses it, or assert the page no longer offers it. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
217
218
  },
218
219
  },
219
220
  suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?",
@@ -374,7 +375,7 @@ export const DEFECTS_CONTRACT = {
374
375
  },
375
376
  untested: {
376
377
  message: "This declared defect has no test in the plan.",
377
- suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. If something stopped this run from writing the test, answer it and set `blocker` to what stopped it.",
378
+ suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. If something stopped this run from writing the test, answer it and set `blocker` to what stopped it.",
378
379
  },
379
380
  expectedPass: {
380
381
  message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.",
@@ -744,27 +744,24 @@ const REFUSING_VERBS = new Set([
744
744
  "assert",
745
745
  ]);
746
746
  /** The text that can name a value on the page: the steps of the UI tests whose verb
747
- * is in `verbs`, and the edit note of each maintained spec, which is prose with no
748
- * verb to read. */
749
- function pageTexts(uiTests, maintained, verbs) {
750
- return [
751
- ...uiTests.flatMap((plannedTest) => plannedTest.scenario.steps
752
- .filter((step) => verbs.has(normalizeStepMethod(step.method) ?? ""))
753
- .flatMap((step) => [step.path, step.description ?? ""])),
754
- ...maintained.map((entry) => entry.differsBy),
755
- ];
747
+ * is in `verbs`. A maintained spec's edit note is prose with no verb to read: an
748
+ * XR02 note named Principal in "4 radios (incl. Principal) render" and Intern in
749
+ * "switches from the removed Intern radio", and neither chose or refused it. */
750
+ function pageTexts(uiTests, verbs) {
751
+ return uiTests.flatMap((plannedTest) => plannedTest.scenario.steps
752
+ .filter((step) => verbs.has(normalizeStepMethod(step.method) ?? ""))
753
+ .flatMap((step) => [step.path, step.description ?? ""]));
756
754
  }
757
755
  /** A change on the page AND the API needs each accepted value chosen on the page,
758
756
  * and each refused value tried or shown absent there, not only sent by an API
759
757
  * test. `surfaceObjections` passes the page once any UI test cites the change,
760
758
  * whatever value it picks. A UI step is free text (`Senior radio`), so the check
761
759
  * reads whether a page step of a citing ui or e2e test that chooses the value (or,
762
- * for a refused one, asserts it), or the `differsBy` of a citing maintained spec,
763
- * names the value.
760
+ * for a refused one, asserts it) names the value.
764
761
  *
765
- * A refused value takes a test, a resolving maintained spec, or a blocker, never a
766
- * sentence: in the XR02 run the page objection for a removed option closed by
767
- * prose, and only an existing spec checked that the option was gone. */
762
+ * Either value takes a UI test step or a blocker, never a
763
+ * sentence: in XR02 runs the page objection for a removed option and for an added
764
+ * one each closed by prose, and no generated test checked that value on the page. */
768
765
  function surfaceCaseObjections(registration, ctx) {
769
766
  const objections = [];
770
767
  const seen = new Set();
@@ -777,15 +774,15 @@ function surfaceCaseObjections(registration, ctx) {
777
774
  const uiTests = registration.plannedTests.filter((plannedTest) => citesChange(plannedTest, id) &&
778
775
  pageTypes.has(plannedTest.scenario.testType.toLowerCase()));
779
776
  // A maintained spec counts only when its file resolves, the bar
780
- // `citedBehaviors` sets: the refused half closes by no sentence, so a note on a
781
- // file nobody can open must not close it either.
777
+ // `citedBehaviors` sets. It decides whether the values are checked, never
778
+ // whether one is met.
782
779
  const maintained = (registration.maintains ?? []).filter((entry) => entry.changes.some((cited) => cited.trim() === id) &&
783
780
  maintainedFileIsInItsRepository(entry, ctx));
784
781
  const citedAtAll = maintained.length > 0 ||
785
782
  registration.plannedTests.some((plannedTest) => citesChange(plannedTest, id));
786
783
  const texts = {
787
- surfaceCase: pageTexts(uiTests, maintained, CHOOSING_VERBS),
788
- surfaceReject: pageTexts(uiTests, maintained, REFUSING_VERBS),
784
+ surfaceCase: pageTexts(uiTests, CHOOSING_VERBS),
785
+ surfaceReject: pageTexts(uiTests, REFUSING_VERBS),
789
786
  };
790
787
  for (const testCase of change.testCases ?? []) {
791
788
  const kind = pageObjectionKind(testCase);
@@ -817,19 +814,22 @@ function surfaceCaseObjections(registration, ctx) {
817
814
  if (seen.has(key))
818
815
  continue;
819
816
  seen.add(key);
820
- const citers = [
821
- ...uiTests.map((plannedTest) => plannedTest.plannedTestId),
822
- ...maintained.map((entry) => entry.file),
823
- ];
817
+ const citers = uiTests.map((plannedTest) => plannedTest.plannedTestId);
818
+ // A maintained spec has no steps to name the value, so say its note is not
819
+ // read rather than fault it for a step it cannot carry.
820
+ const notes = maintained.length > 0
821
+ ? ` The edit note of ${maintained.map((entry) => entry.file).join(", ")} is not read.`
822
+ : "";
824
823
  objections.push({
825
824
  objectionId,
826
825
  verifier: "coverage",
827
826
  message: fillPlaceholders(COVERAGE_CONTRACT.objections[kind].message, {
828
827
  case: rendered,
829
828
  }),
830
- evidence: citers.length === 0
831
- ? `No ui or e2e test or maintained spec cites ${id}.`
832
- : `${citers.join(", ")} cite ${id}; no step or maintained edit of theirs names ${word}.`,
829
+ evidence: (citers.length === 0
830
+ ? `No ui or e2e test cites ${id}.`
831
+ : `${citers.join(", ")} cite ${id}; no page step of theirs names ${word}.`) +
832
+ notes,
833
833
  suggestion: COVERAGE_CONTRACT.objections[kind].suggestion,
834
834
  });
835
835
  }
@@ -81,9 +81,9 @@ export function checkMaintenanceDelivered(plan, maintained) {
81
81
  objections.push({
82
82
  objectionId,
83
83
  verifier: "deliveredMatchesPlan",
84
- message: "The plan says this existing test covers a change, and no maintenance row says it was edited.",
85
- evidence: `plan maintains ${file}; the maintenance rows that changed a file name ${editedPaths.length > 0 ? editedPaths.join(", ") : "no file"}`,
86
- suggestion: "The maintenance rows come from the triage, not from this call, so this is not a field to fill in here: make the edit on the file you claimed and let the maintenance step record it, or drop the entry from `maintains` and register the plan again — coverage counted the change because the plan said you were editing that test. If the test already covered the change and needed no edit, or the edit landed somewhere the rows do not name, say which as the answer.",
84
+ message: "The plan says this existing test covers a change, and no maintenance row says it was edited or run unchanged after the edits.",
85
+ evidence: `plan maintains ${file}; the maintenance rows that edited or ran a file name ${editedPaths.length > 0 ? editedPaths.join(", ") : "no file"}`,
86
+ suggestion: 'The maintenance rows come from the triage, not from this call, so this is not a field to fill in here: make the edit on the file you claimed and let the maintenance step record it, run a file you marked VERIFY with `skyramp_execute_test` and `phase: "after"`, or drop the entry from `maintains` and register the plan again — coverage counted the change because the plan said you were editing that test. If the test already covered the change and needed no edit, or the edit landed somewhere the rows do not name, say which as the answer.',
87
87
  });
88
88
  }
89
89
  return objections;
@@ -1,4 +1,4 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare const ANALYSIS_URI_PREFIX = "skyramp://analysis";
3
3
  /**
4
4
  * Register MCP Resources for analysis data access.
@@ -1,5 +1,5 @@
1
1
  import * as fs from "fs";
2
- import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
3
3
  import { StateManager, getSessionFilePath, getRegisteredSessions, hasSessionData, getSessionData, normalizeRecommendationState, } from "../utils/AnalysisStateManager.js";
4
4
  import { logger } from "../utils/logger.js";
5
5
  import { AnalysisScope } from "../types/RepositoryAnalysis.js";
@@ -1,2 +1,2 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare function registerProgressResource(server: McpServer): void;
@@ -1,2 +1,2 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare function registerSutSetupResource(server: McpServer): void;
@@ -1,4 +1,4 @@
1
- import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { resourceText } from "../utils/utils.js";
4
4
  import { AnalyticsService } from "../services/AnalyticsService.js";
@@ -1,2 +1,2 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare function registerTestbotResource(server: McpServer): void;
@@ -1,4 +1,4 @@
1
- import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { AnalyticsService } from "../services/AnalyticsService.js";
4
4
  import { getTestbotPrompt, parseRelatedRepositories, } from "../prompts/testbot/testbot-prompts.js";
@@ -1,4 +1,4 @@
1
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
1
+ import { CallToolResult } from "@modelcontextprotocol/server";
2
2
  export declare class AnalyticsService {
3
3
  /**
4
4
  * Only the fields named here reach the analytics backend. The tool's own
@@ -0,0 +1,130 @@
1
+ import Docker from "dockerode";
2
+ /**
3
+ * Where the mounted pieces live inside the executor, per container platform.
4
+ *
5
+ * The two images are the same shape on purpose -- /app/classes, /app/lib, the work
6
+ * directory -- so the only real differences are the path separator, the classpath
7
+ * separator and the shell. Keeping them in one table is what stops those three leaking
8
+ * into the rest of the file.
9
+ */
10
+ export type ContainerPlatform = "linux" | "windows";
11
+ interface Layout {
12
+ work: string;
13
+ certs: string;
14
+ /** The directory a branch-built library jar is mounted from. */
15
+ overrideDir: string;
16
+ /** The directory a credential pool outside the work directory is mounted from. */
17
+ poolDir: string;
18
+ imageClasses: string;
19
+ imageLibDir: string;
20
+ classpathSep: string;
21
+ /** Path separator inside the container. */
22
+ sep: string;
23
+ }
24
+ /**
25
+ * A path inside the container's work directory.
26
+ *
27
+ * Everything the run reads or writes lives under the mount, so this is the only place a
28
+ * container path is spelled out -- which is the point of the table above. Writing them
29
+ * longhand per platform put the separator back in the call sites it was meant to keep it
30
+ * out of.
31
+ */
32
+ export declare function workPath(l: Layout, ...parts: string[]): string;
33
+ export declare const LAYOUT: Record<ContainerPlatform, Layout>;
34
+ /**
35
+ * Which platform the executor runs as. The Docker daemon that will run the container
36
+ * decides, not the image name: a released executor is one tag carrying both operating
37
+ * systems, and a daemon pulls and starts the image for its own.
38
+ *
39
+ * Asked of `docker`, the client that creates the container. The image check and pull go
40
+ * through the default client (docker.ts), which is the same daemon for every caller today:
41
+ * skyramp_execute_fleet passes no client. A daemon whose OS cannot be read fails the run
42
+ * here with that error as the cause: guessing linux on a Windows daemon would fail later
43
+ * with a mount or pull error that says nothing about why. An explicit platform must match
44
+ * the daemon; a mismatch would pull or start an image the engine cannot run.
45
+ */
46
+ export declare function resolvePlatform(docker: Docker, explicit?: ContainerPlatform): Promise<ContainerPlatform>;
47
+ export interface FleetExecutionOptions {
48
+ directory: string;
49
+ executorImage?: string;
50
+ platform?: ContainerPlatform;
51
+ entryClass?: string;
52
+ certsDir: string;
53
+ credentials?: string;
54
+ libraryJar?: string;
55
+ timeoutSec?: number;
56
+ env?: Record<string, string>;
57
+ }
58
+ export interface FleetExecutionResult {
59
+ passed: boolean;
60
+ executorImage: string;
61
+ platform: ContainerPlatform;
62
+ entryClass: string;
63
+ exitCode: number | null;
64
+ report: string;
65
+ output: string;
66
+ result?: unknown;
67
+ timedOut?: boolean;
68
+ }
69
+ /** A simple (unqualified) Java class name. */
70
+ export declare const JAVA_CLASS_NAME: RegExp;
71
+ /**
72
+ * The generated fleet manager: the one class in the directory that JUnit runs. The journey
73
+ * and the load class travel with it because they sit beside it, and they are compiled but
74
+ * never selected -- the journey is an input the fleet ships to the worker, and the load
75
+ * class only supplies the scenario.
76
+ */
77
+ export declare function findEntryClass(directory: string, explicit?: string): string;
78
+ /**
79
+ * A fleet file that pins its worker image is refused.
80
+ *
81
+ * The library picks the image from the commit it was built from, and that pairing is what a
82
+ * run through this tool exercises. A pinned image proves the image is good and hides the
83
+ * case where the library asks for one that does not exist.
84
+ */
85
+ export declare function assertNoPinnedImage(fleetFile: string): void;
86
+ /**
87
+ * The fleet reaches the worker it deployed at an address derived from the Docker daemon:
88
+ * the daemon's host for a remote engine, `localhost` for a local socket. Inside this
89
+ * container `localhost` is the container, so a local-socket fleet cannot reach the worker it
90
+ * just deployed and would hang until the timeout instead of failing.
91
+ */
92
+ export declare function assertRemoteDaemon(fleetFile: string): void;
93
+ /**
94
+ * The image the run happens in.
95
+ *
96
+ * The released executor is named by version, and a branch build of it carries no commit in
97
+ * its name the way the worker images do -- `RUNNER_IMAGE` defaults to a constant tag. So a
98
+ * caller testing an unreleased library either names its image here, or mounts the jar.
99
+ */
100
+ export declare function resolveExecutorImage(explicit?: string): string;
101
+ /** What the container is told to run: the argv, and the script it ends with. */
102
+ export interface ContainerCommand {
103
+ /** Passed to Docker as `Cmd`. */
104
+ argv: string[];
105
+ /** The shell script argv ends with -- named, because its index differs per platform. */
106
+ script: string;
107
+ }
108
+ /** javac, then the JUnit console runner, both against the image's classpath. */
109
+ export declare function buildCommand(entryClass: string, overrideJar: string | undefined, platform?: ContainerPlatform): ContainerCommand;
110
+ /** The lines the fleet printed, which is what a reader wants out of the JUnit noise. */
111
+ export declare function extractReport(output: string): string;
112
+ /**
113
+ * Where a path inside the container lands on the host.
114
+ *
115
+ * The work directory is a bind mount of `directory`, so anything written under it appears
116
+ * there. Deriving the host path from the container path keeps the two ends of
117
+ * SKYRAMP_FLEET_REPORT_JSON as one fact: the value handed to the JVM is the value read
118
+ * back, rather than a second hardcoded guess that happens to agree.
119
+ *
120
+ * Returns undefined when the path is not under the work directory -- a caller may point
121
+ * the variable anywhere, and nothing outside the mount is visible from here.
122
+ */
123
+ export declare function hostPathForContainerPath(directory: string, workDir: string, containerPath: string): string | undefined;
124
+ /**
125
+ * Compiles and runs a generated fleet trio in the executor image, and returns what the run
126
+ * reported. The container is the same one every other generated test runs in; only the
127
+ * command and the mounts differ.
128
+ */
129
+ export declare function executeFleet(options: FleetExecutionOptions, docker?: Docker): Promise<FleetExecutionResult>;
130
+ export {};