@vitest-agent/mcp 3.0.4 → 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/tdd-task.js CHANGED
@@ -1,20 +1,11 @@
1
- import { idempotentProcedure } from "../middleware/idempotency.js";
1
+ import { RenderText } from "../annotations.js";
2
+ import { IdempotentReplayMarker } from "../utils/replay-marker.js";
2
3
  import { Effect, Match, Option, Schema, SchemaGetter } from "effect";
3
- import { DataReader, DataStore, GoalDetail } from "@vitest-agent/sdk";
4
+ import { DataReader, DataStore } from "@vitest-agent/engine";
5
+ import { Tool } from "effect/unstable/ai";
6
+ import { GoalDetail } from "@vitest-agent/sdk";
4
7
 
5
8
  //#region src/tools/tdd-task.ts
6
- /**
7
- * Consolidated `tdd_task` MCP tool — Schema-driven implementation.
8
- *
9
- * `start` and `end` mutate; `get` and `resume` read. Every action
10
- * now returns a structured payload — `get` carries the full nested
11
- * `TddTaskDetail` tree plus the `currentPhase` lookup, and `resume`
12
- * carries a compact summary discriminated by `phaseAvailable`. The
13
- * boundary in server.ts uses `formatTddTaskMarkdown` to render
14
- * `get` / `resume` text.
15
- *
16
- * @packageDocumentation
17
- */
18
9
  const TddPhaseRow = Schema.Struct({
19
10
  id: Schema.Number,
20
11
  behaviorId: Schema.NullOr(Schema.Number),
@@ -53,7 +44,8 @@ const TddTaskStartOk = Schema.Struct({
53
44
  action: Schema.Literal("start"),
54
45
  tddTaskId: Schema.Number,
55
46
  goal: Schema.String,
56
- runId: Schema.optional(Schema.String)
47
+ runId: Schema.optional(Schema.String),
48
+ ...IdempotentReplayMarker
57
49
  }).annotate({ identifier: "TddTaskStartOk" });
58
50
  const TddTaskEndOk = Schema.Struct({
59
51
  action: Schema.Literal("end"),
@@ -62,7 +54,8 @@ const TddTaskEndOk = Schema.Struct({
62
54
  "succeeded",
63
55
  "blocked",
64
56
  "abandoned"
65
- ])
57
+ ]),
58
+ ...IdempotentReplayMarker
66
59
  }).annotate({ identifier: "TddTaskEndOk" });
67
60
  const TddTaskGetFound = Schema.Struct({
68
61
  action: Schema.Literal("get"),
@@ -162,32 +155,37 @@ const TddTaskAsMarkdown = TddTaskResult.pipe(Schema.decodeTo(Schema.String, {
162
155
  encode: SchemaGetter.forbidden(() => "TddTaskAsMarkdown is one-way.")
163
156
  }));
164
157
  const StartVariant = Schema.Struct({
165
- action: Schema.Literal("start"),
166
- goal: Schema.String,
167
- sessionId: Schema.optional(Schema.Number),
168
- chatId: Schema.optional(Schema.String),
169
- parentTddTaskId: Schema.optional(Schema.Number),
170
- startedAt: Schema.optional(Schema.String),
171
- runId: Schema.optional(Schema.String)
158
+ action: Schema.Literal("start").annotate({ description: "Lifecycle discriminator" }),
159
+ goal: Schema.String.annotate({ description: "start: goal text" }),
160
+ sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "start: sessions.id (alternative to chatId)" }),
161
+ chatId: Schema.optionalKey(Schema.String).annotate({ description: "start: host chat UUID" }),
162
+ parentTddTaskId: Schema.optionalKey(Schema.Finite).annotate({ description: "start: parent task id when decomposing" }),
163
+ startedAt: Schema.optionalKey(Schema.String),
164
+ runId: Schema.optionalKey(Schema.String)
172
165
  });
173
166
  const EndVariant = Schema.Struct({
174
- action: Schema.Literal("end"),
175
- tddTaskId: Schema.Number,
167
+ action: Schema.Literal("end").annotate({ description: "Lifecycle discriminator" }),
168
+ tddTaskId: Schema.Finite.annotate({ description: "end/get/resume: tdd task id" }),
176
169
  outcome: Schema.Literals([
177
170
  "succeeded",
178
171
  "blocked",
179
172
  "abandoned"
180
- ]),
181
- summaryNoteId: Schema.optional(Schema.Number)
173
+ ]).annotate({ description: "end: final outcome" }),
174
+ summaryNoteId: Schema.optionalKey(Schema.Finite)
182
175
  });
183
176
  const GetVariant = Schema.Struct({
184
- action: Schema.Literal("get"),
185
- tddTaskId: Schema.Number
177
+ action: Schema.Literal("get").annotate({ description: "Lifecycle discriminator" }),
178
+ tddTaskId: Schema.Finite.annotate({ description: "end/get/resume: tdd task id" })
186
179
  });
187
180
  const ResumeVariant = Schema.Struct({
188
- action: Schema.Literal("resume"),
189
- tddTaskId: Schema.Number
181
+ action: Schema.Literal("resume").annotate({ description: "Lifecycle discriminator" }),
182
+ tddTaskId: Schema.Finite.annotate({ description: "end/get/resume: tdd task id" })
190
183
  });
184
+ /**
185
+ * The `tdd_task` tool's parameters — a union discriminated on `action`.
186
+ *
187
+ * @public
188
+ */
191
189
  const TddTaskInput = Schema.Union([
192
190
  StartVariant,
193
191
  EndVariant,
@@ -195,115 +193,119 @@ const TddTaskInput = Schema.Union([
195
193
  ResumeVariant
196
194
  ]);
197
195
  /**
198
- * Single source of truth for the `tdd_task` tool's `action`
199
- * discriminant, consumed by `server.ts`'s served `z.enum(...)` so the
200
- * MCP-SDK-side registration cannot drift from this tRPC input union
201
- * (issue #335).
196
+ * Handler for {@link tddTaskTool}. A `start` with neither `sessionId` nor
197
+ * `chatId`, an unknown `chatId`, or a blank `runId` fails as a defect (the
198
+ * `UnexpectedToolError` envelope on the wire), as the retired tRPC procedure did.
199
+ *
200
+ * @public
202
201
  */
203
- const TDD_TASK_ACTIONS = [
204
- "start",
205
- "end",
206
- "get",
207
- "resume"
208
- ];
209
- const tddTask = idempotentProcedure.input(Schema.toStandardSchemaV1(TddTaskInput)).mutation(async ({ ctx, input }) => {
210
- return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
211
- start: (variant) => Effect.gen(function* () {
212
- const reader = yield* DataReader;
213
- const store = yield* DataStore;
214
- let sessionId;
215
- if (variant.sessionId !== void 0) sessionId = variant.sessionId;
216
- else if (variant.chatId !== void 0) {
217
- const opt = yield* reader.getSessionByChatId(variant.chatId);
218
- if (Option.isNone(opt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${variant.chatId}. Run record session-start first.`));
219
- sessionId = opt.value.id;
220
- } else return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: provide sessionId or chatId"));
221
- if (variant.runId !== void 0 && variant.runId.trim().length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: runId must not be blank"));
222
- return {
223
- action: "start",
224
- tddTaskId: yield* store.writeTddTask({
225
- sessionId,
226
- goal: variant.goal,
227
- startedAt: variant.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
228
- ...variant.runId !== void 0 && { runId: variant.runId },
229
- ...variant.parentTddTaskId !== void 0 && { parentTddTaskId: variant.parentTddTaskId }
230
- }),
202
+ const handleTddTask = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
203
+ start: (variant) => Effect.gen(function* () {
204
+ const reader = yield* DataReader;
205
+ const store = yield* DataStore;
206
+ let sessionId;
207
+ if (variant.sessionId !== void 0) sessionId = variant.sessionId;
208
+ else if (variant.chatId !== void 0) {
209
+ const opt = yield* reader.getSessionByChatId(variant.chatId);
210
+ if (Option.isNone(opt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${variant.chatId}. Run record session-start first.`));
211
+ sessionId = opt.value.id;
212
+ } else return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: provide sessionId or chatId"));
213
+ if (variant.runId !== void 0 && variant.runId.trim().length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("tdd_task action=start: runId must not be blank"));
214
+ return {
215
+ action: "start",
216
+ tddTaskId: yield* store.writeTddTask({
217
+ sessionId,
231
218
  goal: variant.goal,
232
- ...variant.runId !== void 0 && { runId: variant.runId }
233
- };
234
- }),
235
- end: (variant) => Effect.gen(function* () {
236
- yield* (yield* DataStore).endTddTask({
237
- id: variant.tddTaskId,
238
- outcome: variant.outcome,
239
- endedAt: (/* @__PURE__ */ new Date()).toISOString(),
240
- ...variant.summaryNoteId !== void 0 && { summaryNoteId: variant.summaryNoteId }
241
- });
242
- return {
243
- action: "end",
244
- tddTaskId: variant.tddTaskId,
245
- outcome: variant.outcome
246
- };
247
- }),
248
- get: (variant) => Effect.gen(function* () {
249
- const reader = yield* DataReader;
250
- const opt = yield* reader.getTddTaskById(variant.tddTaskId);
251
- if (Option.isNone(opt)) return {
252
- action: "get",
253
- found: false,
254
- tddTaskId: variant.tddTaskId
255
- };
256
- const currentOpt = yield* reader.getCurrentTddPhase(variant.tddTaskId);
257
- const { id, ...rest } = opt.value;
258
- return {
259
- action: "get",
260
- found: true,
261
- task: {
262
- tddTaskId: id,
263
- ...rest
264
- },
265
- currentPhase: Option.match(currentOpt, {
266
- onNone: () => null,
267
- onSome: (p) => ({
268
- id: p.id,
269
- phase: p.phase,
270
- startedAt: p.startedAt,
271
- behaviorId: p.behaviorId
272
- })
219
+ startedAt: variant.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
220
+ ...variant.runId !== void 0 && { runId: variant.runId },
221
+ ...variant.parentTddTaskId !== void 0 && { parentTddTaskId: variant.parentTddTaskId }
222
+ }),
223
+ goal: variant.goal,
224
+ ...variant.runId !== void 0 && { runId: variant.runId }
225
+ };
226
+ }),
227
+ end: (variant) => Effect.gen(function* () {
228
+ yield* (yield* DataStore).endTddTask({
229
+ id: variant.tddTaskId,
230
+ outcome: variant.outcome,
231
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
232
+ ...variant.summaryNoteId !== void 0 && { summaryNoteId: variant.summaryNoteId }
233
+ });
234
+ return {
235
+ action: "end",
236
+ tddTaskId: variant.tddTaskId,
237
+ outcome: variant.outcome
238
+ };
239
+ }),
240
+ get: (variant) => Effect.gen(function* () {
241
+ const reader = yield* DataReader;
242
+ const opt = yield* reader.getTddTaskById(variant.tddTaskId);
243
+ if (Option.isNone(opt)) return {
244
+ action: "get",
245
+ found: false,
246
+ tddTaskId: variant.tddTaskId
247
+ };
248
+ const currentOpt = yield* reader.getCurrentTddPhase(variant.tddTaskId);
249
+ const { id, ...rest } = opt.value;
250
+ return {
251
+ action: "get",
252
+ found: true,
253
+ task: {
254
+ tddTaskId: id,
255
+ ...rest
256
+ },
257
+ currentPhase: Option.match(currentOpt, {
258
+ onNone: () => null,
259
+ onSome: (p) => ({
260
+ id: p.id,
261
+ phase: p.phase,
262
+ startedAt: p.startedAt,
263
+ behaviorId: p.behaviorId
273
264
  })
274
- };
275
- }),
276
- resume: (variant) => Effect.gen(function* () {
277
- const reader = yield* DataReader;
278
- const tddOpt = yield* reader.getTddTaskById(variant.tddTaskId);
279
- if (Option.isNone(tddOpt)) return {
280
- action: "resume",
281
- found: false,
282
- tddTaskId: variant.tddTaskId
283
- };
284
- const tdd = tddOpt.value;
285
- const currentOpt = yield* reader.getCurrentTddPhase(variant.tddTaskId);
286
- return {
287
- action: "resume",
288
- found: true,
289
- tddTaskId: tdd.id,
290
- goal: tdd.goal,
291
- status: tdd.outcome ?? "in progress",
292
- currentPhase: Option.match(currentOpt, {
293
- onNone: () => null,
294
- onSome: (p) => ({
295
- id: p.id,
296
- phase: p.phase,
297
- startedAt: p.startedAt,
298
- behaviorId: p.behaviorId
299
- })
300
- }),
301
- phasesRecorded: tdd.phases.length,
302
- artifactsRecorded: tdd.artifacts.length
303
- };
304
- })
305
- })));
306
- });
265
+ })
266
+ };
267
+ }),
268
+ resume: (variant) => Effect.gen(function* () {
269
+ const reader = yield* DataReader;
270
+ const tddOpt = yield* reader.getTddTaskById(variant.tddTaskId);
271
+ if (Option.isNone(tddOpt)) return {
272
+ action: "resume",
273
+ found: false,
274
+ tddTaskId: variant.tddTaskId
275
+ };
276
+ const tdd = tddOpt.value;
277
+ const currentOpt = yield* reader.getCurrentTddPhase(variant.tddTaskId);
278
+ return {
279
+ action: "resume",
280
+ found: true,
281
+ tddTaskId: tdd.id,
282
+ goal: tdd.goal,
283
+ status: tdd.outcome ?? "in progress",
284
+ currentPhase: Option.match(currentOpt, {
285
+ onNone: () => null,
286
+ onSome: (p) => ({
287
+ id: p.id,
288
+ phase: p.phase,
289
+ startedAt: p.startedAt,
290
+ behaviorId: p.behaviorId
291
+ })
292
+ }),
293
+ phasesRecorded: tdd.phases.length,
294
+ artifactsRecorded: tdd.artifacts.length
295
+ };
296
+ })
297
+ })).pipe(Effect.orDie);
298
+ /**
299
+ * The Effect-native `tdd_task` tool.
300
+ *
301
+ * @public
302
+ */
303
+ const tddTaskTool = Tool.make("tdd_task", {
304
+ description: "Use to manage a TDD task lifecycle, with an action discriminator: action='start' (goal, sessionId|chatId, parentTddTaskId?, startedAt?, runId?) opens a new task; action='end' (tddTaskId, outcome, summaryNoteId?) closes one; action='get' (tddTaskId) returns markdown details; action='resume' (tddTaskId) returns a compact digest.",
305
+ parameters: TddTaskInput,
306
+ success: TddTaskResult,
307
+ dependencies: [DataReader, DataStore]
308
+ }).annotate(Tool.Title, "TDD task").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTddTaskMarkdown(encoded));
307
309
 
308
310
  //#endregion
309
- export { TDD_TASK_ACTIONS, TddTaskAsMarkdown, TddTaskResult, formatTddTaskMarkdown, tddTask };
311
+ export { TddTaskInput, TddTaskResult, formatTddTaskMarkdown, handleTddTask, tddTaskTool };