@vitest-agent/mcp 5.2.9 → 5.3.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/mcp",
3
- "version": "5.2.9",
3
+ "version": "5.3.0",
4
4
  "private": false,
5
5
  "description": "Model Context Protocol server for vitest-agent. Exposes 30 tools for agent access to test data, TDD lifecycle, and session management.",
6
6
  "keywords": [
@@ -49,8 +49,8 @@
49
49
  "@effected/git": "^0.25.0",
50
50
  "@effected/mcp": "^0.9.0",
51
51
  "@effected/walker": "^0.19.0",
52
- "@vitest-agent/engine": "1.1.2",
53
- "@vitest-agent/sdk": "7.0.5",
52
+ "@vitest-agent/engine": "2.0.0",
53
+ "@vitest-agent/sdk": "8.0.0",
54
54
  "effect": "^4.0.6"
55
55
  },
56
56
  "peerDependencies": {
package/tools/help.js CHANGED
@@ -36,11 +36,11 @@ const HELP_TEXT = `# vitest-agent MCP Tools
36
36
 
37
37
  | Tool | Parameters | Description |
38
38
  | ---- | ---------- | ----------- |
39
- | \`test_status\` | \`project?\` | Per-project test pass/fail state |
39
+ | \`test_status\` | \`project?\` | Per-project test pass/fail state; an entry carries \`strayOutput\` when the latest run wrote straight to the terminal |
40
40
  | \`test_overview\` | \`project?\` | Test landscape with run metrics |
41
41
  | \`test_coverage\` | \`project?\` | Coverage gaps with uncovered lines |
42
42
  | \`file_coverage\` | \`filePath\`, \`project?\` | Per-file coverage with uncovered lines and related tests |
43
- | \`test_history\` | \`project\` | Flaky/persistent/recovered tests |
43
+ | \`test_history\` | \`project\`, \`testName?\`, \`modulePath?\`, \`limit?\` | Flaky/persistent/recovered tests, plus \`strayOutput\`: how many of the last 10 runs wrote straight to the terminal |
44
44
  | \`test_trends\` | \`project\`, \`limit?\` | Coverage trajectory over time |
45
45
  | \`test_errors\` | \`project\`, \`errorName?\` | Errors with diffs, stacks, each row's \`annotations[]\` (the test annotations the author recorded), and the cite-able \`testErrorId\` / \`topStackFrameId\` values needed by \`hypothesis (action: record)\` |
46
46
  | \`test\` | \`action\` (\`list\`/\`get\`/\`for_file\`/\`for_tag\`/\`annotations\`/\`artifacts\`), plus per-action params | Consolidated test inspection: list/get/for_file/for_tag/annotations/artifacts |
@@ -73,7 +73,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
73
73
 
74
74
  | Tool | Parameters | Description |
75
75
  | ---- | ---------- | ----------- |
76
- | \`run_tests\` | \`files?\`, \`project?\`, \`tags?\`, \`passWithNoTests?\`, \`timeout?\`, \`format?\` | Run vitest with optional filters. \`tags\` is a structured \`{ all?, any?, none? }\` filter that intersects with \`project\` and \`files\` via AND. When the resolved filter set matches zero test cases the result discriminator is \`no-match\` (carrying the resolved filter context) rather than \`ok\`. \`passWithNoTests\` is a per-call override of Vitest's project-level \`test.passWithNoTests\` policy |
76
+ | \`run_tests\` | \`files?\`, \`project?\`, \`tags?\`, \`passWithNoTests?\`, \`timeout?\`, \`projectRoot?\` | Run vitest with optional filters. \`tags\` is a structured \`{ all?, any?, none? }\` filter that intersects with \`project\` and \`files\` via AND. When the resolved filter set matches zero test cases the result discriminator is \`no-match\` (carrying the resolved filter context) rather than \`ok\`. \`passWithNoTests\` is a per-call override of Vitest's project-level \`test.passWithNoTests\` policy. A \`kind: "error"\` result with \`reason: "stale-install"\` means the vitest install was replaced under this long-lived server; restart the MCP server (\`/mcp\`) instead of retrying |
77
77
  | \`register_agent\` | \`sessionId\`, \`agentType\`, \`hostKind?\`, \`parentAgentId?\`, \`clientNonce?\`, \`startGitBranch?\`, \`startGitCommitSha?\`, \`startWorktreeDir?\` | Idempotent agent invocation registration |
78
78
 
79
79
  ## Diagnostics
@@ -85,7 +85,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
85
85
  | \`failure_signature_get\` | \`hash\` | Stable failure signature with recent example errors |
86
86
  | \`turn_search\` | \`sessionId?\`, \`since?\`, \`type?\`, \`limit?\` | Search turns (default limit 100) |
87
87
  | \`acceptance_metrics\` | _(none)_ | Four spec Annex A acceptance metrics |
88
- | \`triage_brief\` | \`project?\`, \`maxLines?\` | Orientation triage |
88
+ | \`triage_brief\` | \`project?\`, \`maxLines?\` | Orientation triage (recent runs, stray output, sessions, metrics) |
89
89
  | \`wrapup_prompt\` | \`sessionId?\`, \`chatId?\`, \`kind?\`, \`userPromptHint?\` | Tailored wrap-up prompt |
90
90
  | \`commit_changes\` | \`sha?\` | Commit metadata + changed files |
91
91
 
package/tools/history.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { DataReader } from "@vitest-agent/engine";
3
3
  import { Tool } from "effect/ai";
4
- import { HistoryRecord } from "@vitest-agent/sdk";
4
+ import { HistoryRecord, StrayOutput } from "@vitest-agent/sdk";
5
5
 
6
6
  // src/tools/history.ts
7
7
  const FlakyTestRow = Schema.Struct({
@@ -36,6 +36,14 @@ const RecoveredTestRow = Schema.Struct({
36
36
  identifier: "RecoveredTestRow",
37
37
  description: "A test whose latest run passed after the previous one failed."
38
38
  });
39
+ const StrayOutputHistoryRow = Schema.Struct({
40
+ runs: Schema.Finite.annotate({ description: "Recent runs considered (the newest ones, up to 10)." }),
41
+ runsWithStrayOutput: Schema.Finite.annotate({ description: "How many of those runs wrote to the terminal past Vitest's console capture." }),
42
+ latest: Schema.NullOr(StrayOutput).annotate({ description: "The most recent run's stray output (line counts, bytes, first samples); `null` when it recorded none." })
43
+ }).annotate({
44
+ identifier: "StrayOutputHistoryRow",
45
+ description: "How often recent runs of the project wrote straight to the terminal (a test or a child process with inherited stdio). A run-level signal: nothing attributes it to one test."
46
+ });
39
47
  /**
40
48
  * The `test_history` tool's success payload.
41
49
  *
@@ -47,7 +55,8 @@ const TestHistoryResult = Schema.Struct({
47
55
  history: HistoryRecord.annotate({ description: "Raw per-test history record (stored in `test_runs` joins)." }),
48
56
  flaky: Schema.Array(FlakyTestRow).annotate({ description: "Tests with mixed pass/fail outcomes recently." }),
49
57
  persistent: Schema.Array(PersistentFailureRow).annotate({ description: "Tests failing across consecutive runs." }),
50
- recovered: Schema.Array(RecoveredTestRow).annotate({ description: "Tests that just transitioned from failing to passing in the last run." })
58
+ recovered: Schema.Array(RecoveredTestRow).annotate({ description: "Tests that just transitioned from failing to passing in the last run." }),
59
+ strayOutput: StrayOutputHistoryRow
51
60
  }).annotate({
52
61
  identifier: "TestHistoryResult",
53
62
  title: "test_history result",
@@ -79,10 +88,11 @@ const handleTestHistory = (input) => Effect.gen(function* () {
79
88
  ...scopeOptions,
80
89
  ...input.limit !== void 0 && { limit: input.limit }
81
90
  };
82
- const [history, flaky, persistent] = yield* Effect.all([
91
+ const [history, flaky, persistent, strayOutput] = yield* Effect.all([
83
92
  reader.getHistory(input.project, historyOptions),
84
93
  reader.getFlaky(input.project, scopeOptions),
85
- reader.getPersistentFailures(input.project, scopeOptions)
94
+ reader.getPersistentFailures(input.project, scopeOptions),
95
+ reader.getStrayOutputHistory({ project: input.project })
86
96
  ], { concurrency: "unbounded" });
87
97
  const recovered = history.tests.filter((t) => {
88
98
  const runs = t.runs;
@@ -102,7 +112,8 @@ const handleTestHistory = (input) => Effect.gen(function* () {
102
112
  history,
103
113
  flaky,
104
114
  persistent,
105
- recovered
115
+ recovered,
116
+ strayOutput
106
117
  };
107
118
  }).pipe(Effect.orDie);
108
119
  /**
@@ -111,7 +122,7 @@ const handleTestHistory = (input) => Effect.gen(function* () {
111
122
  * @public
112
123
  */
113
124
  const testHistoryTool = Tool.make("test_history", {
114
- description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
125
+ description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[], strayOutput: how many of the last 10 runs wrote straight to the terminal). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
115
126
  parameters: TestHistoryInput,
116
127
  success: TestHistoryResult,
117
128
  dependencies: [DataReader]
@@ -4,7 +4,7 @@ import { ToolOutputSchema } from "@effected/mcp";
4
4
  import { Data, Effect, Layer, Option, Path, Result, Schema, Semaphore } from "effect";
5
5
  import { DataReader, DataStore } from "@vitest-agent/engine";
6
6
  import { Tool } from "effect/ai";
7
- import { AgentReport, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote, readStrayOutput } from "@vitest-agent/sdk";
7
+ import { AgentReport, SuggestedActionRecord, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote, readStrayOutput } from "@vitest-agent/sdk";
8
8
  import { AsyncLocalStorage } from "node:async_hooks";
9
9
  import { existsSync, mkdtempSync, rmSync } from "node:fs";
10
10
  import { stat } from "node:fs/promises";
@@ -43,15 +43,26 @@ const RunTestsOk = Schema.Struct({
43
43
  report: AgentReport.annotate({ description: "Full AgentReport including pass/fail counts and per-module errors." }),
44
44
  classifications: Schema.Record(Schema.String, Schema.String).annotate({ description: "Per-test classification labels: stable, new-failure, persistent, flaky, recovered." }),
45
45
  discoveryLastScannedAt: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "ISO timestamp of the most recent real disk scan performed by discoverProjects() in this process (issue #100). `null`/absent means discovery has not scanned disk in this process yet (e.g. a config that doesn't call AgentPlugin.discover()). A stale-looking test count is self-explaining when compared against this value." }),
46
- scopedNote: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Set (issue #160) when this call was scoped to a subset of the project's test files (files/project/tags filter supplied). Vitest enforces coverage.thresholds against the whole-project denominator regardless of how many files ran, so a scoped run's threshold check is unreliable; this note explains why no coverage verdict should be trusted for this call. `null`/absent means the run was unscoped." })
46
+ scopedNote: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Set (issue #160) when this call was scoped to a subset of the project's test files (files/project/tags filter supplied). Vitest enforces coverage.thresholds against the whole-project denominator regardless of how many files ran, so a scoped run's threshold check is unreliable; this note explains why no coverage verdict should be trusted for this call. `null`/absent means the run was unscoped." }),
47
+ suggestedActions: Schema.optionalKey(Schema.Array(SuggestedActionRecord)).annotate({ description: "Run-level suggested actions the plugin raised at config time (issue #544): one entry per coverage-config diagnostic the plugin's ConfigValidation reported, such as a coverageTargets metric with no matching coverage.thresholds floor (severity warn). The same entries print on the plugin's stderr for a human and in the rendered report's Actions trailer. Absent when the project's config validated clean or does not load the plugin." })
47
48
  }).annotate({ identifier: "RunTestsOk" });
48
49
  const RunTestsTimeout = Schema.Struct({
49
50
  kind: Schema.Literal("timeout"),
50
51
  timeoutSeconds: Schema.Number
51
52
  }).annotate({ identifier: "RunTestsTimeout" });
53
+ /**
54
+ * The machine-readable reasons a `run_tests` error result may carry. Only
55
+ * failures an agent must act on in a specific way get one; the rest stay
56
+ * message-only. `stale-install` (issue #461): the `vitest/node` entry this
57
+ * long-lived server resolved for the project no longer exists on disk
58
+ * because the dependency tree was reinstalled under it — the remedy is to
59
+ * restart the MCP server (`/mcp` in Claude Code), not to retry the call.
60
+ */
61
+ const RUN_TESTS_ERROR_REASONS = ["stale-install"];
52
62
  const RunTestsError = Schema.Struct({
53
63
  kind: Schema.Literal("error"),
54
- message: Schema.String
64
+ message: Schema.String,
65
+ reason: Schema.optionalKey(Schema.Literals(RUN_TESTS_ERROR_REASONS)).annotate({ description: "Set when the failure has a specific remedy. `stale-install`: the vitest/node entry this server resolved for the project vanished from disk (the dependency tree was reinstalled under the long-lived server); `message` names the missing path. Restart the MCP server (/mcp in Claude Code) — retrying the call cannot fix it. Absent for a message-only error." })
55
66
  }).annotate({ identifier: "RunTestsError" });
56
67
  const RunTestsNoMatch = Schema.Struct({
57
68
  kind: Schema.Literal("no-match").annotate({ description: "Discriminant — the resolved filter set matched zero test cases. Tests did not run; this is independent of passWithNoTests policy." }),
@@ -115,6 +126,22 @@ function readDiscoveryLastScannedAt() {
115
126
  const value = globalThis[DISCOVERY_LAST_SCAN_SYMBOL];
116
127
  return typeof value === "string" ? value : void 0;
117
128
  }
129
+ const CONFIG_SUGGESTED_ACTIONS_SYMBOL = Symbol.for("vitest-agent:config-suggested-actions");
130
+ /**
131
+ * Reads the config-derived suggested actions the plugin published on a
132
+ * Vitest instance. `undefined` when none were published (the project does
133
+ * not load the plugin, or its config validated clean). Each entry is
134
+ * decoded against `SuggestedActionRecord` so a foreign shape can never
135
+ * reach the wire.
136
+ *
137
+ * @internal
138
+ */
139
+ function readConfigSuggestedActions(vitest) {
140
+ const value = vitest[CONFIG_SUGGESTED_ACTIONS_SYMBOL];
141
+ if (!Array.isArray(value) || value.length === 0) return void 0;
142
+ const decoded = Schema.decodeUnknownResult(Schema.Array(SuggestedActionRecord))(value);
143
+ return Result.isSuccess(decoded) ? decoded.success : void 0;
144
+ }
118
145
  /**
119
146
  * Per-invocation coverage temp directory. Vitest's v8 provider `rm -rf`s the
120
147
  * shared `coverage/` reports directory at run start (`clean: true` default),
@@ -333,6 +360,12 @@ async function validateProjectRoot(projectRoot, ctxCwd) {
333
360
  * resolution throws (e.g. a project root with no local vitest install) so
334
361
  * that case keeps working exactly as it did before this fix.
335
362
  *
363
+ * The property this buys — one module instance shared by the server and the
364
+ * project's test files — is pinned by `vitest-node-module-identity.test.ts`
365
+ * (issue #387), which asserts realpath AND `Object.is` identity against the
366
+ * live repo layout, so a pnpm layout change that reintroduces two copies
367
+ * fails there rather than as a snapshot failure in a consumer's run.
368
+ *
336
369
  * @internal exported for tests
337
370
  */
338
371
  function resolveVitestNodeEntry(root) {
@@ -498,7 +531,8 @@ function toOkPayload(args) {
498
531
  report: args.report,
499
532
  classifications: args.classifications ? Object.fromEntries(args.classifications) : {},
500
533
  discoveryLastScannedAt: args.discoveryLastScannedAt ?? null,
501
- scopedNote: args.scopedNote
534
+ scopedNote: args.scopedNote,
535
+ ...args.suggestedActions !== void 0 && args.suggestedActions.length > 0 ? { suggestedActions: args.suggestedActions } : {}
502
536
  };
503
537
  }
504
538
  /**
@@ -544,6 +578,7 @@ const runTestsBody = async (input, ctx) => {
544
578
  const staleMessage = staleInstallMessage(vitestEntry);
545
579
  if (staleMessage !== null) return {
546
580
  kind: "error",
581
+ reason: "stale-install",
547
582
  message: staleMessage
548
583
  };
549
584
  const timeoutMs = (input.timeout ?? 120) * 1e3;
@@ -604,6 +639,7 @@ const runTestsBody = async (input, ctx) => {
604
639
  const baseReport = buildAgentReport(testModules, unhandledErrors, preliminaryReason, { omitPassingTests: true });
605
640
  const leaks = buildConsoleLeaks(collectConsoleLeakEntries(localVitest.state.getFiles()));
606
641
  const strayOutput = readStrayOutput(localVitest.logger);
642
+ const configSuggestedActions = readConfigSuggestedActions(localVitest);
607
643
  const report = {
608
644
  ...baseReport,
609
645
  ...leaks !== void 0 && { consoleLeaks: leaks },
@@ -644,7 +680,8 @@ const runTestsBody = async (input, ctx) => {
644
680
  report,
645
681
  classifications,
646
682
  discoveryLastScannedAt: readDiscoveryLastScannedAt(),
647
- scopedNote
683
+ scopedNote,
684
+ ...configSuggestedActions !== void 0 && { suggestedActions: configSuggestedActions }
648
685
  });
649
686
  } catch (err) {
650
687
  let message;
@@ -702,7 +739,7 @@ const handleRunTests = (input) => Effect.gen(function* () {
702
739
  * @public
703
740
  */
704
741
  const runTestsTool = Tool.make("run_tests", {
705
- description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot. When projectRoot is omitted, the server anchors the Vitest root at the directory of the vitest (or vite) config Vitest would load anyway, walking up from its boot dir and stopping at the git root — so a server booted inside a package subtree still resolves the root config's relative globalSetup/setupFiles correctly. projectRoot overrides that for this call and is used verbatim, but only after validation: it must be an existing directory belonging to the same git repository as ctx.cwd (checked via `git rev-parse --path-format=absolute --git-common-dir`, which needs git 2.31 or newer and is identical across a repo and all its worktrees, including a sibling worktree checked out from the same repo). A path in a different repository, or a non-existent path, is rejected with `{ kind: \"error\" }` naming both paths — never a silent fallback to ctx.cwd. The resolved root actually used is always echoed back on success. The legacy format=json arg is dropped — structuredContent supersedes it.",
742
+ description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot. When projectRoot is omitted, the server anchors the Vitest root at the directory of the vitest (or vite) config Vitest would load anyway, walking up from its boot dir and stopping at the git root — so a server booted inside a package subtree still resolves the root config's relative globalSetup/setupFiles correctly. projectRoot overrides that for this call and is used verbatim, but only after validation: it must be an existing directory belonging to the same git repository as ctx.cwd (checked via `git rev-parse --path-format=absolute --git-common-dir`, which needs git 2.31 or newer and is identical across a repo and all its worktrees, including a sibling worktree checked out from the same repo). A path in a different repository, or a non-existent path, is rejected with `{ kind: \"error\" }` naming both paths — never a silent fallback to ctx.cwd. The resolved root actually used is always echoed back on success. An error result with `reason: \"stale-install\"` means the vitest install was replaced under this long-lived server (the lockfile was regenerated) and the resolved vitest/node entry is gone; restart the MCP server (/mcp in Claude Code) rather than retrying. The legacy format=json arg is dropped — structuredContent supersedes it.",
706
743
  parameters: RunTestsInput,
707
744
  success: RunTestsResult,
708
745
  dependencies: [
@@ -712,4 +749,4 @@ const runTestsTool = Tool.make("run_tests", {
712
749
  ]
713
750
  }).annotate(Tool.Title, "Run tests").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
714
751
 
715
- export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, deriveRunScope, handleRunTests, isNoMatch, makeBestEffortFork, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveVitestNodeEntry, runTestsSemaphore, runTestsTool, sanitizeTestArgs, staleInstallMessage, toNoMatch, toOkPayload, validateProjectRoot, vitestLoader, withStdioCaptured };
752
+ export { RUN_TESTS_ERROR_REASONS, RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, deriveRunScope, handleRunTests, isNoMatch, makeBestEffortFork, makeCoverageDirOverride, readConfigSuggestedActions, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveVitestNodeEntry, runTestsSemaphore, runTestsTool, sanitizeTestArgs, staleInstallMessage, toNoMatch, toOkPayload, validateProjectRoot, vitestLoader, withStdioCaptured };
package/tools/status.js CHANGED
@@ -2,14 +2,23 @@ import { ToolOutputSchema } from "@effected/mcp";
2
2
  import { Effect, Option, Schema } from "effect";
3
3
  import { DataReader } from "@vitest-agent/engine";
4
4
  import { Tool } from "effect/ai";
5
- import { CacheManifestEntry } from "@vitest-agent/sdk";
5
+ import { CacheManifestEntry, StrayOutput } from "@vitest-agent/sdk";
6
6
 
7
7
  // src/tools/status.ts
8
+ /**
9
+ * A manifest entry plus the project's latest-run stray output (issue 578),
10
+ * present only when that run recorded any. Derived from the sdk entry's
11
+ * fields so the two cannot drift.
12
+ */
13
+ const StatusEntry = Schema.Struct({
14
+ ...CacheManifestEntry.fields,
15
+ strayOutput: Schema.optionalKey(StrayOutput).annotate({ description: "Output the latest run wrote straight to the terminal past Vitest's console capture (a test or a child process with inherited stdio). Absent when it recorded none." })
16
+ }).annotate({ identifier: "TestStatusEntry" });
8
17
  const StatusAvailable = Schema.Struct({
9
18
  dataAvailable: Schema.Literal(true).annotate({ description: "Discriminant — `true` when at least one project entry exists in the manifest." }),
10
19
  manifestUpdatedAt: Schema.String,
11
20
  projectFilter: Schema.optional(Schema.String).annotate({ description: "Echo of the optional `project` filter." }),
12
- entries: Schema.Array(CacheManifestEntry).annotate({ description: "Per-project last-run summary rows. Filtered by `projectFilter` when set." })
21
+ entries: Schema.Array(StatusEntry).annotate({ description: "Per-project last-run summary rows. Filtered by `projectFilter` when set." })
13
22
  }).annotate({ identifier: "TestStatusAvailable" });
14
23
  const StatusAbsent = Schema.Struct({
15
24
  dataAvailable: Schema.Literal(false).annotate({ description: "Discriminant — `false` when no manifest exists or the project filter matched nothing." }),
@@ -39,14 +48,22 @@ const TestStatusInput = Schema.Struct({ project: Schema.optionalKey(Schema.Strin
39
48
  * @public
40
49
  */
41
50
  const handleTestStatus = (input) => Effect.gen(function* () {
42
- const manifestOpt = yield* (yield* DataReader).getManifest();
51
+ const reader = yield* DataReader;
52
+ const manifestOpt = yield* reader.getManifest();
43
53
  if (Option.isNone(manifestOpt)) return {
44
54
  dataAvailable: false,
45
55
  reason: "no_manifest",
46
56
  ...input.project !== void 0 && { projectFilter: input.project }
47
57
  };
48
58
  const manifest = manifestOpt.value;
49
- const entries = input.project === void 0 ? manifest.projects : manifest.projects.filter((e) => e.project === input.project);
59
+ const strayByProject = new Map((yield* reader.getRunsByProject()).map((summary) => [summary.project, summary.strayOutput]));
60
+ const entries = (input.project === void 0 ? manifest.projects : manifest.projects.filter((e) => e.project === input.project)).map((entry) => {
61
+ const strayOutput = strayByProject.get(entry.project) ?? null;
62
+ return strayOutput === null ? entry : {
63
+ ...entry,
64
+ strayOutput
65
+ };
66
+ });
50
67
  if (entries.length === 0) return {
51
68
  dataAvailable: false,
52
69
  reason: "project_filter_empty",
@@ -65,7 +82,7 @@ const handleTestStatus = (input) => Effect.gen(function* () {
65
82
  * @public
66
83
  */
67
84
  const testStatusTool = Tool.make("test_status", {
68
- description: "Use when you need each project's current pass/fail state from the most recent run. Returns a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant).",
85
+ description: "Use when you need each project's current pass/fail state from the most recent run. Returns a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant); an entry carries strayOutput when the project's latest run wrote straight to the terminal.",
69
86
  parameters: TestStatusInput,
70
87
  success: TestStatusResult,
71
88
  dependencies: [DataReader]
@@ -49,7 +49,7 @@ const handleTriageBrief = (input) => Effect.gen(function* () {
49
49
  * @public
50
50
  */
51
51
  const triageBriefTool = Tool.make("triage_brief", {
52
- description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, open TDD sessions, and suggested next actions. Returns a typed envelope in structuredContent ({ hasContent, markdown }).",
52
+ description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, runs that wrote stray output past Vitest's console capture, open TDD sessions, and suggested next actions. Returns a typed envelope in structuredContent ({ hasContent, markdown }).",
53
53
  parameters: TriageBriefInput,
54
54
  success: TriageBriefResult,
55
55
  dependencies: [DataReader]
package/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  *
8
8
  * @public
9
9
  */
10
- const CURRENT_MCP_VERSION = "5.2.9";
10
+ const CURRENT_MCP_VERSION = "5.3.0";
11
11
 
12
12
  export { CURRENT_MCP_VERSION };