@vitest-agent/mcp 3.0.3 → 4.0.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.
Files changed (54) hide show
  1. package/README.md +7 -6
  2. package/annotations.js +18 -0
  3. package/bin/vitest-agent-mcp.js +5 -140
  4. package/{middleware/idempotency.js → idempotency.js} +48 -49
  5. package/index.d.ts +5610 -1549
  6. package/index.js +35 -17
  7. package/main.d.ts +31 -0
  8. package/main.js +144 -0
  9. package/package.json +9 -6
  10. package/prompts/layer.js +108 -0
  11. package/register-toolkit.js +311 -0
  12. package/server.js +24 -894
  13. package/{context.js → session.js} +38 -22
  14. package/toolkit.js +86 -0
  15. package/tools/acceptance-metrics.js +26 -14
  16. package/tools/cache-health.js +28 -17
  17. package/tools/commit-changes.js +33 -10
  18. package/tools/configure.js +33 -10
  19. package/tools/coverage.js +34 -5
  20. package/tools/errors.js +36 -26
  21. package/tools/failure-signature-get.js +33 -10
  22. package/tools/file-coverage.js +37 -13
  23. package/tools/help.js +45 -4
  24. package/tools/history.js +39 -19
  25. package/tools/hypothesis.js +118 -102
  26. package/tools/inventory.js +149 -146
  27. package/tools/note.js +131 -113
  28. package/tools/overview.js +33 -10
  29. package/tools/ping.js +22 -10
  30. package/tools/register-agent.js +88 -62
  31. package/tools/run-tests.js +76 -23
  32. package/tools/settings-list.js +27 -10
  33. package/tools/status.js +35 -10
  34. package/tools/tdd-artifact.js +37 -22
  35. package/tools/tdd-behavior.js +120 -108
  36. package/tools/tdd-goal.js +98 -85
  37. package/tools/tdd-phase-transition-request.js +201 -166
  38. package/tools/tdd-progress-push.js +102 -0
  39. package/tools/tdd-task.js +140 -138
  40. package/tools/test.js +152 -138
  41. package/tools/trends.js +37 -18
  42. package/tools/triage-brief.js +34 -15
  43. package/tools/turn-search.js +39 -16
  44. package/tools/wrapup-prompt.js +37 -17
  45. package/utils/crash-guards.js +0 -22
  46. package/utils/replay-marker.js +12 -0
  47. package/utils/safe-format-fatal-error.js +0 -16
  48. package/utils/tool-error-envelope.js +3 -3
  49. package/version.js +13 -0
  50. package/layers/McpLive.js +0 -29
  51. package/prompts/index.js +0 -89
  52. package/router.js +0 -74
  53. package/session-env.js +0 -112
  54. package/utils/effect-to-zod.js +0 -158
package/tools/test.js CHANGED
@@ -1,7 +1,8 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
3
3
  import { Effect, Match, Option, Schema, SchemaGetter } from "effect";
4
- import { DataReader } from "@vitest-agent/sdk";
4
+ import { DataReader } from "@vitest-agent/engine";
5
+ import { Tool } from "effect/unstable/ai";
5
6
 
6
7
  //#region src/tools/test.ts
7
8
  const TestRowSchema = Schema.Struct({
@@ -141,6 +142,11 @@ const TestArtifactsResult = Schema.Struct({
141
142
  count: Schema.Number,
142
143
  artifacts: Schema.Array(ArtifactRowSchema)
143
144
  }).annotate({ identifier: "TestArtifacts" });
145
+ /**
146
+ * The `test` tool's success payload.
147
+ *
148
+ * @public
149
+ */
144
150
  const TestResult = Schema.Union([
145
151
  TestListResult,
146
152
  TestGetFound,
@@ -297,40 +303,47 @@ const TestAsMarkdown = TestResult.pipe(Schema.decodeTo(Schema.String, {
297
303
  }));
298
304
  const ListVariant = Schema.Struct({
299
305
  action: Schema.Literal("list"),
300
- project: Schema.optional(Schema.String),
301
- state: Schema.optional(Schema.String),
302
- module: Schema.optional(Schema.String),
303
- limit: Schema.optional(Schema.Number)
306
+ project: Schema.optionalKey(Schema.String),
307
+ state: Schema.optionalKey(Schema.String).annotate({ description: "list: filter by state" }),
308
+ module: Schema.optionalKey(Schema.String).annotate({ description: "list: filter by module path" }),
309
+ limit: Schema.optionalKey(Schema.Finite).annotate({ description: "list: max rows to return" })
304
310
  });
305
311
  const GetVariant = Schema.Struct({
306
312
  action: Schema.Literal("get"),
307
- fullName: Schema.String,
308
- project: Schema.optional(Schema.String),
309
- modulePath: Schema.optional(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." })
313
+ fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
314
+ project: Schema.optionalKey(Schema.String),
315
+ modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." })
310
316
  });
311
317
  const ForFileVariant = Schema.Struct({
312
318
  action: Schema.Literal("for_file"),
313
- filePath: Schema.String
319
+ filePath: Schema.String.annotate({ description: "for_file: source file path" })
314
320
  });
315
321
  const ForTagVariant = Schema.Struct({
316
322
  action: Schema.Literal("for_tag"),
317
- tag: Schema.String,
318
- project: Schema.optional(Schema.String)
323
+ tag: Schema.String.annotate({ description: "for_tag: tag name" }),
324
+ project: Schema.optionalKey(Schema.String)
319
325
  });
320
326
  const AnnotationsVariant = Schema.Struct({
321
327
  action: Schema.Literal("annotations"),
322
- fullName: Schema.String,
323
- project: Schema.optional(Schema.String),
324
- modulePath: Schema.optional(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
325
- maxBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
328
+ fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
329
+ project: Schema.optionalKey(Schema.String),
330
+ modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
331
+ maxBytes: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
326
332
  });
327
333
  const ArtifactsVariant = Schema.Struct({
328
334
  action: Schema.Literal("artifacts"),
329
- fullName: Schema.String,
330
- project: Schema.optional(Schema.String),
331
- modulePath: Schema.optional(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
332
- maxBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
335
+ fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
336
+ project: Schema.optionalKey(Schema.String),
337
+ modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
338
+ maxBytes: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
333
339
  });
340
+ /**
341
+ * The `test` tool's parameters — a union discriminated on `action`. The
342
+ * served JSON Schema is a `oneOf` over the variants with
343
+ * `x-discriminator: "action"`.
344
+ *
345
+ * @public
346
+ */
334
347
  const TestInput = Schema.Union([
335
348
  ListVariant,
336
349
  GetVariant,
@@ -340,132 +353,133 @@ const TestInput = Schema.Union([
340
353
  ArtifactsVariant
341
354
  ]);
342
355
  /**
343
- * Single source of truth for the `test` tool's `action` discriminant,
344
- * consumed by `server.ts`'s served `z.enum(...)` so the MCP-SDK-side
345
- * registration cannot drift from this tRPC input union (issue #335).
356
+ * Handler for {@link testTool}.
357
+ *
358
+ * @public
346
359
  */
347
- const TEST_ACTIONS = [
348
- "list",
349
- "get",
350
- "for_file",
351
- "for_tag",
352
- "annotations",
353
- "artifacts"
354
- ];
355
- const test = publicProcedure.input(Schema.toStandardSchemaV1(TestInput)).query(async ({ ctx, input }) => {
356
- return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
357
- list: (variant) => Effect.gen(function* () {
358
- const reader = yield* DataReader;
359
- const opts = {};
360
- if (variant.state !== void 0) opts.state = variant.state;
361
- if (variant.module !== void 0) opts.module = variant.module;
362
- if (variant.limit !== void 0) opts.limit = variant.limit;
363
- const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
364
- const grouped = yield* collectProjectRows(targets, (project) => reader.listTests(project, opts));
365
- const groups = grouped.groups.map((group) => ({
366
- project: group.project,
367
- tests: group.rows
368
- }));
369
- return {
370
- action: "list",
371
- count: grouped.total,
372
- groups
373
- };
374
- }),
375
- get: (variant) => Effect.gen(function* () {
376
- const reader = yield* DataReader;
377
- const candidates = variant.project ? [variant.project] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => r.project)));
378
- for (const project of candidates) {
379
- const modules = yield* reader.getTestModulesByFullName(project, variant.fullName);
380
- if (modules.length === 0) continue;
381
- if (variant.modulePath === void 0 && modules.length > 1) return {
382
- action: "get",
383
- found: false,
384
- project,
385
- fullName: variant.fullName,
386
- ambiguous: true,
387
- candidateModules: modules
388
- };
389
- const testOpt = yield* reader.getTestByFullName(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
390
- if (Option.isNone(testOpt)) continue;
391
- const matchingErrors = (yield* reader.getErrors(project)).filter((e) => e.testFullName === variant.fullName && e.moduleFile === testOpt.value.module).map((e) => ({
392
- name: e.name,
393
- message: e.message,
394
- diff: e.diff,
395
- stack: e.stack
396
- }));
397
- const testHistory = (yield* reader.getHistory(project, {
398
- testName: variant.fullName,
399
- modulePath: testOpt.value.module
400
- })).tests.find((entry) => entry.fullName === variant.fullName);
401
- return {
402
- action: "get",
403
- found: true,
404
- project,
405
- test: testOpt.value,
406
- errors: matchingErrors,
407
- runs: testHistory ? testHistory.runs : []
408
- };
409
- }
410
- return {
360
+ const handleTest = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
361
+ list: (variant) => Effect.gen(function* () {
362
+ const reader = yield* DataReader;
363
+ const opts = {};
364
+ if (variant.state !== void 0) opts.state = variant.state;
365
+ if (variant.module !== void 0) opts.module = variant.module;
366
+ if (variant.limit !== void 0) opts.limit = variant.limit;
367
+ const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
368
+ const grouped = yield* collectProjectRows(targets, (project) => reader.listTests(project, opts));
369
+ const groups = grouped.groups.map((group) => ({
370
+ project: group.project,
371
+ tests: group.rows
372
+ }));
373
+ return {
374
+ action: "list",
375
+ count: grouped.total,
376
+ groups
377
+ };
378
+ }),
379
+ get: (variant) => Effect.gen(function* () {
380
+ const reader = yield* DataReader;
381
+ const candidates = variant.project ? [variant.project] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => r.project)));
382
+ for (const project of candidates) {
383
+ const modules = yield* reader.getTestModulesByFullName(project, variant.fullName);
384
+ if (modules.length === 0) continue;
385
+ if (variant.modulePath === void 0 && modules.length > 1) return {
411
386
  action: "get",
412
387
  found: false,
413
- project: variant.project ?? candidates[0] ?? "",
414
- fullName: variant.fullName
415
- };
416
- }),
417
- for_file: (variant) => Effect.gen(function* () {
418
- const testFiles = yield* (yield* DataReader).getTestsForFile(variant.filePath);
419
- return {
420
- action: "for_file",
421
- filePath: variant.filePath,
422
- count: testFiles.length,
423
- testFiles
424
- };
425
- }),
426
- for_tag: (variant) => Effect.gen(function* () {
427
- const reader = yield* DataReader;
428
- const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
429
- const grouped = yield* collectProjectRows(targets, (project) => reader.listTestsForTag(variant.tag, { project }));
430
- const groups = grouped.groups.map((group) => ({
431
- project: group.project,
432
- tests: group.rows
433
- }));
434
- return {
435
- action: "for_tag",
436
- tag: variant.tag,
437
- count: grouped.total,
438
- groups
439
- };
440
- }),
441
- annotations: (variant) => Effect.gen(function* () {
442
- const reader = yield* DataReader;
443
- const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
444
- const rows = yield* reader.getAnnotationsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
445
- const annotations = applyBodyBudget(rows, variant.maxBytes ?? 0);
446
- return {
447
- action: "annotations",
448
388
  project,
449
389
  fullName: variant.fullName,
450
- count: annotations.length,
451
- annotations
390
+ ambiguous: true,
391
+ candidateModules: modules
452
392
  };
453
- }),
454
- artifacts: (variant) => Effect.gen(function* () {
455
- const reader = yield* DataReader;
456
- const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
457
- const rows = yield* reader.getArtifactsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
458
- const artifacts = applyBodyBudget(rows, variant.maxBytes ?? 0);
393
+ const testOpt = yield* reader.getTestByFullName(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
394
+ if (Option.isNone(testOpt)) continue;
395
+ const matchingErrors = (yield* reader.getErrors(project)).filter((e) => e.testFullName === variant.fullName && e.moduleFile === testOpt.value.module).map((e) => ({
396
+ name: e.name,
397
+ message: e.message,
398
+ diff: e.diff,
399
+ stack: e.stack
400
+ }));
401
+ const testHistory = (yield* reader.getHistory(project, {
402
+ testName: variant.fullName,
403
+ modulePath: testOpt.value.module
404
+ })).tests.find((entry) => entry.fullName === variant.fullName);
459
405
  return {
460
- action: "artifacts",
406
+ action: "get",
407
+ found: true,
461
408
  project,
462
- fullName: variant.fullName,
463
- count: artifacts.length,
464
- artifacts
409
+ test: testOpt.value,
410
+ errors: matchingErrors,
411
+ runs: testHistory ? testHistory.runs : []
465
412
  };
466
- })
467
- })));
468
- });
413
+ }
414
+ return {
415
+ action: "get",
416
+ found: false,
417
+ project: variant.project ?? candidates[0] ?? "",
418
+ fullName: variant.fullName
419
+ };
420
+ }),
421
+ for_file: (variant) => Effect.gen(function* () {
422
+ const testFiles = yield* (yield* DataReader).getTestsForFile(variant.filePath);
423
+ return {
424
+ action: "for_file",
425
+ filePath: variant.filePath,
426
+ count: testFiles.length,
427
+ testFiles
428
+ };
429
+ }),
430
+ for_tag: (variant) => Effect.gen(function* () {
431
+ const reader = yield* DataReader;
432
+ const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
433
+ const grouped = yield* collectProjectRows(targets, (project) => reader.listTestsForTag(variant.tag, { project }));
434
+ const groups = grouped.groups.map((group) => ({
435
+ project: group.project,
436
+ tests: group.rows
437
+ }));
438
+ return {
439
+ action: "for_tag",
440
+ tag: variant.tag,
441
+ count: grouped.total,
442
+ groups
443
+ };
444
+ }),
445
+ annotations: (variant) => Effect.gen(function* () {
446
+ const reader = yield* DataReader;
447
+ const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
448
+ const rows = yield* reader.getAnnotationsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
449
+ const annotations = applyBodyBudget(rows, variant.maxBytes ?? 0);
450
+ return {
451
+ action: "annotations",
452
+ project,
453
+ fullName: variant.fullName,
454
+ count: annotations.length,
455
+ annotations
456
+ };
457
+ }),
458
+ artifacts: (variant) => Effect.gen(function* () {
459
+ const reader = yield* DataReader;
460
+ const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
461
+ const rows = yield* reader.getArtifactsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
462
+ const artifacts = applyBodyBudget(rows, variant.maxBytes ?? 0);
463
+ return {
464
+ action: "artifacts",
465
+ project,
466
+ fullName: variant.fullName,
467
+ count: artifacts.length,
468
+ artifacts
469
+ };
470
+ })
471
+ })).pipe(Effect.orDie);
472
+ /**
473
+ * The Effect-native `test` tool.
474
+ *
475
+ * @public
476
+ */
477
+ const testTool = Tool.make("test", {
478
+ description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project; action='annotations' (fullName, project?, modulePath?) returns the test annotations the author recorded via context.annotate; action='artifacts' (fullName, project?, modulePath?) returns the test artifacts recorded for the test — both return attachment descriptors (contentType, path, byteSize) and omit inline bodies unless maxBytes (a non-negative integer byte budget for the whole response, default 0) is passed, and neither has anything to do with TDD artifacts (see tdd_artifact_list). structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
479
+ parameters: TestInput,
480
+ success: TestResult,
481
+ dependencies: [DataReader]
482
+ }).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestMarkdown(encoded));
469
483
 
470
484
  //#endregion
471
- export { TEST_ACTIONS, TestAsMarkdown, TestResult, formatTestMarkdown, test };
485
+ export { TestInput, TestResult, formatTestMarkdown, handleTest, testTool };
package/tools/trends.js CHANGED
@@ -1,18 +1,10 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Option, Schema, SchemaGetter } from "effect";
3
- import { DataReader, TrendRecord } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
5
+ import { TrendRecord } from "@vitest-agent/sdk";
4
6
 
5
7
  //#region src/tools/trends.ts
6
- /**
7
- * `test_trends` MCP tool — Schema-driven implementation.
8
- *
9
- * Wraps the existing `TrendRecord` Schema in a result envelope that
10
- * carries the project name and a `dataAvailable` flag, so callers
11
- * can distinguish "no trend data yet" from "data plus rendering"
12
- * without parsing prose.
13
- *
14
- * @packageDocumentation
15
- */
16
8
  const TrendsAvailable = Schema.Struct({
17
9
  dataAvailable: Schema.Literal(true).annotate({ description: "Discriminant — `true` when at least one trend entry exists for the project." }),
18
10
  project: Schema.String,
@@ -22,6 +14,11 @@ const TrendsAbsent = Schema.Struct({
22
14
  dataAvailable: Schema.Literal(false).annotate({ description: "Discriminant — `false` when fewer than two runs have been recorded for the project." }),
23
15
  project: Schema.String
24
16
  }).annotate({ identifier: "TestTrendsAbsent" });
17
+ /**
18
+ * The `test_trends` tool's success payload.
19
+ *
20
+ * @public
21
+ */
25
22
  const TestTrendsResult = Schema.Union([TrendsAvailable, TrendsAbsent]).annotate({
26
23
  identifier: "TestTrendsResult",
27
24
  title: "test_trends result",
@@ -92,10 +89,21 @@ const TestTrendsAsMarkdown = TestTrendsResult.pipe(Schema.decodeTo(Schema.String
92
89
  decode: SchemaGetter.transform((data) => formatTestTrendsMarkdown(data)),
93
90
  encode: SchemaGetter.forbidden(() => "TestTrendsAsMarkdown is one-way: markdown cannot be parsed back to TestTrendsResult.")
94
91
  }));
95
- const testTrends = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
96
- project: Schema.String,
97
- limit: Schema.optional(Schema.Number)
98
- }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
92
+ /**
93
+ * The `test_trends` tool's parameters.
94
+ *
95
+ * @public
96
+ */
97
+ const TestTrendsInput = Schema.Struct({
98
+ project: Schema.String.annotate({ description: "Project name (required)" }),
99
+ limit: Schema.optionalKey(Schema.Finite).annotate({ description: "Max number of trend entries to return" })
100
+ });
101
+ /**
102
+ * Handler for {@link testTrendsTool}.
103
+ *
104
+ * @public
105
+ */
106
+ const handleTestTrends = (input) => Effect.gen(function* () {
99
107
  const trendsOpt = yield* (yield* DataReader).getTrends(input.project, input.limit);
100
108
  if (Option.isNone(trendsOpt) || trendsOpt.value.entries.length === 0) return {
101
109
  dataAvailable: false,
@@ -106,7 +114,18 @@ const testTrends = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
106
114
  project: input.project,
107
115
  trends: trendsOpt.value
108
116
  };
109
- })));
117
+ }).pipe(Effect.orDie);
118
+ /**
119
+ * The Effect-native `test_trends` tool.
120
+ *
121
+ * @public
122
+ */
123
+ const testTrendsTool = Tool.make("test_trends", {
124
+ description: "Use when you want to see whether a project's coverage is trending up or down over time. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, project, trends? }).",
125
+ parameters: TestTrendsInput,
126
+ success: TestTrendsResult,
127
+ dependencies: [DataReader]
128
+ }).annotate(Tool.Title, "Test trends").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestTrendsMarkdown(encoded));
110
129
 
111
130
  //#endregion
112
- export { TestTrendsAsMarkdown, TestTrendsResult, formatTestTrendsMarkdown, testTrends };
131
+ export { TestTrendsInput, TestTrendsResult, formatTestTrendsMarkdown, handleTestTrends, testTrendsTool };
@@ -1,17 +1,13 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema } from "effect";
3
- import { formatTriageEffect } from "@vitest-agent/sdk";
3
+ import { DataReader, formatTriageEffect } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/triage-brief.ts
6
7
  /**
7
- * `triage_brief` MCP tool Schema-driven implementation.
8
+ * The `triage_brief` tool's success payload.
8
9
  *
9
- * The structured payload is a thin envelope around the markdown
10
- * rendering since this is a narrative tool — there's no underlying
11
- * record set the agent would parse separately. The `hasContent` flag
12
- * lets callers branch on the cold-start case without grepping prose.
13
- *
14
- * @packageDocumentation
10
+ * @public
15
11
  */
16
12
  const TriageBriefResult = Schema.Struct({
17
13
  hasContent: Schema.Boolean.annotate({ description: "`false` when no orientation signal is available yet (run tests to populate)." }),
@@ -21,10 +17,21 @@ const TriageBriefResult = Schema.Struct({
21
17
  title: "triage_brief result",
22
18
  description: "Orientation triage envelope. Branch on `hasContent` for cold-start; consume `markdown` for rendering."
23
19
  });
24
- const triageBrief = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
25
- project: Schema.optional(Schema.String),
26
- maxLines: Schema.optional(Schema.Number)
27
- }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
20
+ /**
21
+ * The `triage_brief` tool's parameters.
22
+ *
23
+ * @public
24
+ */
25
+ const TriageBriefInput = Schema.Struct({
26
+ project: Schema.optionalKey(Schema.String).annotate({ description: "Filter to a specific project" }),
27
+ maxLines: Schema.optionalKey(Schema.Finite).annotate({ description: "Soft cap on rendered output lines" })
28
+ });
29
+ /**
30
+ * Handler for {@link triageBriefTool}.
31
+ *
32
+ * @public
33
+ */
34
+ const handleTriageBrief = (input) => Effect.gen(function* () {
28
35
  const md = yield* formatTriageEffect({
29
36
  ...input.project !== void 0 && { project: input.project },
30
37
  ...input.maxLines !== void 0 && { maxLines: input.maxLines }
@@ -36,7 +43,19 @@ const triageBrief = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struc
36
43
  hasContent: false,
37
44
  markdown: "No orientation signal yet — run tests to populate the database."
38
45
  };
39
- })));
46
+ });
47
+ /**
48
+ * The Effect-native `triage_brief` tool. The text channel is the
49
+ * pre-rendered `markdown` field itself.
50
+ *
51
+ * @public
52
+ */
53
+ const triageBriefTool = Tool.make("triage_brief", {
54
+ description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, open TDD sessions, and suggested next actions. Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, markdown }).",
55
+ parameters: TriageBriefInput,
56
+ success: TriageBriefResult,
57
+ dependencies: [DataReader]
58
+ }).annotate(Tool.Title, "Triage brief").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.markdown);
40
59
 
41
60
  //#endregion
42
- export { TriageBriefResult, triageBrief };
61
+ export { TriageBriefInput, TriageBriefResult, handleTriageBrief, triageBriefTool };
@@ -1,13 +1,9 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema, SchemaGetter } from "effect";
3
- import { DataReader } from "@vitest-agent/sdk";
3
+ import { DataReader } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/turn-search.ts
6
- /**
7
- * `turn_search` MCP tool — Schema-driven implementation.
8
- *
9
- * @packageDocumentation
10
- */
11
7
  const TurnRow = Schema.Struct({
12
8
  id: Schema.Finite.annotate({ description: "Numeric primary key of this turn row." }),
13
9
  sessionId: Schema.Finite.annotate({ description: "Owning `sessions.id` (integer FK)." }),
@@ -19,6 +15,11 @@ const TurnRow = Schema.Struct({
19
15
  identifier: "TurnRow",
20
16
  description: "One row from the turns log."
21
17
  });
18
+ /**
19
+ * The `turn_search` tool's success payload.
20
+ *
21
+ * @public
22
+ */
22
23
  const TurnSearchResult = Schema.Struct({
23
24
  count: Schema.Finite.annotate({ description: "Number of matching turn rows returned." }),
24
25
  turns: Schema.Array(TurnRow).annotate({ description: "Matching turns ordered by `occurredAt` ascending." })
@@ -37,10 +38,15 @@ const TurnSearchAsMarkdown = TurnSearchResult.pipe(Schema.decodeTo(Schema.String
37
38
  decode: SchemaGetter.transform((data) => formatTurnSearchMarkdown(data)),
38
39
  encode: SchemaGetter.forbidden(() => "TurnSearchAsMarkdown is one-way: markdown cannot be parsed back to TurnSearchResult.")
39
40
  }));
40
- const turnSearch = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
41
- sessionId: Schema.optional(Schema.Number),
42
- since: Schema.optional(Schema.String),
43
- type: Schema.optional(Schema.Literals([
41
+ /**
42
+ * The `turn_search` tool's parameters.
43
+ *
44
+ * @public
45
+ */
46
+ const TurnSearchInput = Schema.Struct({
47
+ sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "Filter to a specific session id" }),
48
+ since: Schema.optionalKey(Schema.String).annotate({ description: "ISO 8601 cutoff — return turns after this timestamp" }),
49
+ type: Schema.optionalKey(Schema.Literals([
44
50
  "user_prompt",
45
51
  "tool_call",
46
52
  "tool_result",
@@ -48,9 +54,15 @@ const turnSearch = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
48
54
  "hook_fire",
49
55
  "note",
50
56
  "hypothesis"
51
- ])),
52
- limit: Schema.optional(Schema.Number)
53
- }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
57
+ ])).annotate({ description: "Filter by turn type" }),
58
+ limit: Schema.optionalKey(Schema.Finite).annotate({ description: "Max turns to return (default 100)" })
59
+ });
60
+ /**
61
+ * Handler for {@link turnSearchTool}.
62
+ *
63
+ * @public
64
+ */
65
+ const handleTurnSearch = (input) => Effect.gen(function* () {
54
66
  const rows = yield* (yield* DataReader).searchTurns({
55
67
  ...input.sessionId !== void 0 && { sessionId: input.sessionId },
56
68
  ...input.since !== void 0 && { since: input.since },
@@ -61,7 +73,18 @@ const turnSearch = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
61
73
  count: rows.length,
62
74
  turns: rows
63
75
  };
64
- })));
76
+ }).pipe(Effect.orDie);
77
+ /**
78
+ * The Effect-native `turn_search` tool.
79
+ *
80
+ * @public
81
+ */
82
+ const turnSearchTool = Tool.make("turn_search", {
83
+ description: "Use when you need to find past turns across sessions by type, time, or session. Returns markdown in content[] and a typed JSON object in structuredContent ({ count, turns[] }).",
84
+ parameters: TurnSearchInput,
85
+ success: TurnSearchResult,
86
+ dependencies: [DataReader]
87
+ }).annotate(Tool.Title, "Turn search").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTurnSearchMarkdown(encoded));
65
88
 
66
89
  //#endregion
67
- export { TurnSearchAsMarkdown, TurnSearchResult, formatTurnSearchMarkdown, turnSearch };
90
+ export { TurnSearchInput, TurnSearchResult, formatTurnSearchMarkdown, handleTurnSearch, turnSearchTool };
@@ -1,16 +1,13 @@
1
- import { publicProcedure } from "../context.js";
1
+ import { RenderText } from "../annotations.js";
2
2
  import { Effect, Schema } from "effect";
3
- import { formatWrapupEffect } from "@vitest-agent/sdk";
3
+ import { DataReader, formatWrapupEffect } from "@vitest-agent/engine";
4
+ import { Tool } from "effect/unstable/ai";
4
5
 
5
6
  //#region src/tools/wrapup-prompt.ts
6
7
  /**
7
- * `wrapup_prompt` MCP tool Schema-driven implementation.
8
+ * The `wrapup_prompt` tool's success payload.
8
9
  *
9
- * Same envelope shape as `triage_brief`: thin wrapper around the
10
- * markdown rendering with a `hasContent` discriminant for the empty
11
- * case.
12
- *
13
- * @packageDocumentation
10
+ * @public
14
11
  */
15
12
  const WrapupPromptResult = Schema.Struct({
16
13
  hasContent: Schema.Boolean.annotate({ description: "`false` when there is nothing to wrap up for the named session/kind." }),
@@ -27,18 +24,29 @@ const WrapupPromptResult = Schema.Struct({
27
24
  title: "wrapup_prompt result",
28
25
  description: "Wrap-up envelope. Branch on `hasContent` for the empty case; consume `markdown` for rendering."
29
26
  });
30
- const wrapupPrompt = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
31
- sessionId: Schema.optional(Schema.Number),
32
- chatId: Schema.optional(Schema.String),
33
- kind: Schema.optional(Schema.Literals([
27
+ /**
28
+ * The `wrapup_prompt` tool's parameters.
29
+ *
30
+ * @public
31
+ */
32
+ const WrapupPromptInput = Schema.Struct({
33
+ sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "sessions.id (integer); omit to use chatId" }),
34
+ chatId: Schema.optionalKey(Schema.String).annotate({ description: "Host chat UUID (alternative to sessionId)" }),
35
+ kind: Schema.optionalKey(Schema.Literals([
34
36
  "stop",
35
37
  "session_end",
36
38
  "pre_compact",
37
39
  "tdd_handoff",
38
40
  "user_prompt_nudge"
39
- ])),
40
- userPromptHint: Schema.optional(Schema.String)
41
- }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
41
+ ])).annotate({ description: "Wrap-up flavor (default: session_end)" }),
42
+ userPromptHint: Schema.optionalKey(Schema.String).annotate({ description: "For user_prompt_nudge: the prompt text to inspect" })
43
+ });
44
+ /**
45
+ * Handler for {@link wrapupPromptTool}.
46
+ *
47
+ * @public
48
+ */
49
+ const handleWrapupPrompt = (input) => Effect.gen(function* () {
42
50
  const kind = input.kind ?? "session_end";
43
51
  const md = yield* formatWrapupEffect({
44
52
  ...input.sessionId !== void 0 && { sessionId: input.sessionId },
@@ -55,7 +63,19 @@ const wrapupPrompt = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Stru
55
63
  kind,
56
64
  markdown: "Nothing to wrap up."
57
65
  };
58
- })));
66
+ });
67
+ /**
68
+ * The Effect-native `wrapup_prompt` tool. The text channel is the
69
+ * pre-rendered `markdown` field itself.
70
+ *
71
+ * @public
72
+ */
73
+ const wrapupPromptTool = Tool.make("wrapup_prompt", {
74
+ description: "Use when a session is ending and you need a tailored wrap-up prompt (Stop / SessionEnd / PreCompact / TDD handoff / UserPromptSubmit nudge variants). Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, kind, markdown }).",
75
+ parameters: WrapupPromptInput,
76
+ success: WrapupPromptResult,
77
+ dependencies: [DataReader]
78
+ }).annotate(Tool.Title, "Wrap-up prompt").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.markdown);
59
79
 
60
80
  //#endregion
61
- export { WrapupPromptResult, wrapupPrompt };
81
+ export { WrapupPromptInput, WrapupPromptResult, handleWrapupPrompt, wrapupPromptTool };