@vitest-agent/mcp 4.3.1 → 5.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.
- package/README.md +5 -4
- package/idempotency.js +1 -1
- package/index.d.ts +2235 -2128
- package/index.js +2 -2
- package/main.d.ts +15 -16
- package/main.js +58 -96
- package/package.json +5 -3
- package/server.js +16 -28
- package/toolkit.js +12 -8
- package/tools/_tdd-error-envelope.js +5 -14
- package/tools/cache-health.js +2 -1
- package/tools/configure.js +2 -1
- package/tools/coverage.js +2 -1
- package/tools/failure-signature-get.js +2 -1
- package/tools/file-coverage.js +3 -2
- package/tools/hypothesis.js +33 -23
- package/tools/inventory.js +6 -5
- package/tools/note.js +6 -5
- package/tools/overview.js +2 -1
- package/tools/ping.js +15 -5
- package/tools/register-agent.js +2 -1
- package/tools/run-tests.js +18 -8
- package/tools/status.js +2 -1
- package/tools/tdd-behavior.js +8 -10
- package/tools/tdd-goal.js +8 -10
- package/tools/tdd-phase-transition-request.js +21 -14
- package/tools/tdd-task.js +25 -13
- package/tools/test.js +6 -5
- package/tools/trends.js +2 -1
- package/version.js +1 -1
- package/register-toolkit.js +0 -328
- package/utils/crash-guards.js +0 -13
- package/utils/tool-error-envelope.js +0 -61
package/tools/hypothesis.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { McpSession } from "../session.js";
|
|
2
2
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
3
|
+
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
3
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
4
5
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
6
|
import { Tool } from "effect/unstable/ai";
|
|
6
|
-
import { DataStoreError } from "@vitest-agent/sdk";
|
|
7
7
|
|
|
8
8
|
//#region src/tools/hypothesis.ts
|
|
9
9
|
const HypothesisRowSchema = Schema.Struct({
|
|
@@ -32,11 +32,11 @@ const HypothesisListOk = Schema.Struct({
|
|
|
32
32
|
count: Schema.Number,
|
|
33
33
|
hypotheses: Schema.Array(HypothesisRowSchema)
|
|
34
34
|
});
|
|
35
|
-
const HypothesisResult = Schema.Union([
|
|
35
|
+
const HypothesisResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
36
36
|
HypothesisRecordOk,
|
|
37
37
|
HypothesisValidateOk,
|
|
38
38
|
HypothesisListOk
|
|
39
|
-
]).annotate({
|
|
39
|
+
])).annotate({
|
|
40
40
|
identifier: "HypothesisResult",
|
|
41
41
|
title: "hypothesis result",
|
|
42
42
|
description: "Discriminate on `action`. record returns the new id; list returns the matching rows; validate returns an empty acknowledgement."
|
|
@@ -85,9 +85,11 @@ const HypothesisInput = Schema.Union([
|
|
|
85
85
|
ListVariant
|
|
86
86
|
]);
|
|
87
87
|
/**
|
|
88
|
-
* Handler for {@link hypothesisTool}. `record` fails
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* Handler for {@link hypothesisTool}. `record` fails with a
|
|
89
|
+
* {@link ToolRefusal} (an `isError` result naming the fix) when the binding
|
|
90
|
+
* session cannot be resolved: an unknown `tddTaskId`, an unknown fallback
|
|
91
|
+
* `sessionId`, or no session at all; `validate` does for an unknown id. Any
|
|
92
|
+
* other store failure is a defect.
|
|
91
93
|
*
|
|
92
94
|
* @public
|
|
93
95
|
*/
|
|
@@ -99,10 +101,13 @@ const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminators
|
|
|
99
101
|
let resolvedSessionId;
|
|
100
102
|
if (variant.tddTaskId !== void 0) {
|
|
101
103
|
const bound = yield* reader.getSessionByTddTaskId(variant.tddTaskId);
|
|
102
|
-
if (Option.isNone(bound)) return yield* Effect.fail(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (Option.isNone(bound)) return yield* Effect.fail(ToolRefusal.refuse(`Unknown tddTaskId ${variant.tddTaskId}: no session to attribute the hypothesis to.`, {
|
|
105
|
+
hint: "Pass the tddTaskId returned by tdd_task action='start' (tdd_task action='get' confirms it exists).",
|
|
106
|
+
suggestedTool: "tdd_task",
|
|
107
|
+
suggestedArgs: {
|
|
108
|
+
action: "get",
|
|
109
|
+
tddTaskId: variant.tddTaskId
|
|
110
|
+
}
|
|
106
111
|
}));
|
|
107
112
|
resolvedSessionId = bound.value.id;
|
|
108
113
|
} else {
|
|
@@ -115,10 +120,15 @@ const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminators
|
|
|
115
120
|
resolvedSessionId = Option.isSome(sub) ? sub.value.id : main.value.id;
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
|
-
if (resolvedSessionId === void 0) return yield* Effect.fail(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
if (resolvedSessionId === void 0) return yield* Effect.fail(ToolRefusal.refuse("No session to attribute the hypothesis to: no host session context was recovered.", {
|
|
124
|
+
hint: "Pass tddTaskId (the id returned by tdd_task action='start') to bind this hypothesis to your task's session; do not retry with a raw sessionId, and never pass tddTaskId under a sessionId key.",
|
|
125
|
+
suggestedTool: "tdd_task",
|
|
126
|
+
suggestedArgs: { action: "start" }
|
|
127
|
+
}));
|
|
128
|
+
if (resolvedSessionId === variant.sessionId && Option.isNone(yield* reader.getSessionById(resolvedSessionId))) return yield* Effect.fail(ToolRefusal.refuse(`Unknown sessionId ${resolvedSessionId}.`, {
|
|
129
|
+
hint: "Pass tddTaskId (the id returned by tdd_task action='start') instead of a raw sessionId.",
|
|
130
|
+
suggestedTool: "tdd_task",
|
|
131
|
+
suggestedArgs: { action: "start" }
|
|
122
132
|
}));
|
|
123
133
|
}
|
|
124
134
|
return {
|
|
@@ -138,7 +148,11 @@ const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminators
|
|
|
138
148
|
outcome: variant.outcome,
|
|
139
149
|
validatedAt: variant.validatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
140
150
|
...variant.validatedTurnId !== void 0 && { validatedTurnId: variant.validatedTurnId }
|
|
141
|
-
})
|
|
151
|
+
}).pipe(Effect.catchIf((error) => error.reason.startsWith("unknown hypothesis id"), () => Effect.fail(ToolRefusal.refuse(`Unknown hypothesis id ${variant.id}.`, {
|
|
152
|
+
hint: "Use the id hypothesis action='record' returned, or find it with hypothesis action='list'.",
|
|
153
|
+
suggestedTool: "hypothesis",
|
|
154
|
+
suggestedArgs: { action: "list" }
|
|
155
|
+
}))));
|
|
142
156
|
return { action: "validate" };
|
|
143
157
|
}),
|
|
144
158
|
list: (variant) => Effect.gen(function* () {
|
|
@@ -153,22 +167,18 @@ const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminators
|
|
|
153
167
|
hypotheses: rows
|
|
154
168
|
};
|
|
155
169
|
})
|
|
156
|
-
})).pipe(Effect.
|
|
170
|
+
})).pipe(Effect.catchTag("DataStoreError", Effect.die));
|
|
157
171
|
/**
|
|
158
172
|
* The Effect-native `hypothesis` tool.
|
|
159
173
|
*
|
|
160
174
|
* @public
|
|
161
175
|
*/
|
|
162
|
-
const hypothesisTool =
|
|
176
|
+
const hypothesisTool = McpToolkit.unionTool("hypothesis", {
|
|
163
177
|
description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (content, tddTaskId?, optional citation ids) writes a hypothesis — the binding session is resolved server-side from the recovered host context (active TDD subagent, else main session); pass tddTaskId (returned by tdd_task action='start') to bind deterministically to that task's session, and do not pass sessionId when recording; action='validate' (id, outcome, validatedAt?) records a validation outcome — validatedAt is optional and defaults server-side to now when omitted, or is honored verbatim when supplied; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses.",
|
|
164
178
|
parameters: HypothesisInput,
|
|
165
179
|
success: HypothesisResult,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
DataStore,
|
|
169
|
-
McpSession
|
|
170
|
-
]
|
|
171
|
-
}).annotate(Tool.Title, "Hypothesis").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
180
|
+
failure: ToolRefusal
|
|
181
|
+
}).addDependency(DataReader).addDependency(DataStore).addDependency(McpSession).annotate(Tool.Title, "Hypothesis").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
172
182
|
|
|
173
183
|
//#endregion
|
|
174
184
|
export { HypothesisInput, HypothesisResult, handleHypothesis, hypothesisTool };
|
package/tools/inventory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
|
|
2
|
+
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
2
3
|
import { Effect, Match, Option, Schema } from "effect";
|
|
3
4
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
5
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -114,7 +115,7 @@ const TagInventoryUnscoped = Schema.Struct({
|
|
|
114
115
|
*
|
|
115
116
|
* @public
|
|
116
117
|
*/
|
|
117
|
-
const InventoryResult = Schema.Union([
|
|
118
|
+
const InventoryResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
118
119
|
ProjectInventory,
|
|
119
120
|
ModuleInventory,
|
|
120
121
|
SuiteInventory,
|
|
@@ -123,7 +124,7 @@ const InventoryResult = Schema.Union([
|
|
|
123
124
|
SessionListInventory,
|
|
124
125
|
TagInventoryScoped,
|
|
125
126
|
TagInventoryUnscoped
|
|
126
|
-
]).annotate({
|
|
127
|
+
])).annotate({
|
|
127
128
|
identifier: "InventoryResult",
|
|
128
129
|
title: "inventory result",
|
|
129
130
|
description: "Discriminate on `inventoryKind`. project/module/suite carry counted lists; session_detail discriminates further on `found`; session_list returns the matching sessions; tag_scoped and tag_unscoped carry per-tag counts (the unscoped form also carries a `byProject` breakdown per tag)."
|
|
@@ -291,12 +292,12 @@ const handleInventory = (input) => Match.value(input).pipe(Match.discriminatorsE
|
|
|
291
292
|
*
|
|
292
293
|
* @public
|
|
293
294
|
*/
|
|
294
|
-
const inventoryTool =
|
|
295
|
+
const inventoryTool = McpToolkit.unionTool("inventory", {
|
|
295
296
|
description: "Use to discover what exists in the workspace, with a kind discriminator: project / module / suite / session / tag. structuredContent discriminates on `inventoryKind` (project, module, suite, session_detail, session_list, tag_scoped, tag_unscoped) so callers can branch on the response shape without parsing markdown.",
|
|
296
297
|
parameters: InventoryInput,
|
|
297
298
|
success: InventoryResult,
|
|
298
|
-
|
|
299
|
-
}).annotate(Tool.Title, "Inventory").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
299
|
+
failure: ToolRefusal
|
|
300
|
+
}).addDependency(DataReader).annotate(Tool.Title, "Inventory").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
300
301
|
|
|
301
302
|
//#endregion
|
|
302
303
|
export { InventoryInput, InventoryResult, handleInventory, inventoryTool };
|
package/tools/note.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
1
2
|
import { Effect, Match, Option, Schema } from "effect";
|
|
2
3
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
3
4
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -64,7 +65,7 @@ const NoteSearchOk = Schema.Struct({
|
|
|
64
65
|
*
|
|
65
66
|
* @public
|
|
66
67
|
*/
|
|
67
|
-
const NoteResult = Schema.Union([
|
|
68
|
+
const NoteResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
68
69
|
NoteCreateOk,
|
|
69
70
|
NoteListOk,
|
|
70
71
|
NoteGetFound,
|
|
@@ -72,7 +73,7 @@ const NoteResult = Schema.Union([
|
|
|
72
73
|
NoteUpdateOk,
|
|
73
74
|
NoteDeleteOk,
|
|
74
75
|
NoteSearchOk
|
|
75
|
-
]).annotate({
|
|
76
|
+
])).annotate({
|
|
76
77
|
identifier: "NoteResult",
|
|
77
78
|
title: "note result",
|
|
78
79
|
description: "Discriminate on `action`. `get` further discriminates on `found`."
|
|
@@ -210,12 +211,12 @@ const handleNote = (input) => Match.value(input).pipe(Match.discriminatorsExhaus
|
|
|
210
211
|
*
|
|
211
212
|
* @public
|
|
212
213
|
*/
|
|
213
|
-
const noteTool =
|
|
214
|
+
const noteTool = McpToolkit.unionTool("note", {
|
|
214
215
|
description: "Use to manage notes, with a CRUD action discriminator: action='create' writes a scoped note; action='list' (scope?, project?, testFullName?) returns matching notes; action='get' (id) returns a structured note; action='update' (id, ...patch) edits; action='delete' (id) removes; action='search' (query) does FTS5 across title and content. structuredContent always carries the typed result (discriminate on `action`).",
|
|
215
216
|
parameters: NoteParams,
|
|
216
217
|
success: NoteResult,
|
|
217
|
-
|
|
218
|
-
}).annotate(Tool.Title, "Note").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
218
|
+
failure: ToolRefusal
|
|
219
|
+
}).addDependency(DataReader).addDependency(DataStore).annotate(Tool.Title, "Note").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
219
220
|
|
|
220
221
|
//#endregion
|
|
221
222
|
export { NoteParams, NoteResult, handleNote, noteTool };
|
package/tools/overview.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolOutputSchema } from "@effected/mcp";
|
|
1
2
|
import { Effect, Option, Schema } from "effect";
|
|
2
3
|
import { DataReader } from "@vitest-agent/engine";
|
|
3
4
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -34,7 +35,7 @@ const OverviewAbsent = Schema.Struct({
|
|
|
34
35
|
*
|
|
35
36
|
* @public
|
|
36
37
|
*/
|
|
37
|
-
const TestOverviewResult = Schema.Union([OverviewAvailable, OverviewAbsent]).annotate({
|
|
38
|
+
const TestOverviewResult = ToolOutputSchema.objectRooted(Schema.Union([OverviewAvailable, OverviewAbsent])).annotate({
|
|
38
39
|
identifier: "TestOverviewResult",
|
|
39
40
|
title: "test_overview result",
|
|
40
41
|
description: "Per-project run metrics. Discriminate on `dataAvailable` for cold-start handling."
|
package/tools/ping.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
2
|
import { Tool } from "effect/unstable/ai";
|
|
3
|
+
import { CurrentDistribution, DistributionField } from "@effected/engine";
|
|
3
4
|
|
|
4
5
|
//#region src/tools/ping.ts
|
|
5
6
|
/**
|
|
@@ -7,10 +8,13 @@ import { Tool } from "effect/unstable/ai";
|
|
|
7
8
|
*
|
|
8
9
|
* @public
|
|
9
10
|
*/
|
|
10
|
-
const PingResult = Schema.Struct({
|
|
11
|
+
const PingResult = Schema.Struct({
|
|
12
|
+
message: Schema.Literal("pong").annotate({ description: "Constant `pong`. Presence confirms the MCP server responded." }),
|
|
13
|
+
distribution: DistributionField.annotate({ description: "The package that shipped this bin (`{ name, version }`, e.g. `@vitest-agent/plugin`), or null when `@vitest-agent/mcp` was launched directly. Packaging provenance, not a version to compare." })
|
|
14
|
+
}).annotate({
|
|
11
15
|
identifier: "PingResult",
|
|
12
16
|
title: "ping result",
|
|
13
|
-
description: "Liveness probe
|
|
17
|
+
description: "Liveness probe: the constant `pong`, plus the distribution the server was launched through."
|
|
14
18
|
});
|
|
15
19
|
/**
|
|
16
20
|
* The Effect-native `ping` tool. No parameters (the default
|
|
@@ -20,7 +24,7 @@ const PingResult = Schema.Struct({ message: Schema.Literal("pong").annotate({ de
|
|
|
20
24
|
* @public
|
|
21
25
|
*/
|
|
22
26
|
const pingTool = Tool.make("ping", {
|
|
23
|
-
description: "Ping the MCP server — returns 'pong'. Used to verify hot-patch reload.",
|
|
27
|
+
description: "Ping the MCP server — returns 'pong' and the distribution (carrier package) it was launched through. Used to verify hot-patch reload.",
|
|
24
28
|
success: PingResult
|
|
25
29
|
}).annotate(Tool.Title, "Ping").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
26
30
|
/**
|
|
@@ -28,7 +32,13 @@ const pingTool = Tool.make("ping", {
|
|
|
28
32
|
*
|
|
29
33
|
* @public
|
|
30
34
|
*/
|
|
31
|
-
const handlePing = () => Effect.
|
|
35
|
+
const handlePing = () => Effect.gen(function* () {
|
|
36
|
+
const distribution = yield* CurrentDistribution;
|
|
37
|
+
return {
|
|
38
|
+
message: "pong",
|
|
39
|
+
distribution: Option.getOrNull(distribution)
|
|
40
|
+
};
|
|
41
|
+
});
|
|
32
42
|
|
|
33
43
|
//#endregion
|
|
34
44
|
export { PingResult, handlePing, pingTool };
|
package/tools/register-agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolOutputSchema } from "@effected/mcp";
|
|
1
2
|
import { Effect, Option, Schema } from "effect";
|
|
2
3
|
import { DataReader, DataStore, deriveIdempotencyKey } from "@vitest-agent/engine";
|
|
3
4
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -75,7 +76,7 @@ const RegisterAgentFailure = Schema.Struct({
|
|
|
75
76
|
*
|
|
76
77
|
* @public
|
|
77
78
|
*/
|
|
78
|
-
const RegisterAgentResult = Schema.Union([RegisterAgentSuccess, RegisterAgentFailure]).annotate({
|
|
79
|
+
const RegisterAgentResult = ToolOutputSchema.objectRooted(Schema.Union([RegisterAgentSuccess, RegisterAgentFailure])).annotate({
|
|
79
80
|
identifier: "RegisterAgentResult",
|
|
80
81
|
title: "register_agent result",
|
|
81
82
|
description: "Discriminate on `ok`. The four failure codes are documented per their `code` literal."
|
package/tools/run-tests.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpSession } from "../session.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { ToolOutputSchema } from "@effected/mcp";
|
|
3
4
|
import { Data, Effect, Schema, Semaphore } from "effect";
|
|
4
5
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
6
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -64,12 +65,12 @@ const RunTestsNoMatch = Schema.Struct({
|
|
|
64
65
|
*
|
|
65
66
|
* @public
|
|
66
67
|
*/
|
|
67
|
-
const RunTestsResult = Schema.Union([
|
|
68
|
+
const RunTestsResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
68
69
|
RunTestsOk,
|
|
69
70
|
RunTestsTimeout,
|
|
70
71
|
RunTestsError,
|
|
71
72
|
RunTestsNoMatch
|
|
72
|
-
]).annotate({
|
|
73
|
+
])).annotate({
|
|
73
74
|
identifier: "RunTestsResult",
|
|
74
75
|
title: "run_tests result",
|
|
75
76
|
description: "Discriminate on `kind`. ok carries the full AgentReport plus per-test classifications; timeout / error are the two failure modes; no-match indicates that the resolved filter set matched zero test cases."
|
|
@@ -420,13 +421,22 @@ const makeBestEffortFork = (services) => (effect) => Effect.runFork(Effect.provi
|
|
|
420
421
|
* `{ kind: "error" }` envelope, so it never rejects.
|
|
421
422
|
*/
|
|
422
423
|
const runTestsBody = async (input, ctx) => {
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
let files;
|
|
425
|
+
let project;
|
|
425
426
|
const tagsInput = input.tags;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (tagsInput
|
|
427
|
+
try {
|
|
428
|
+
files = input.files ? sanitizeTestArgs(input.files) : [];
|
|
429
|
+
project = input.project ? sanitizeTestArgs([input.project])[0] : void 0;
|
|
430
|
+
if (tagsInput) {
|
|
431
|
+
if (tagsInput.all) sanitizeTestArgs(tagsInput.all);
|
|
432
|
+
if (tagsInput.any) sanitizeTestArgs(tagsInput.any);
|
|
433
|
+
if (tagsInput.none) sanitizeTestArgs(tagsInput.none);
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return {
|
|
437
|
+
kind: "error",
|
|
438
|
+
message: err instanceof Error ? err.message : String(err)
|
|
439
|
+
};
|
|
430
440
|
}
|
|
431
441
|
const resolvedExpression = composeTagExpression(tagsInput ?? null);
|
|
432
442
|
const hasFilter = files.length > 0 || project !== void 0 || resolvedExpression !== null;
|
package/tools/status.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolOutputSchema } from "@effected/mcp";
|
|
1
2
|
import { Effect, Option, Schema } from "effect";
|
|
2
3
|
import { DataReader } from "@vitest-agent/engine";
|
|
3
4
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -20,7 +21,7 @@ const StatusAbsent = Schema.Struct({
|
|
|
20
21
|
*
|
|
21
22
|
* @public
|
|
22
23
|
*/
|
|
23
|
-
const TestStatusResult = Schema.Union([StatusAvailable, StatusAbsent]).annotate({
|
|
24
|
+
const TestStatusResult = ToolOutputSchema.objectRooted(Schema.Union([StatusAvailable, StatusAbsent])).annotate({
|
|
24
25
|
identifier: "TestStatusResult",
|
|
25
26
|
title: "test_status result",
|
|
26
27
|
description: "Per-project last-run summary. Discriminate on `dataAvailable` for cold-start handling."
|
package/tools/tdd-behavior.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
2
|
import { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
|
|
3
|
+
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
3
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
4
5
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
6
|
import { Tool } from "effect/unstable/ai";
|
|
6
7
|
import { BehaviorDetail, BehaviorRow } from "@vitest-agent/sdk";
|
|
8
|
+
import { Remediation } from "@effected/engine";
|
|
7
9
|
|
|
8
10
|
//#region src/tools/tdd-behavior.ts
|
|
9
11
|
/**
|
|
@@ -25,11 +27,7 @@ const TddBehaviorErrorEnvelope = Schema.Struct({
|
|
|
25
27
|
ok: Schema.Literal(false).annotate({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
|
|
26
28
|
error: Schema.StructWithRest(Schema.Struct({
|
|
27
29
|
_tag: Schema.String.annotate({ description: "Tagged error name (e.g. BehaviorNotFoundError, GoalNotFoundError)." }),
|
|
28
|
-
remediation:
|
|
29
|
-
suggestedTool: Schema.String,
|
|
30
|
-
suggestedArgs: Schema.Record(Schema.String, Schema.Unknown),
|
|
31
|
-
humanHint: Schema.String
|
|
32
|
-
}).annotate({ description: "Suggested next action: the tool to call, its arguments, and a plain-language hint." })
|
|
30
|
+
remediation: Remediation.annotate({ description: "Suggested next action: a plain-language hint, the tool to call, and its arguments." })
|
|
33
31
|
}), [Schema.Record(Schema.String, Schema.Unknown)])
|
|
34
32
|
}).annotate({ identifier: "TddBehaviorErrorEnvelope" });
|
|
35
33
|
const TddBehaviorCreateOk = Schema.Struct({
|
|
@@ -70,7 +68,7 @@ const TddBehaviorListByTddTaskOk = Schema.Struct({
|
|
|
70
68
|
tddTaskId: Schema.Number,
|
|
71
69
|
behaviors: Schema.Array(BehaviorRow)
|
|
72
70
|
});
|
|
73
|
-
const TddBehaviorResult = Schema.Union([
|
|
71
|
+
const TddBehaviorResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
74
72
|
TddBehaviorCreateOk,
|
|
75
73
|
TddBehaviorUpdateOk,
|
|
76
74
|
TddBehaviorDeleteOk,
|
|
@@ -79,7 +77,7 @@ const TddBehaviorResult = Schema.Union([
|
|
|
79
77
|
TddBehaviorListByGoalOk,
|
|
80
78
|
TddBehaviorListByTddTaskOk,
|
|
81
79
|
TddBehaviorErrorEnvelope
|
|
82
|
-
]).annotate({
|
|
80
|
+
])).annotate({
|
|
83
81
|
identifier: "TddBehaviorResult",
|
|
84
82
|
title: "tdd_behavior result",
|
|
85
83
|
description: "Discriminate on `action` (or `ok=false` for the tagged-error envelope)."
|
|
@@ -210,12 +208,12 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
210
208
|
*
|
|
211
209
|
* @public
|
|
212
210
|
*/
|
|
213
|
-
const tddBehaviorTool =
|
|
211
|
+
const tddBehaviorTool = McpToolkit.unionTool("tdd_behavior", {
|
|
214
212
|
description: "Use to manage TDD behaviors, with a CRUD action discriminator: action='create' (goalId, behavior, suggestedTestName?, dependsOnBehaviorIds?) is idempotent on (goalId, behavior); action='update' (id, ...patch) edits; action='delete' (id) hard-deletes; action='get' (id) reads; action='list_by_goal' (goalId) lists one goal's behaviors; action='list_by_tdd_task' (tddTaskId) lists across all goals.",
|
|
215
213
|
parameters: TddBehaviorInput,
|
|
216
214
|
success: TddBehaviorResult,
|
|
217
|
-
|
|
218
|
-
}).annotate(Tool.Title, "TDD behavior").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
215
|
+
failure: ToolRefusal
|
|
216
|
+
}).addDependency(DataReader).addDependency(DataStore).annotate(Tool.Title, "TDD behavior").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
219
217
|
|
|
220
218
|
//#endregion
|
|
221
219
|
export { TddBehaviorInput, TddBehaviorResult, handleTddBehavior, tddBehaviorTool };
|
package/tools/tdd-goal.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
2
|
import { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
|
|
3
|
+
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
3
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
4
5
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
6
|
import { Tool } from "effect/unstable/ai";
|
|
6
7
|
import { GoalDetail, GoalRow } from "@vitest-agent/sdk";
|
|
8
|
+
import { Remediation } from "@effected/engine";
|
|
7
9
|
|
|
8
10
|
//#region src/tools/tdd-goal.ts
|
|
9
11
|
/**
|
|
@@ -25,11 +27,7 @@ const TddErrorEnvelope = Schema.Struct({
|
|
|
25
27
|
ok: Schema.Literal(false).annotate({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
|
|
26
28
|
error: Schema.StructWithRest(Schema.Struct({
|
|
27
29
|
_tag: Schema.String.annotate({ description: "Tagged error name (e.g. GoalNotFoundError, TddTaskNotFoundError)." }),
|
|
28
|
-
remediation:
|
|
29
|
-
suggestedTool: Schema.String,
|
|
30
|
-
suggestedArgs: Schema.Record(Schema.String, Schema.Unknown),
|
|
31
|
-
humanHint: Schema.String
|
|
32
|
-
}).annotate({ description: "Suggested next action: the tool to call, its arguments, and a plain-language hint." })
|
|
30
|
+
remediation: Remediation.annotate({ description: "Suggested next action: a plain-language hint, the tool to call, and its arguments." })
|
|
33
31
|
}), [Schema.Record(Schema.String, Schema.Unknown)])
|
|
34
32
|
}).annotate({ identifier: "TddErrorEnvelope" });
|
|
35
33
|
const TddGoalCreateOk = Schema.Struct({
|
|
@@ -64,7 +62,7 @@ const TddGoalListOk = Schema.Struct({
|
|
|
64
62
|
tddTaskId: Schema.Number,
|
|
65
63
|
goals: Schema.Array(GoalDetail).annotate({ description: "All goals for the TDD task, with their behaviors." })
|
|
66
64
|
}).annotate({ identifier: "TddGoalListOk" });
|
|
67
|
-
const TddGoalResult = Schema.Union([
|
|
65
|
+
const TddGoalResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
68
66
|
TddGoalCreateOk,
|
|
69
67
|
TddGoalUpdateOk,
|
|
70
68
|
TddGoalDeleteOk,
|
|
@@ -72,7 +70,7 @@ const TddGoalResult = Schema.Union([
|
|
|
72
70
|
TddGoalGetMissing,
|
|
73
71
|
TddGoalListOk,
|
|
74
72
|
TddErrorEnvelope
|
|
75
|
-
]).annotate({
|
|
73
|
+
])).annotate({
|
|
76
74
|
identifier: "TddGoalResult",
|
|
77
75
|
title: "tdd_goal result",
|
|
78
76
|
description: "Discriminate first on `action` (or on `ok=false` for the tagged-error envelope). Get returns `found:true|false`; create/update/delete/list return `ok:true` on success."
|
|
@@ -178,12 +176,12 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
178
176
|
*
|
|
179
177
|
* @public
|
|
180
178
|
*/
|
|
181
|
-
const tddGoalTool =
|
|
179
|
+
const tddGoalTool = McpToolkit.unionTool("tdd_goal", {
|
|
182
180
|
description: "Use to manage TDD goals, with a CRUD action discriminator: action='create' (tddTaskId, goal) is idempotent on (tddTaskId, goal); action='update' (id, goal?, status?) edits text and/or lifecycle status; action='delete' (id) hard-deletes (prefer status:'abandoned'); action='get' (id) reads with nested behaviors; action='list' (tddTaskId) returns all goals for a TDD task.",
|
|
183
181
|
parameters: TddGoalInput,
|
|
184
182
|
success: TddGoalResult,
|
|
185
|
-
|
|
186
|
-
}).annotate(Tool.Title, "TDD goal").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
183
|
+
failure: ToolRefusal
|
|
184
|
+
}).addDependency(DataReader).addDependency(DataStore).annotate(Tool.Title, "TDD goal").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
187
185
|
|
|
188
186
|
//#endregion
|
|
189
187
|
export { TddGoalInput, TddGoalResult, handleTddGoal, tddGoalTool };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ToolOutputSchema } from "@effected/mcp";
|
|
1
2
|
import { Effect, Option, Schema } from "effect";
|
|
2
3
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
3
4
|
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validatePhaseTransition } from "@vitest-agent/sdk";
|
|
6
|
+
import { Remediation } from "@effected/engine";
|
|
5
7
|
|
|
6
8
|
//#region src/tools/tdd-phase-transition-request.ts
|
|
7
9
|
/**
|
|
@@ -45,11 +47,7 @@ const denialReasonLiteral = Schema.Literals([
|
|
|
45
47
|
"evidence_not_for_behavior",
|
|
46
48
|
"evidence_test_was_already_failing"
|
|
47
49
|
]);
|
|
48
|
-
const RemediationSchema =
|
|
49
|
-
suggestedTool: Schema.String.annotate({ description: "Next tool the agent should call to make progress." }),
|
|
50
|
-
suggestedArgs: Schema.Record(Schema.String, Schema.Unknown).annotate({ description: "Concrete arguments for `suggestedTool` that fix the underlying issue." }),
|
|
51
|
-
humanHint: Schema.String.annotate({ description: "Plain-language explanation of what to do next." })
|
|
52
|
-
}).annotate({ identifier: "PhaseTransitionRemediation" });
|
|
50
|
+
const RemediationSchema = Remediation.annotate({ description: "Suggested next action: `hint` explains what to do, `suggestedTool` is the tool to call next, and `suggestedArgs` its arguments." });
|
|
53
51
|
const PhaseTransitionAccepted = Schema.Struct({
|
|
54
52
|
accepted: Schema.Literal(true).annotate({ description: "Discriminant — `true` when the transition was granted." }),
|
|
55
53
|
phase: phaseLiteral.annotate({ description: "Phase the session is now in (echo of `requestedPhase`)." }),
|
|
@@ -74,7 +72,7 @@ const PhaseTransitionDenied = Schema.Struct({
|
|
|
74
72
|
*
|
|
75
73
|
* @public
|
|
76
74
|
*/
|
|
77
|
-
const PhaseTransitionResult = Schema.Union([PhaseTransitionAccepted, PhaseTransitionDenied]).annotate({
|
|
75
|
+
const PhaseTransitionResult = ToolOutputSchema.objectRooted(Schema.Union([PhaseTransitionAccepted, PhaseTransitionDenied])).annotate({
|
|
78
76
|
identifier: "PhaseTransitionResult",
|
|
79
77
|
title: "tdd_phase_transition_request result",
|
|
80
78
|
description: "Discriminate on `accepted`. Acceptance carries the new phaseId; denial carries a typed reason and a remediation pointer."
|
|
@@ -124,7 +122,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
124
122
|
action: "list",
|
|
125
123
|
tddTaskId: input.tddTaskId
|
|
126
124
|
},
|
|
127
|
-
|
|
125
|
+
hint: `No tdd_session_goals row with id=${input.goalId}. Call tdd_goal({ action: "list" }) to find the correct goal id.`
|
|
128
126
|
}
|
|
129
127
|
};
|
|
130
128
|
if (goalOpt.value.sessionId !== input.tddTaskId) return {
|
|
@@ -137,7 +135,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
137
135
|
action: "list",
|
|
138
136
|
tddTaskId: input.tddTaskId
|
|
139
137
|
},
|
|
140
|
-
|
|
138
|
+
hint: `Goal id=${input.goalId} belongs to TDD task ${goalOpt.value.sessionId}, not the requested tddTaskId=${input.tddTaskId}. Pass the tddTaskId of the goal's parent task, or pick a goal that belongs to the active task.`
|
|
141
139
|
}
|
|
142
140
|
};
|
|
143
141
|
if (goalOpt.value.status !== "in_progress") return {
|
|
@@ -150,7 +148,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
150
148
|
id: input.goalId,
|
|
151
149
|
status: "in_progress"
|
|
152
150
|
},
|
|
153
|
-
|
|
151
|
+
hint: `Goal id=${input.goalId} has status '${goalOpt.value.status}'. Phase transitions require the goal to be in_progress. Call tdd_goal_update({status:'in_progress'}) before requesting transitions.`
|
|
154
152
|
}
|
|
155
153
|
};
|
|
156
154
|
if (input.behaviorId !== void 0) {
|
|
@@ -165,7 +163,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
165
163
|
scope: "goal",
|
|
166
164
|
goalId: input.goalId
|
|
167
165
|
},
|
|
168
|
-
|
|
166
|
+
hint: `No tdd_session_behaviors row with id=${input.behaviorId}. Call tdd_behavior_list to find the correct behavior id.`
|
|
169
167
|
}
|
|
170
168
|
};
|
|
171
169
|
if (behaviorOpt.value.goalId !== input.goalId) return {
|
|
@@ -175,7 +173,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
175
173
|
remediation: {
|
|
176
174
|
suggestedTool: "tdd_behavior_get",
|
|
177
175
|
suggestedArgs: { id: input.behaviorId },
|
|
178
|
-
|
|
176
|
+
hint: `Behavior id=${input.behaviorId} belongs to goal ${behaviorOpt.value.goalId}, not the requested goalId=${input.goalId}. Pass the goalId of the behavior's parent goal.`
|
|
179
177
|
}
|
|
180
178
|
};
|
|
181
179
|
}
|
|
@@ -215,7 +213,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
215
213
|
remediation: {
|
|
216
214
|
suggestedTool: "run_tests",
|
|
217
215
|
suggestedArgs: {},
|
|
218
|
-
|
|
216
|
+
hint: crossSessionCount > 0 ? `${baseHint} ${crossSessionCount} artifact${crossSessionCount === 1 ? " was" : "s were"} recorded under a different session of this conversation in the last ${DIAGNOSTIC_WINDOW_MINUTES} minutes — the subagent's hooks may be attributing to a detached session; set VITEST_AGENT_TDD_TASK_ID=${input.tddTaskId} in the subagent's environment or dispatch it as an unnamed background subagent.` : baseHint
|
|
219
217
|
}
|
|
220
218
|
};
|
|
221
219
|
}
|
|
@@ -231,7 +229,7 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
231
229
|
remediation: {
|
|
232
230
|
suggestedTool: "run_tests",
|
|
233
231
|
suggestedArgs: {},
|
|
234
|
-
|
|
232
|
+
hint: `Cited artifact id ${resolvedArtifactId} does not exist. Artifacts are recorded by hooks observing your tool calls (Decision D7), so run the test (e.g. via the run_tests MCP tool) or make the file edit first; the post-tool-use hook will write the matching tdd_artifacts row and return its id, which can then be cited here.`
|
|
235
233
|
}
|
|
236
234
|
};
|
|
237
235
|
citedArtifact = artifactOpt.value;
|
|
@@ -257,7 +255,16 @@ const handlePhaseTransitionRequest = (input) => Effect.gen(function* () {
|
|
|
257
255
|
cited_artifact: citedArtifact,
|
|
258
256
|
requested_behavior_id: input.behaviorId ?? null
|
|
259
257
|
});
|
|
260
|
-
if (!result.accepted)
|
|
258
|
+
if (!result.accepted) {
|
|
259
|
+
const { humanHint, ...remediation } = result.remediation;
|
|
260
|
+
return {
|
|
261
|
+
...result,
|
|
262
|
+
remediation: {
|
|
263
|
+
...remediation,
|
|
264
|
+
hint: humanHint
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
261
268
|
const out = yield* store.writeTddPhase({
|
|
262
269
|
tddTaskId: input.tddTaskId,
|
|
263
270
|
phase: result.phase,
|