@vitest-agent/mcp 4.2.2 → 4.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/annotations.js CHANGED
@@ -2,14 +2,14 @@ import { Context } from "effect";
2
2
 
3
3
  //#region src/annotations.ts
4
4
  /**
5
- * Renders a tool's encoded result as the human-readable `content[0].text`
6
- * channel (markdown, typically). Returning `undefined` — or leaving the
7
- * annotation unset — falls back to `JSON.stringify(encoded)`.
8
- *
9
- * Attach with `tool.annotate(RenderText, (encoded) => ...)`; the renderer
10
- * receives the wire-encoded result (the same value that becomes
11
- * `structuredContent`), never the decoded domain value.
5
+ * Formerly rendered a tool's encoded result as a markdown `content[0].text`
6
+ * channel. The server no longer reads it: every successful result carries
7
+ * the typed object in `structuredContent` and its JSON in `content[0].text`,
8
+ * because Claude Code forwards only `structuredContent` to the model.
9
+ * Attaching it is a no-op.
12
10
  *
11
+ * @deprecated Nothing reads this annotation any more; remove the
12
+ * `.annotate(RenderText, ...)` call. It will be removed in the next major.
13
13
  * @public
14
14
  */
15
15
  const RenderText = Context.Reference("@vitest-agent/mcp/RenderText", { defaultValue: () => void 0 });
package/index.d.ts CHANGED
@@ -4,14 +4,14 @@ import { McpSchema, Tool, Toolkit } from "effect/unstable/ai";
4
4
  import "@vitest-agent/sdk";
5
5
  //#region src/annotations.d.ts
6
6
  /**
7
- * Renders a tool's encoded result as the human-readable `content[0].text`
8
- * channel (markdown, typically). Returning `undefined` — or leaving the
9
- * annotation unset — falls back to `JSON.stringify(encoded)`.
10
- *
11
- * Attach with `tool.annotate(RenderText, (encoded) => ...)`; the renderer
12
- * receives the wire-encoded result (the same value that becomes
13
- * `structuredContent`), never the decoded domain value.
7
+ * Formerly rendered a tool's encoded result as a markdown `content[0].text`
8
+ * channel. The server no longer reads it: every successful result carries
9
+ * the typed object in `structuredContent` and its JSON in `content[0].text`,
10
+ * because Claude Code forwards only `structuredContent` to the model.
11
+ * Attaching it is a no-op.
14
12
  *
13
+ * @deprecated Nothing reads this annotation any more; remove the
14
+ * `.annotate(RenderText, ...)` call. It will be removed in the next major.
15
15
  * @public
16
16
  */
17
17
  export declare const RenderText: Context.Reference<((result: unknown) => string | undefined) | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/mcp",
3
- "version": "4.2.2",
3
+ "version": "4.3.0",
4
4
  "private": false,
5
5
  "description": "Model Context Protocol server for vitest-agent. Exposes 53 tools for agent access to test data, TDD lifecycle, and session management.",
6
6
  "keywords": [
package/prompts/layer.js CHANGED
@@ -38,12 +38,14 @@ const toMessages = (result) => result.messages.map((m) => ({
38
38
  const projectArg = Schema.optionalKey(Schema.String.annotate({ description: "Filter to a specific project" }));
39
39
  const Triage = McpServer.prompt({
40
40
  name: PROMPT_NAMES[0],
41
+ title: "Triage Recent Failures",
41
42
  description: "Orient toward a triage workflow over the most recent test run; compose triage_brief, failure_signature_get, hypothesis_record.",
42
43
  parameters: { project: projectArg },
43
44
  content: (args) => Effect.succeed(toMessages(triagePrompt(args.project !== void 0 ? { project: args.project } : {})))
44
45
  });
45
46
  const WhyFlaky = McpServer.prompt({
46
47
  name: PROMPT_NAMES[1],
48
+ title: "Diagnose a Flaky Test",
47
49
  description: "Diagnose why a named test is flaky; compose test_history and failure_signature_get with timing/shared-state framing.",
48
50
  parameters: {
49
51
  test: Schema.String.annotate({ description: "Full hierarchical test name (e.g. 'Suite > nested > test')" }),
@@ -56,6 +58,7 @@ const WhyFlaky = McpServer.prompt({
56
58
  });
57
59
  const RegressionSincePass = McpServer.prompt({
58
60
  name: PROMPT_NAMES[2],
61
+ title: "Find What Broke a Test",
59
62
  description: "Walk back from the test's most recent passing run to identify the change that broke it; compose test_history, commit_changes, turn_search.",
60
63
  parameters: {
61
64
  test: Schema.String.annotate({ description: "Full hierarchical test name" }),
@@ -68,12 +71,14 @@ const RegressionSincePass = McpServer.prompt({
68
71
  });
69
72
  const ExplainFailure = McpServer.prompt({
70
73
  name: PROMPT_NAMES[3],
74
+ title: "Explain a Failure Class",
71
75
  description: "Synthesize a root-cause explanation from the recurrence history of a failure signature.",
72
76
  parameters: { signature: Schema.String.annotate({ description: "16-char failure signature hex" }) },
73
77
  content: (args) => Effect.succeed(toMessages(explainFailurePrompt({ signature: args.signature })))
74
78
  });
75
79
  const TddResume = McpServer.prompt({
76
80
  name: PROMPT_NAMES[4],
81
+ title: "Resume TDD Work",
77
82
  description: "Resume the active TDD task from its current phase; iron-law reminder for evidence-bound transitions.",
78
83
  parameters: { sessionId: Schema.optionalKey(Schema.String.annotate({ description: "Host session id (defaults to MCP server's recovered SessionContext)" })) },
79
84
  content: (args) => Effect.gen(function* () {
@@ -84,6 +89,7 @@ const TddResume = McpServer.prompt({
84
89
  });
85
90
  const Wrapup = McpServer.prompt({
86
91
  name: PROMPT_NAMES[5],
92
+ title: "Generate a Session Wrapup",
87
93
  description: "Surface the same wrapup content the post-hooks emit automatically.",
88
94
  parameters: {
89
95
  kind: Schema.optionalKey(Schema.Literals(WRAPUP_KINDS).annotate({ description: "Wrapup variant (default: user_prompt_nudge)" })),
@@ -1,4 +1,3 @@
1
- import { RenderText } from "./annotations.js";
2
1
  import { buildUnexpectedToolErrorEnvelope } from "./utils/tool-error-envelope.js";
3
2
  import { Cause, Context, Effect, ErrorReporter, Layer, Option, References, Result, Schema, Stream } from "effect";
4
3
  import { AiError, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai";
@@ -68,48 +67,22 @@ const inlineRootRefs = (schema) => {
68
67
  };
69
68
  };
70
69
  /**
71
- * Deep-copy `schema`, setting `additionalProperties: false` on every
72
- * object node that declares `properties` (or is a bare object with no
73
- * combinator), and rewriting a top-level union of discriminated object
74
- * shapes to `{ type: "object", oneOf, "x-discriminator" }` so the served
75
- * schema still satisfies MCP's object-root requirement.
70
+ * Make an input JSON Schema servable as an MCP tool input: inline a `$ref`
71
+ * root (and `$ref` members of a top-level union), and rewrite a top-level
72
+ * union of discriminated object shapes to `{ type: "object", oneOf,
73
+ * "x-discriminator" }` so it satisfies MCP's object-root requirement. The
74
+ * object nodes themselves are left as Effect emitted them — closed, when the
75
+ * document was built with `onExcessProperty: "error"`.
76
76
  *
77
77
  * @internal
78
78
  */
79
- const strictifyJsonSchema = (schema) => {
80
- const visit = (node) => {
81
- if (Array.isArray(node)) return node.map(visit);
82
- if (!isPlainObject(node)) return node;
83
- const out = {};
84
- for (const [key, value] of Object.entries(node)) switch (key) {
85
- case "properties":
86
- case "$defs":
87
- case "definitions":
88
- out[key] = isPlainObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, visit(v)])) : value;
89
- break;
90
- case "items":
91
- case "prefixItems":
92
- case "anyOf":
93
- case "oneOf":
94
- case "allOf":
95
- case "not":
96
- out[key] = visit(value);
97
- break;
98
- case "additionalProperties":
99
- out[key] = isPlainObject(value) ? visit(value) : value;
100
- break;
101
- default: out[key] = value;
102
- }
103
- const hasCombinator = Array.isArray(out.anyOf) || Array.isArray(out.oneOf) || Array.isArray(out.allOf);
104
- if ((out.type === "object" || isPlainObject(out.properties)) && !hasCombinator && !isPlainObject(out.additionalProperties)) out.additionalProperties = false;
105
- return out;
106
- };
107
- const strict = visit(inlineRootRefs(schema));
108
- const members = strict.anyOf ?? strict.oneOf;
109
- if (Array.isArray(members) && strict.type === void 0) {
79
+ const objectRootedInputSchema = (schema) => {
80
+ const root = inlineRootRefs(schema);
81
+ const members = root.anyOf ?? root.oneOf;
82
+ if (Array.isArray(members) && root.type === void 0) {
110
83
  const discriminant = findDiscriminant(members);
111
84
  if (discriminant !== void 0) {
112
- const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = strict;
85
+ const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = root;
113
86
  return {
114
87
  type: "object",
115
88
  ...rest,
@@ -118,7 +91,7 @@ const strictifyJsonSchema = (schema) => {
118
91
  };
119
92
  }
120
93
  }
121
- return strict;
94
+ return root;
122
95
  };
123
96
  const resolveRef = (node, root) => lookupRef(node.$ref, root) ?? node;
124
97
  /** Pick the union member a payload object matches, or `undefined` when none does unambiguously. */
@@ -229,17 +202,20 @@ const toolResultContent = (encoded) => encoded === void 0 ? [] : [{
229
202
  const omitRequestServices = Context.omit(McpSchema.McpRequestContext, McpSchema.McpServerClient, HttpServerRequest.HttpServerRequest, References.CurrentLogLevel);
230
203
  const isParameterValidationError = (error) => AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError";
231
204
  /**
232
- * The input JSON Schema document as rc.116's `toolInputJsonSchema` builds it
233
- * (`Schema.toJsonSchemaDocument` with `onExcessProperty` following the
234
- * tool's strict mode, definitions attached as `$defs`); the top-level `$ref`
235
- * hoist happens in `inlineRootRefs` via `strictifyJsonSchema`.
205
+ * The input JSON Schema a tool is served with: the document rc.116's
206
+ * `toolInputJsonSchema` builds for a strict tool (`Schema.toJsonSchemaDocument`
207
+ * with `onExcessProperty: "error"`, so every object node carries
208
+ * `additionalProperties: false`; definitions attached as `$defs`), made
209
+ * object-rooted by {@link objectRootedInputSchema}.
210
+ *
211
+ * @internal
236
212
  */
237
- const toolInputJsonSchema = (schema, strict) => {
238
- const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" });
239
- return Object.keys(document.definitions).length === 0 ? document.schema : {
213
+ const servedInputJsonSchema = (schema) => {
214
+ const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" });
215
+ return objectRootedInputSchema(Object.keys(document.definitions).length === 0 ? document.schema : {
240
216
  ...document.schema,
241
217
  $defs: document.definitions
242
- };
218
+ });
243
219
  };
244
220
  const envelopeResult = (toolName, err) => {
245
221
  const envelope = buildUnexpectedToolErrorEnvelope(toolName, err);
@@ -281,12 +257,9 @@ const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
281
257
  const failure = Cause.findFail(cause);
282
258
  return Result.isFailure(failure) && !Cause.hasDies(cause) ? Effect.failCause(failure.failure) : Effect.logError(`tool ${tool.name} failed`, cause).pipe(Effect.andThen(reportCause(cause)), Effect.as(envelopeResult(tool.name, Result.isSuccess(failure) ? failure.success.error : Cause.squash(cause))));
283
259
  };
284
- const strict = Tool.getStrictMode(tool) === true;
285
- const rawJsonSchema = Tool.isDynamic(tool) ? tool.jsonSchema : void 0;
286
- if (strict && rawJsonSchema !== void 0) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
287
- const decodeOptions = strict ? { onExcessProperty: "error" } : void 0;
260
+ if (Tool.isDynamic(tool)) return yield* Effect.die(`McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`);
261
+ const decodeOptions = { onExcessProperty: "error" };
288
262
  const annotations = tool.annotations;
289
- const render = Context.get(annotations, RenderText);
290
263
  const toolMeta = Context.getOrUndefined(annotations, Tool.Meta);
291
264
  const isDeclaredFailure = Schema.is(tool.failureSchema);
292
265
  const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema);
@@ -306,7 +279,7 @@ const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
306
279
  };
307
280
  const outputJsonSchema = inlineRootRefs(Tool.getJsonSchemaFromSchema(tool.successSchema));
308
281
  const outputSchema = outputJsonSchema.type === "object" ? yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(outputJsonSchema).pipe(Effect.orDie) : void 0;
309
- const servedInput = strictifyJsonSchema(rawJsonSchema ?? toolInputJsonSchema(tool.parametersSchema, strict));
282
+ const servedInput = servedInputJsonSchema(tool.parametersSchema);
310
283
  const inputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolJson)(servedInput).pipe(Effect.orDie);
311
284
  const description = Tool.getDescription(tool);
312
285
  const mcpTool = new McpSchema.Tool({
@@ -333,10 +306,7 @@ const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
333
306
  return built.handle(tool.name, raw, void 0, decodeOptions).pipe(Stream.unwrap, Stream.runLast, Effect.flatMap(Effect.fromOption), Effect.flatMap((result) => result.isFailure && result.failureOrigin !== "handler" ? Effect.failCause(Cause.annotate(Cause.fail(result.result), Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result"))) : Effect.succeed(new McpSchema.CallToolResult({
334
307
  isError: result.isFailure,
335
308
  structuredContent: result.isFailure ? void 0 : toStructuredContent(result.encodedResult),
336
- content: result.isFailure || result.encodedResult === void 0 ? toolResultContent(result.encodedResult) : [{
337
- type: "text",
338
- text: render?.(result.encodedResult) ?? JSON.stringify(result.encodedResult)
339
- }]
309
+ content: toolResultContent(result.encodedResult)
340
310
  }))), Effect.catchCause(handleCause), Effect.provideContext(services));
341
311
  }
342
312
  });
@@ -355,4 +325,4 @@ const registerStrictToolkitEffect = Effect.fnUntraced(function* (toolkit) {
355
325
  const registerStrictToolkit = (toolkit) => Layer.effectDiscard(registerStrictToolkitEffect(toolkit)).pipe(Layer.provide(McpServer.McpServer.layer));
356
326
 
357
327
  //#endregion
358
- export { collectUnknownKeys, inlineRootRefs, registerStrictToolkit, registerStrictToolkitEffect, strictifyJsonSchema };
328
+ export { collectUnknownKeys, inlineRootRefs, objectRootedInputSchema, registerStrictToolkit, registerStrictToolkitEffect, servedInputJsonSchema };
package/server.js CHANGED
@@ -20,7 +20,7 @@ const SERVER_INSTRUCTIONS = [
20
20
  "vitest-agent serves the test landscape of one Vitest project to LLM coding agents: run results, failure detail and history, coverage, flakiness, and the TDD lifecycle (tasks, goals, behaviors, phase transitions, hypotheses, notes).",
21
21
  "Call the help tool first for the full tool reference; triage_brief orients on the current landscape; run_tests executes Vitest and persists the run so every query tool reads it back.",
22
22
  "Every input is strict at every object level: an unknown key is rejected with a message naming the unrecognized key(s) and the accepted params, so fix the call rather than retrying it.",
23
- "Every successful result carries the typed object in structuredContent and a text rendering (markdown where the tool renders one, otherwise the same JSON) in content[0].text; read structuredContent when you need fields.",
23
+ "Every successful result carries the typed object in structuredContent, and content[0].text holds the same object as JSON; read structuredContent when you need fields.",
24
24
  "An expected domain error is NOT an isError result: it is a normal result whose structuredContent has ok: false and an error object. tdd_goal and tdd_behavior put _tag, the failing ids and a remediation naming the tool to call next in that object; register_agent puts a code and a message in it, plus existingAgentId or expectedPrefix when the code calls for one.",
25
25
  "An isError result means the call itself failed: invalid params (bare text in content[0].text, no structuredContent) or an unexpected crash inside the tool (structuredContent is an UnexpectedToolError envelope with ok: false and a remediation). Read the message and fix the call."
26
26
  ].join("\n");
@@ -1,11 +1,10 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
6
5
  //#region src/tools/acceptance-metrics.ts
7
6
  const totalAnnotation = { description: "Sample size — number of observations the metric ratio is computed over." };
8
- const ratioAnnotation = { description: "Compliance ratio in [0, 1]. Multiply by 100 for the percentage form rendered in the markdown view." };
7
+ const ratioAnnotation = { description: "Compliance ratio in [0, 1]. Multiply by 100 for the percentage form." };
9
8
  /**
10
9
  * The `acceptance_metrics` tool's success payload.
11
10
  *
@@ -49,19 +48,6 @@ const AcceptanceMetricsResult = Schema.Struct({
49
48
  title: "Acceptance metrics",
50
49
  description: "The four spec Annex A metrics computed from the current database. Each carries a sample size, a count, and a ratio."
51
50
  });
52
- const fmtBucket = (r) => r.total === 0 ? "no data" : `${(r.ratio * 100).toFixed(1)}% (n=${r.total})`;
53
- const formatAcceptanceMetricsMarkdown = (m) => [
54
- "# Acceptance metrics",
55
- "",
56
- `1. Phase-evidence integrity: ${fmtBucket(m.phaseEvidenceIntegrity)} — target ≥80%`,
57
- `2. Compliance-hook responsiveness: ${fmtBucket(m.complianceHookResponsiveness)} — target ≥40%`,
58
- `3. Orientation usefulness: ${fmtBucket(m.orientationUsefulness)} — target ≥50%`,
59
- `4. Anti-pattern detection rate: ${fmtBucket(m.antiPatternDetectionRate)} — target ≥95%`
60
- ].join("\n");
61
- const AcceptanceMetricsAsMarkdown = AcceptanceMetricsResult.pipe(Schema.decodeTo(Schema.String, {
62
- decode: SchemaGetter.transform((data) => formatAcceptanceMetricsMarkdown(data)),
63
- encode: SchemaGetter.forbidden(() => "AcceptanceMetricsAsMarkdown is one-way: markdown cannot be parsed back to AcceptanceMetricsResult.")
64
- }));
65
51
  /**
66
52
  * Handler for {@link acceptanceMetricsTool}.
67
53
  *
@@ -77,10 +63,10 @@ const handleAcceptanceMetrics = () => Effect.gen(function* () {
77
63
  * @public
78
64
  */
79
65
  const acceptanceMetricsTool = Tool.make("acceptance_metrics", {
80
- description: "Use when you need the four spec Annex A acceptance metrics computed from the current database. Returns markdown in content[] and a typed JSON object in structuredContent (per-metric { total, ratio, ... }).",
66
+ description: "Use when you need the four spec Annex A acceptance metrics computed from the current database. Returns a typed JSON object in structuredContent (per-metric { total, ratio, ... }).",
81
67
  success: AcceptanceMetricsResult,
82
68
  dependencies: [DataReader]
83
- }).annotate(Tool.Title, "Acceptance metrics").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatAcceptanceMetricsMarkdown(encoded));
69
+ }).annotate(Tool.Title, "Acceptance metrics").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
84
70
 
85
71
  //#endregion
86
- export { AcceptanceMetricsResult, acceptanceMetricsTool, formatAcceptanceMetricsMarkdown, handleAcceptanceMetrics };
72
+ export { AcceptanceMetricsResult, acceptanceMetricsTool, handleAcceptanceMetrics };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
  import { CacheManifest } from "@vitest-agent/sdk";
@@ -29,37 +28,6 @@ const CacheHealthResult = Schema.Union([ManifestPresent, ManifestAbsent]).annota
29
28
  description: "Cache health snapshot. Discriminate on `manifestPresent` to see whether the manifest exists."
30
29
  });
31
30
  const STALE_AFTER_MS = 864e5;
32
- const iconForResult = (r) => {
33
- if (r === "passed") return "✅";
34
- if (r === "failed") return "❌";
35
- if (r === "interrupted") return "⚠️";
36
- return "⬜";
37
- };
38
- const formatCacheHealthMarkdown = (data) => {
39
- const lines = ["# Cache Health", ""];
40
- if (data.manifestPresent === false) {
41
- lines.push("- ❌ **Manifest:** not found — run tests to populate the cache");
42
- return lines.join("\n");
43
- }
44
- const { manifest, ageMs, stale } = data;
45
- const ageHours = ageMs / 36e5;
46
- lines.push("- ✅ **Manifest:** present");
47
- lines.push(`- ℹ️ **Projects:** ${manifest.projects.length}`);
48
- lines.push(`- ℹ️ **Cache directory:** \`${manifest.cacheDir}\``);
49
- lines.push(`- ℹ️ **Last updated:** ${manifest.updatedAt}`);
50
- if (stale) lines.push(`- ⚠️ **Staleness:** cache is ${Math.round(ageHours)} hours old — consider re-running tests`);
51
- else lines.push(`- ✅ **Staleness:** cache is ${Math.round(ageHours * 60)} minutes old`);
52
- lines.push("", "## Projects", "");
53
- for (const entry of manifest.projects) {
54
- const lastRun = entry.lastRun ? new Date(entry.lastRun).toLocaleString() : "never";
55
- lines.push(`- ${iconForResult(entry.lastResult)} **${entry.project}** — last run: ${lastRun}`);
56
- }
57
- return lines.join("\n");
58
- };
59
- const CacheHealthAsMarkdown = CacheHealthResult.pipe(Schema.decodeTo(Schema.String, {
60
- decode: SchemaGetter.transform((data) => formatCacheHealthMarkdown(data)),
61
- encode: SchemaGetter.forbidden(() => "CacheHealthAsMarkdown is one-way: markdown cannot be parsed back to CacheHealthResult.")
62
- }));
63
31
  /**
64
32
  * Handler for {@link cacheHealthTool}.
65
33
  *
@@ -84,10 +52,10 @@ const handleCacheHealth = () => Effect.gen(function* () {
84
52
  * @public
85
53
  */
86
54
  const cacheHealthTool = Tool.make("cache_health", {
87
- description: "Use when you suspect stale data and need manifest presence, project states, and staleness. Returns markdown in content[] and a typed JSON object in structuredContent ({ manifestPresent, manifest?, ageMs?, stale? }).",
55
+ description: "Use when you suspect stale data and need manifest presence, project states, and staleness. Returns a typed JSON object in structuredContent ({ manifestPresent, manifest?, ageMs?, stale? }).",
88
56
  success: CacheHealthResult,
89
57
  dependencies: [DataReader]
90
- }).annotate(Tool.Title, "Cache health").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatCacheHealthMarkdown(encoded));
58
+ }).annotate(Tool.Title, "Cache health").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
91
59
 
92
60
  //#endregion
93
- export { CacheHealthResult, cacheHealthTool, formatCacheHealthMarkdown, handleCacheHealth };
61
+ export { CacheHealthResult, cacheHealthTool, handleCacheHealth };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
@@ -37,26 +36,6 @@ const CommitChangesResult = Schema.Struct({
37
36
  title: "commit_changes result",
38
37
  description: "Commit metadata + per-file changes captured by the post-commit Bash hook."
39
38
  });
40
- const formatCommitChangesMarkdown = (data) => {
41
- if (data.commits.length === 0) return data.filterSha !== void 0 ? `No commit recorded with sha ${data.filterSha}.` : "No commits recorded yet. The PostToolUse hook on `git commit` populates this table.";
42
- const lines = [];
43
- for (const e of data.commits) {
44
- lines.push(`## ${e.sha.slice(0, 8)} ${e.message ?? "(no message)"}`);
45
- if (e.author !== null) lines.push(`- Author: ${e.author}`);
46
- if (e.committedAt !== null) lines.push(`- When: ${e.committedAt}`);
47
- if (e.branch !== null) lines.push(`- Branch: ${e.branch}`);
48
- if (e.files.length > 0) {
49
- lines.push("- Changed files:");
50
- for (const f of e.files) lines.push(` - \`${f.filePath}\` (${f.changeKind})`);
51
- }
52
- lines.push("");
53
- }
54
- return lines.join("\n").trim();
55
- };
56
- const CommitChangesAsMarkdown = CommitChangesResult.pipe(Schema.decodeTo(Schema.String, {
57
- decode: SchemaGetter.transform((data) => formatCommitChangesMarkdown(data)),
58
- encode: SchemaGetter.forbidden(() => "CommitChangesAsMarkdown is one-way: markdown cannot be parsed back to CommitChangesResult.")
59
- }));
60
39
  /**
61
40
  * The `commit_changes` tool's parameters.
62
41
  *
@@ -82,11 +61,11 @@ const handleCommitChanges = (input) => Effect.gen(function* () {
82
61
  * @public
83
62
  */
84
63
  const commitChangesTool = Tool.make("commit_changes", {
85
- description: "Use when you need commit metadata and changed files captured by the post-commit hook. Returns up to 20 most-recent when sha is omitted. Returns markdown in content[] and a typed JSON object in structuredContent ({ filterSha?, count, commits[] }).",
64
+ description: "Use when you need commit metadata and changed files captured by the post-commit hook. Returns up to 20 most-recent when sha is omitted. Returns a typed JSON object in structuredContent ({ filterSha?, count, commits[] }).",
86
65
  parameters: CommitChangesInput,
87
66
  success: CommitChangesResult,
88
67
  dependencies: [DataReader]
89
- }).annotate(Tool.Title, "Commit changes").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatCommitChangesMarkdown(encoded));
68
+ }).annotate(Tool.Title, "Commit changes").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
90
69
 
91
70
  //#endregion
92
- export { CommitChangesInput, CommitChangesResult, commitChangesTool, formatCommitChangesMarkdown, handleCommitChanges };
71
+ export { CommitChangesInput, CommitChangesResult, commitChangesTool, handleCommitChanges };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
@@ -41,40 +40,6 @@ const ConfigureResult = Schema.Union([SettingsFound, SettingsAbsent]).annotate({
41
40
  title: "configure result",
42
41
  description: "Captured Vitest settings for a run, or an absence record when the lookup found nothing."
43
42
  });
44
- const formatSettings = (s) => {
45
- const lines = [`# Settings — \`${s.hash}\``, ""];
46
- lines.push(`**Captured:** ${s.capturedAt}`);
47
- if (s.project !== null) lines.push(`**Project:** ${s.project}`);
48
- if (s.environment !== null) lines.push(`**Environment:** ${s.environment}`);
49
- if (s.pool !== null) lines.push(`**Pool:** ${s.pool}`);
50
- if (s.shard !== null) lines.push(`**Shard:** ${s.shard}`);
51
- lines.push("", "## Coverage", `- **Enabled:** ${s.coverageEnabled ? "yes" : "no"}`);
52
- if (s.coverageProvider !== null) lines.push(`- **Provider:** ${s.coverageProvider}`);
53
- if (s.coverageThresholds !== null) lines.push(`- **Thresholds:** \`${s.coverageThresholds}\``);
54
- if (s.coverageTargets !== null) lines.push(`- **Targets:** \`${s.coverageTargets}\``);
55
- if (s.reporters !== null) lines.push("", "## Reporters", `\`${s.reporters}\``);
56
- const envKeys = Object.keys(s.envVars);
57
- if (envKeys.length > 0) {
58
- lines.push("", "## Environment Variables");
59
- for (const key of envKeys) lines.push(`- \`${key}\`: \`${s.envVars[key]}\``);
60
- }
61
- return lines.join("\n");
62
- };
63
- const formatConfigureMarkdown = (data) => {
64
- if (data.found) return formatSettings(data.settings);
65
- if (data.source === "latest") return [
66
- "# Configure",
67
- "",
68
- "No settings captured yet. Run tests first.",
69
- "",
70
- "Configuration is written automatically by `AgentPlugin` when tests run."
71
- ].join("\n");
72
- return `No settings found for hash \`${data.requestedHash ?? "(unknown)"}\`.`;
73
- };
74
- const ConfigureAsMarkdown = ConfigureResult.pipe(Schema.decodeTo(Schema.String, {
75
- decode: SchemaGetter.transform((data) => formatConfigureMarkdown(data)),
76
- encode: SchemaGetter.forbidden(() => "ConfigureAsMarkdown is one-way: markdown cannot be parsed back to ConfigureResult.")
77
- }));
78
43
  /**
79
44
  * The `configure` tool's parameters.
80
45
  *
@@ -116,11 +81,11 @@ const handleConfigure = (input) => Effect.gen(function* () {
116
81
  * @public
117
82
  */
118
83
  const configureTool = Tool.make("configure", {
119
- description: "Use when you need the captured Vitest settings for a test run. Returns markdown in content[] and a typed JSON object in structuredContent ({ found, source, settings?, requestedHash? }).",
84
+ description: "Use when you need the captured Vitest settings for a test run. Returns a typed JSON object in structuredContent ({ found, source, settings?, requestedHash? }).",
120
85
  parameters: ConfigureInput,
121
86
  success: ConfigureResult,
122
87
  dependencies: [DataReader]
123
- }).annotate(Tool.Title, "Configure").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatConfigureMarkdown(encoded));
88
+ }).annotate(Tool.Title, "Configure").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
124
89
 
125
90
  //#endregion
126
- export { ConfigureInput, ConfigureResult, configureTool, formatConfigureMarkdown, handleConfigure };
91
+ export { ConfigureInput, ConfigureResult, configureTool, handleConfigure };
package/tools/coverage.js CHANGED
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
  import { CoverageReport } from "@vitest-agent/sdk";
@@ -24,55 +23,6 @@ const TestCoverageResult = Schema.Union([CoverageAvailable, CoverageAbsent]).ann
24
23
  title: "test_coverage result",
25
24
  description: "Per-project coverage report. Discriminate on `dataAvailable` for cold-start handling."
26
25
  });
27
- const METRICS = [
28
- "statements",
29
- "branches",
30
- "functions",
31
- "lines"
32
- ];
33
- /** @internal */
34
- const renderFileCoverageTable = (fileCoverage) => {
35
- const rows = [
36
- `### \`${fileCoverage.file}\``,
37
- "",
38
- "| Metric | Value |",
39
- "| --- | --- |"
40
- ];
41
- for (const metric of METRICS) rows.push(`| ${metric} | ${fileCoverage.summary[metric].toFixed(2)}% |`);
42
- if (fileCoverage.uncoveredLines) rows.push(`| Uncovered lines | \`${fileCoverage.uncoveredLines}\` |`);
43
- rows.push("");
44
- return rows;
45
- };
46
- const formatTestCoverageMarkdown = (data) => {
47
- if (!data.dataAvailable) return "No coverage data available. Run tests with coverage enabled.";
48
- const lines = ["# Coverage Report", ""];
49
- const { totals, thresholds, targets } = data.coverage;
50
- const headerCols = targets ? "| Metric | Value | Enforced threshold | Target |" : "| Metric | Value | Enforced threshold |";
51
- const sepCols = targets ? "| --- | --- | --- | --- |" : "| --- | --- | --- |";
52
- lines.push("## Totals", "", headerCols, sepCols);
53
- for (const metric of METRICS) {
54
- const value = totals[metric];
55
- const threshold = thresholds.global[metric];
56
- const thresholdStr = threshold !== void 0 ? `${threshold}%` : "—";
57
- const icon = threshold !== void 0 && value < threshold ? "❌" : "✅";
58
- const targetStr = targets ? targets.global[metric] !== void 0 ? `${targets.global[metric]}%` : "—" : void 0;
59
- lines.push(`| ${metric} | ${icon} ${value.toFixed(2)}% | ${thresholdStr} |${targetStr !== void 0 ? ` ${targetStr} |` : ""}`);
60
- }
61
- lines.push("");
62
- if (data.coverage.lowCoverage.length > 0) {
63
- lines.push("## Coverage Gaps", "", "Files below the enforced threshold (build-blocking):", "");
64
- for (const fileCoverage of data.coverage.lowCoverage) lines.push(...renderFileCoverageTable(fileCoverage));
65
- } else lines.push("✅ All files meet coverage thresholds.", "");
66
- if (data.coverage.belowTarget && data.coverage.belowTarget.length > 0) {
67
- lines.push("## Coverage Improvements Needed", "", "Files below the aspirational target (passing the enforced threshold):", "");
68
- for (const fileCoverage of data.coverage.belowTarget) lines.push(...renderFileCoverageTable(fileCoverage));
69
- }
70
- return lines.join("\n");
71
- };
72
- const TestCoverageAsMarkdown = TestCoverageResult.pipe(Schema.decodeTo(Schema.String, {
73
- decode: SchemaGetter.transform((data) => formatTestCoverageMarkdown(data)),
74
- encode: SchemaGetter.forbidden(() => "TestCoverageAsMarkdown is one-way.")
75
- }));
76
26
  /**
77
27
  * The `test_coverage` tool's parameters.
78
28
  *
@@ -104,11 +54,11 @@ const handleTestCoverage = (input) => Effect.gen(function* () {
104
54
  * @public
105
55
  */
106
56
  const testCoverageTool = Tool.make("test_coverage", {
107
- description: "Use when coverage drops and you need per-metric gap analysis against thresholds and targets. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, project, coverage } or absent variant).",
57
+ description: "Use when coverage drops and you need per-metric gap analysis against thresholds and targets. Returns a typed JSON object in structuredContent ({ dataAvailable, project, coverage } or absent variant).",
108
58
  parameters: TestCoverageInput,
109
59
  success: TestCoverageResult,
110
60
  dependencies: [DataReader]
111
- }).annotate(Tool.Title, "Test coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestCoverageMarkdown(encoded));
61
+ }).annotate(Tool.Title, "Test coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
112
62
 
113
63
  //#endregion
114
- export { TestCoverageInput, TestCoverageResult, formatTestCoverageMarkdown, handleTestCoverage, testCoverageTool };
64
+ export { TestCoverageInput, TestCoverageResult, handleTestCoverage, testCoverageTool };