@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 +7 -7
- package/index.d.ts +7 -7
- package/package.json +1 -1
- package/prompts/layer.js +6 -0
- package/register-toolkit.js +28 -58
- package/server.js +1 -1
- package/tools/acceptance-metrics.js +5 -19
- package/tools/cache-health.js +4 -36
- package/tools/commit-changes.js +4 -25
- package/tools/configure.js +4 -39
- package/tools/coverage.js +4 -54
- package/tools/errors.js +4 -83
- package/tools/failure-signature-get.js +4 -27
- package/tools/file-coverage.js +4 -36
- package/tools/help.js +6 -8
- package/tools/history.js +4 -40
- package/tools/hypothesis.js +3 -16
- package/tools/inventory.js +3 -96
- package/tools/note.js +3 -43
- package/tools/overview.js +4 -36
- package/tools/run-tests.js +3 -129
- package/tools/settings-list.js +4 -20
- package/tools/status.js +4 -28
- package/tools/tdd-artifact.js +3 -27
- package/tools/tdd-phase-transition-request.js +1 -2
- package/tools/tdd-progress-push.js +1 -2
- package/tools/tdd-task.js +4 -64
- package/tools/test.js +3 -145
- package/tools/trends.js +4 -70
- package/tools/triage-brief.js +3 -5
- package/tools/turn-search.js +4 -15
- package/tools/wrapup-prompt.js +3 -5
- package/version.js +1 -1
package/annotations.js
CHANGED
|
@@ -2,14 +2,14 @@ import { Context } from "effect";
|
|
|
2
2
|
|
|
3
3
|
//#region src/annotations.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* channel
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
8
|
-
* channel
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
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)" })),
|
package/register-toolkit.js
CHANGED
|
@@ -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
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
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 } =
|
|
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
|
|
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
|
|
233
|
-
* (`Schema.toJsonSchemaDocument`
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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
|
|
238
|
-
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty:
|
|
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
|
-
|
|
285
|
-
const
|
|
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 =
|
|
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:
|
|
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,
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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)
|
|
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,
|
|
72
|
+
export { AcceptanceMetricsResult, acceptanceMetricsTool, handleAcceptanceMetrics };
|
package/tools/cache-health.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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)
|
|
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,
|
|
61
|
+
export { CacheHealthResult, cacheHealthTool, handleCacheHealth };
|
package/tools/commit-changes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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)
|
|
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,
|
|
71
|
+
export { CommitChangesInput, CommitChangesResult, commitChangesTool, handleCommitChanges };
|
package/tools/configure.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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)
|
|
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,
|
|
91
|
+
export { ConfigureInput, ConfigureResult, configureTool, handleConfigure };
|
package/tools/coverage.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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)
|
|
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,
|
|
64
|
+
export { TestCoverageInput, TestCoverageResult, handleTestCoverage, testCoverageTool };
|