@vitest-agent/mcp 1.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/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/vitest-agent-mcp.js +93 -0
- package/context.js +72 -0
- package/index.d.ts +1577 -0
- package/index.js +19 -0
- package/layers/McpLive.js +30 -0
- package/middleware/idempotency.js +128 -0
- package/package.json +58 -0
- package/prompts/explain-failure.js +27 -0
- package/prompts/index.js +89 -0
- package/prompts/regression-since-pass.js +28 -0
- package/prompts/tdd-resume.js +28 -0
- package/prompts/triage.js +24 -0
- package/prompts/why-flaky.js +30 -0
- package/prompts/wrapup.js +19 -0
- package/resources/index.js +155 -0
- package/resources/indexes.js +77 -0
- package/resources/manifest-schema.js +46 -0
- package/resources/paths.js +20 -0
- package/resources/patterns.js +22 -0
- package/resources/upstream-docs.js +22 -0
- package/router.js +74 -0
- package/server.js +838 -0
- package/tools/_tdd-error-envelope.js +98 -0
- package/tools/acceptance-metrics.js +75 -0
- package/tools/cache-health.js +83 -0
- package/tools/commit-changes.js +64 -0
- package/tools/configure.js +107 -0
- package/tools/coverage.js +76 -0
- package/tools/errors.js +151 -0
- package/tools/failure-signature-get.js +73 -0
- package/tools/file-coverage.js +106 -0
- package/tools/help.js +146 -0
- package/tools/history.js +121 -0
- package/tools/hypothesis.js +127 -0
- package/tools/inventory.js +377 -0
- package/tools/note.js +208 -0
- package/tools/overview.js +92 -0
- package/tools/ping.js +22 -0
- package/tools/register-agent.js +135 -0
- package/tools/run-tests.js +359 -0
- package/tools/settings-list.js +48 -0
- package/tools/status.js +74 -0
- package/tools/tdd-artifact.js +101 -0
- package/tools/tdd-behavior.js +177 -0
- package/tools/tdd-goal.js +147 -0
- package/tools/tdd-phase-transition-request.js +212 -0
- package/tools/tdd-task.js +278 -0
- package/tools/test.js +281 -0
- package/tools/trends.js +112 -0
- package/tools/triage-brief.js +42 -0
- package/tools/turn-search.js +60 -0
- package/tools/wrapup-prompt.js +49 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/effect-to-zod.js +81 -0
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createCallerFactory, createCurrentSessionIdRef, createSessionContextRef } from "./context.js";
|
|
2
|
+
import { McpLive } from "./layers/McpLive.js";
|
|
3
|
+
import { appRouter } from "./router.js";
|
|
4
|
+
import { startMcpServer } from "./server.js";
|
|
5
|
+
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* The version of this package, inlined at build time from
|
|
9
|
+
* package.json#version via rslib-builder's __PACKAGE_VERSION__ substitution.
|
|
10
|
+
* Compared against CURRENT_SDK_VERSION at MCP bin init to surface
|
|
11
|
+
* partially-upgraded installs as a single stderr warning. See the root
|
|
12
|
+
* CLAUDE.md "Cross-package version drift" section.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
const CURRENT_MCP_VERSION = "1.0.0";
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { CURRENT_MCP_VERSION, McpLive, appRouter, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, startMcpServer };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NodeFileSystem } from "@effect/platform-node";
|
|
2
|
+
import * as NodeContext$1 from "@effect/platform-node/NodeContext";
|
|
3
|
+
import { layer } from "@effect/sql-sqlite-node/SqliteClient";
|
|
4
|
+
import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
|
|
5
|
+
import { DataReaderLive, DataStoreLive, LoggerLive, OutputPipelineLive, ProjectDiscoveryLive, migration0001 } from "@vitest-agent/sdk";
|
|
6
|
+
import { Layer } from "effect";
|
|
7
|
+
|
|
8
|
+
//#region src/layers/McpLive.ts
|
|
9
|
+
/**
|
|
10
|
+
* Builds the Effect Layer that provides all services required by the MCP server.
|
|
11
|
+
*
|
|
12
|
+
* Composes DataReader, DataStore, ProjectDiscovery, OutputPipeline, SQLite
|
|
13
|
+
* client, migrator, NodeContext, NodeFileSystem, and the logger into a single
|
|
14
|
+
* layer suitable for `ManagedRuntime.make`.
|
|
15
|
+
*
|
|
16
|
+
* @param dbPath - absolute path to the SQLite database file
|
|
17
|
+
* @param logLevel - optional log level; defaults to the logger's own default
|
|
18
|
+
* @param logFile - optional path to write structured log output
|
|
19
|
+
* @returns an Effect Layer providing all MCP runtime services
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const McpLive = (dbPath, logLevel, logFile) => {
|
|
23
|
+
const SqliteLayer = layer({ filename: dbPath });
|
|
24
|
+
const PlatformLayer = NodeContext$1.layer;
|
|
25
|
+
const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": migration0001 }) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
|
|
26
|
+
return Layer.mergeAll(DataReaderLive, DataStoreLive, ProjectDiscoveryLive, OutputPipelineLive).pipe(Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { McpLive };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { middleware, publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader, DataStore } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/middleware/idempotency.ts
|
|
6
|
+
/**
|
|
7
|
+
* Registered idempotency specs for mutation procedures.
|
|
8
|
+
*
|
|
9
|
+
* `hypothesis validate` is covered (key: `validate:${id}:${outcome}`).
|
|
10
|
+
* `hypothesis record` is deliberately not covered — see the note in the
|
|
11
|
+
* hypothesis spec below.
|
|
12
|
+
*
|
|
13
|
+
* Add an entry here whenever a new idempotent mutation is introduced.
|
|
14
|
+
*/
|
|
15
|
+
const idempotencyKeys = [
|
|
16
|
+
{
|
|
17
|
+
procedurePath: "hypothesis",
|
|
18
|
+
deriveKey: (input) => {
|
|
19
|
+
if (input === null || typeof input !== "object" || !("action" in input)) return null;
|
|
20
|
+
const i = input;
|
|
21
|
+
if (i.action === "validate" && typeof i.id === "number" && typeof i.outcome === "string") return `validate:${i.id}:${i.outcome}`;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
procedurePath: "_legacy_hypothesis_validate",
|
|
27
|
+
deriveKey: (input) => {
|
|
28
|
+
if (input !== null && typeof input === "object" && "id" in input && "outcome" in input && typeof input.id === "number" && typeof input.outcome === "string") {
|
|
29
|
+
const i = input;
|
|
30
|
+
return `${i.id}:${i.outcome}`;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
procedurePath: "tdd_task",
|
|
37
|
+
deriveKey: (input) => {
|
|
38
|
+
if (input === null || typeof input !== "object" || !("action" in input)) return null;
|
|
39
|
+
const i = input;
|
|
40
|
+
if (i.action === "start" && typeof i.goal === "string") {
|
|
41
|
+
if (typeof i.runId === "string") {
|
|
42
|
+
if (typeof i.sessionId === "number") return `start:sid:${i.sessionId}:run:${i.runId}`;
|
|
43
|
+
if (typeof i.chatId === "string") return `start:chat:${i.chatId}:run:${i.runId}`;
|
|
44
|
+
}
|
|
45
|
+
if (typeof i.sessionId === "number") return `start:sid:${i.sessionId}:${i.goal}`;
|
|
46
|
+
if (typeof i.chatId === "string") return `start:chat:${i.chatId}:${i.goal}`;
|
|
47
|
+
}
|
|
48
|
+
if (i.action === "end" && typeof i.tddTaskId === "number" && typeof i.outcome === "string") return `end:${i.tddTaskId}:${i.outcome}`;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
procedurePath: "tdd_goal",
|
|
54
|
+
deriveKey: (input) => {
|
|
55
|
+
if (input === null || typeof input !== "object" || !("action" in input)) return null;
|
|
56
|
+
const i = input;
|
|
57
|
+
if (i.action === "create" && typeof i.tddTaskId === "number" && typeof i.goal === "string") return `create:${i.tddTaskId}:${i.goal}`;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
procedurePath: "tdd_behavior",
|
|
63
|
+
deriveKey: (input) => {
|
|
64
|
+
if (input === null || typeof input !== "object" || !("action" in input)) return null;
|
|
65
|
+
const i = input;
|
|
66
|
+
if (i.action === "create" && typeof i.goalId === "number" && typeof i.behavior === "string") return `create:${i.goalId}:${i.behavior}`;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
/** Lookup table keyed by procedure path for O(1) spec retrieval. */
|
|
72
|
+
const keySpecByPath = new Map(idempotencyKeys.map((s) => [s.procedurePath, s]));
|
|
73
|
+
/**
|
|
74
|
+
* tRPC middleware that caches mutation results in `mcp_idempotent_responses`.
|
|
75
|
+
*
|
|
76
|
+
* On every mutation call where the procedure path has a registered
|
|
77
|
+
* `IdempotencyKeySpec`:
|
|
78
|
+
*
|
|
79
|
+
* 1. Derive the idempotency key from the raw input.
|
|
80
|
+
* 2. Look up `DataReader.findIdempotentResponse(path, key)`.
|
|
81
|
+
* - Cache HIT → return the stored JSON result immediately (no handler).
|
|
82
|
+
* - Cache MISS → call `next()` to run the handler, then persist the result
|
|
83
|
+
* via `DataStore.recordIdempotentResponse`. Persistence errors are
|
|
84
|
+
* swallowed (best-effort) so a transient DB failure doesn't surface to
|
|
85
|
+
* the caller as a tool error.
|
|
86
|
+
*
|
|
87
|
+
* Procedures without a registered spec, or where key derivation returns
|
|
88
|
+
* `null`, pass straight through to `next()` without any caching.
|
|
89
|
+
*/
|
|
90
|
+
const idempotent = middleware(async (opts) => {
|
|
91
|
+
const { ctx, path, type, getRawInput, next } = opts;
|
|
92
|
+
if (type !== "mutation") return next();
|
|
93
|
+
const spec = keySpecByPath.get(path);
|
|
94
|
+
if (!spec) return next();
|
|
95
|
+
const rawInput = await getRawInput();
|
|
96
|
+
const key = spec.deriveKey(rawInput);
|
|
97
|
+
if (key === null) return next();
|
|
98
|
+
const cached = await ctx.runtime.runPromise(Effect.gen(function* () {
|
|
99
|
+
return yield* (yield* DataReader).findIdempotentResponse(path, key);
|
|
100
|
+
}));
|
|
101
|
+
if (Option.isSome(cached)) {
|
|
102
|
+
const parsed = JSON.parse(cached.value);
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
data: parsed !== null && typeof parsed === "object" ? {
|
|
106
|
+
...parsed,
|
|
107
|
+
_idempotentReplay: true
|
|
108
|
+
} : parsed,
|
|
109
|
+
marker: "middlewareMarker",
|
|
110
|
+
ctx
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const result = await next();
|
|
114
|
+
if (result.ok) await ctx.runtime.runPromise(Effect.gen(function* () {
|
|
115
|
+
yield* (yield* DataStore).recordIdempotentResponse({
|
|
116
|
+
procedurePath: path,
|
|
117
|
+
key,
|
|
118
|
+
resultJson: JSON.stringify(result.data),
|
|
119
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
120
|
+
});
|
|
121
|
+
}).pipe(Effect.orElseSucceed(() => void 0)));
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
/** Drop-in replacement for `publicProcedure` on idempotent mutations. */
|
|
125
|
+
const idempotentProcedure = publicProcedure.use(idempotent);
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
export { idempotentProcedure };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitest-agent/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Model Context Protocol server for vitest-agent. Exposes 53 tools for agent access to test data, TDD lifecycle, and session management.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vitest",
|
|
8
|
+
"agent",
|
|
9
|
+
"mcp",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"claude"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/spencerbeggs/vitest-agent#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/spencerbeggs/vitest-agent/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/spencerbeggs/vitest-agent.git",
|
|
20
|
+
"directory": "packages/mcp"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "C. Spencer Beggs",
|
|
25
|
+
"email": "spencer@beggs.codes",
|
|
26
|
+
"url": "https://spencerbeg.gs"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./index.d.ts",
|
|
32
|
+
"import": "./index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"vitest-agent-mcp": "bin/vitest-agent-mcp.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@effect/cluster": "^0.59.0",
|
|
41
|
+
"@effect/platform": "^0.96.2",
|
|
42
|
+
"@effect/platform-node": "^0.107.0",
|
|
43
|
+
"@effect/rpc": "^0.75.1",
|
|
44
|
+
"@effect/sql": "^0.51.1",
|
|
45
|
+
"@effect/sql-sqlite-node": "^0.52.0",
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"@trpc/server": "^11.18.0",
|
|
48
|
+
"@vitest-agent/sdk": "1.0.0",
|
|
49
|
+
"effect": "^3.21.4",
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"vitest": "^4.1.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=24.11.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/prompts/explain-failure.ts
|
|
2
|
+
function explainFailurePrompt(args) {
|
|
3
|
+
return { messages: [{
|
|
4
|
+
role: "user",
|
|
5
|
+
content: {
|
|
6
|
+
type: "text",
|
|
7
|
+
text: [
|
|
8
|
+
`Explain the failure class identified by signature \`${args.signature}\`.`,
|
|
9
|
+
"",
|
|
10
|
+
"Steps:",
|
|
11
|
+
"",
|
|
12
|
+
`1. Call \`failure_signature_get\` with \`signature: "${args.signature}"\` to fetch the recurrence history (occurrence_count, first_seen_at, last_seen_at) and up to 10 recent test_errors with the same signature.`,
|
|
13
|
+
"2. Read the diffs and stack frames across the recent occurrences. The signature is stable across line shifts — same shape, possibly different line numbers — so recurrences are meaningful.",
|
|
14
|
+
"3. Distinguish:",
|
|
15
|
+
" - **New instance of an old class** — the same kind of bug has appeared before; this occurrence adds to a known pattern.",
|
|
16
|
+
" - **Fresh evidence** — the signature is recent (low occurrence_count, last_seen_at near first_seen_at) and represents a new bug class.",
|
|
17
|
+
"",
|
|
18
|
+
"Synthesize the root cause as a single explanation that accounts for every recent occurrence. If the occurrences disagree on root cause, the signature is too coarse — note this and flag the divergent occurrence ids.",
|
|
19
|
+
"",
|
|
20
|
+
"If a fix is obvious, record a hypothesis via `hypothesis_record` citing one of the test_error_ids as evidence. If a fix requires more investigation, note the missing data instead of guessing."
|
|
21
|
+
].join("\n")
|
|
22
|
+
}
|
|
23
|
+
}] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { explainFailurePrompt };
|
package/prompts/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { explainFailurePrompt } from "./explain-failure.js";
|
|
2
|
+
import { regressionSincePassPrompt } from "./regression-since-pass.js";
|
|
3
|
+
import { tddResumePrompt } from "./tdd-resume.js";
|
|
4
|
+
import { triagePrompt } from "./triage.js";
|
|
5
|
+
import { whyFlakyPrompt } from "./why-flaky.js";
|
|
6
|
+
import { wrapupPrompt } from "./wrapup.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
//#region src/prompts/index.ts
|
|
10
|
+
function toMessages(messages) {
|
|
11
|
+
return messages.map((m) => ({
|
|
12
|
+
role: m.role,
|
|
13
|
+
content: {
|
|
14
|
+
type: "text",
|
|
15
|
+
text: m.content.text
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function registerAllPrompts(server) {
|
|
20
|
+
server.registerPrompt("triage", {
|
|
21
|
+
title: "Triage Recent Failures",
|
|
22
|
+
description: "Orient toward a triage workflow over the most recent test run; compose triage_brief, failure_signature_get, hypothesis_record.",
|
|
23
|
+
argsSchema: { project: z.optional(z.string()).describe("Filter to a specific project") }
|
|
24
|
+
}, (args) => {
|
|
25
|
+
return { messages: toMessages(triagePrompt(args.project !== void 0 ? { project: args.project } : {}).messages) };
|
|
26
|
+
});
|
|
27
|
+
server.registerPrompt("why-flaky", {
|
|
28
|
+
title: "Diagnose a Flaky Test",
|
|
29
|
+
description: "Diagnose why a named test is flaky; compose test_history and failure_signature_get with timing/shared-state framing.",
|
|
30
|
+
argsSchema: {
|
|
31
|
+
test: z.string().describe("Full hierarchical test name (e.g. 'Suite > nested > test')"),
|
|
32
|
+
project: z.optional(z.string()).describe("Filter to a specific project")
|
|
33
|
+
}
|
|
34
|
+
}, (args) => {
|
|
35
|
+
return { messages: toMessages(whyFlakyPrompt(args.project !== void 0 ? {
|
|
36
|
+
test: args.test,
|
|
37
|
+
project: args.project
|
|
38
|
+
} : { test: args.test }).messages) };
|
|
39
|
+
});
|
|
40
|
+
server.registerPrompt("regression-since-pass", {
|
|
41
|
+
title: "Find What Broke a Test",
|
|
42
|
+
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.",
|
|
43
|
+
argsSchema: {
|
|
44
|
+
test: z.string().describe("Full hierarchical test name"),
|
|
45
|
+
project: z.optional(z.string()).describe("Filter to a specific project")
|
|
46
|
+
}
|
|
47
|
+
}, (args) => {
|
|
48
|
+
return { messages: toMessages(regressionSincePassPrompt(args.project !== void 0 ? {
|
|
49
|
+
test: args.test,
|
|
50
|
+
project: args.project
|
|
51
|
+
} : { test: args.test }).messages) };
|
|
52
|
+
});
|
|
53
|
+
server.registerPrompt("explain-failure", {
|
|
54
|
+
title: "Explain a Failure Class",
|
|
55
|
+
description: "Synthesize a root-cause explanation from the recurrence history of a failure signature.",
|
|
56
|
+
argsSchema: { signature: z.string().describe("16-char failure signature hex") }
|
|
57
|
+
}, (args) => {
|
|
58
|
+
return { messages: toMessages(explainFailurePrompt({ signature: args.signature }).messages) };
|
|
59
|
+
});
|
|
60
|
+
server.registerPrompt("tdd-resume", {
|
|
61
|
+
title: "Resume TDD Work",
|
|
62
|
+
description: "Resume the active TDD task from its current phase; iron-law reminder for evidence-bound transitions.",
|
|
63
|
+
argsSchema: { sessionId: z.optional(z.string()).describe("Host session id (defaults to MCP server's recovered SessionContext)") }
|
|
64
|
+
}, (args) => {
|
|
65
|
+
return { messages: toMessages(tddResumePrompt(args.sessionId !== void 0 ? { sessionId: args.sessionId } : {}).messages) };
|
|
66
|
+
});
|
|
67
|
+
server.registerPrompt("wrapup", {
|
|
68
|
+
title: "Generate a Session Wrapup",
|
|
69
|
+
description: "Surface the same wrapup content the post-hooks emit automatically.",
|
|
70
|
+
argsSchema: {
|
|
71
|
+
kind: z.optional(z.enum([
|
|
72
|
+
"stop",
|
|
73
|
+
"session_end",
|
|
74
|
+
"pre_compact",
|
|
75
|
+
"tdd_handoff",
|
|
76
|
+
"user_prompt_nudge"
|
|
77
|
+
])).describe("Wrapup variant (default: user_prompt_nudge)"),
|
|
78
|
+
since: z.optional(z.string()).describe("ISO 8601 timestamp lower bound for activity to summarize")
|
|
79
|
+
}
|
|
80
|
+
}, (args) => {
|
|
81
|
+
const wrapupArgs = {};
|
|
82
|
+
if (args.kind !== void 0) wrapupArgs.kind = args.kind;
|
|
83
|
+
if (args.since !== void 0) wrapupArgs.since = args.since;
|
|
84
|
+
return { messages: toMessages(wrapupPrompt(wrapupArgs).messages) };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { registerAllPrompts };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/prompts/regression-since-pass.ts
|
|
2
|
+
function regressionSincePassPrompt(args) {
|
|
3
|
+
const projectClause = args.project ? ` in project \`${args.project}\`` : "";
|
|
4
|
+
return { messages: [{
|
|
5
|
+
role: "user",
|
|
6
|
+
content: {
|
|
7
|
+
type: "text",
|
|
8
|
+
text: [
|
|
9
|
+
`Identify what broke the test \`${args.test}\`${projectClause}.`,
|
|
10
|
+
"",
|
|
11
|
+
"The test passed at some point in recent history and now fails. The change that broke it lies in the window between then and now.",
|
|
12
|
+
"",
|
|
13
|
+
"Steps:",
|
|
14
|
+
"",
|
|
15
|
+
`1. Call \`test_history\` for ${args.project ? `\`project: "${args.project}"\` and ` : ""}the named test. Find the timestamp of the most recent passing run. Note the run id.`,
|
|
16
|
+
"2. Call `commit_changes` with no `sha` argument to fetch up to 20 most-recent commits. Filter to commits whose `committedAt` is later than the last passing run's timestamp.",
|
|
17
|
+
"3. Call `turn_search` for the same time window to see the agent activity (file_edits especially) between the last pass and the current fail.",
|
|
18
|
+
"4. Cross-reference the failing test's source / test files against the changed-files list. The cause is almost always in a file that appears in both.",
|
|
19
|
+
"5. Once you have a likely culprit, record a hypothesis via `hypothesis_record` citing the test_error_id and stack_frame_id of the most recent failure plus a short description of the change.",
|
|
20
|
+
"",
|
|
21
|
+
"If `turn_search` reveals the failing test was newly written in this window (no prior history), this is not a regression — it never passed. Treat it as a TDD-cycle failure instead."
|
|
22
|
+
].join("\n")
|
|
23
|
+
}
|
|
24
|
+
}] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { regressionSincePassPrompt };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/prompts/tdd-resume.ts
|
|
2
|
+
function tddResumePrompt(args) {
|
|
3
|
+
return { messages: [{
|
|
4
|
+
role: "user",
|
|
5
|
+
content: {
|
|
6
|
+
type: "text",
|
|
7
|
+
text: [
|
|
8
|
+
`Resume TDD work${args.sessionId ? ` for sessionId \`${args.sessionId}\`` : " for the active session (inferred from MCP server's recovered SessionContext)"}.`,
|
|
9
|
+
"",
|
|
10
|
+
"Steps:",
|
|
11
|
+
"",
|
|
12
|
+
`1. Call \`tdd_task({ action: "resume", id: <tdd-task-id> })\` to get the most recent open TDD task, including the current phase and the behavior backlog.`,
|
|
13
|
+
"2. The current phase determines what comes next:",
|
|
14
|
+
" - **`spike` or `red.triangulate`** — write the next failing test for the current behavior.",
|
|
15
|
+
" - **`red`** — run the test once to capture a `test_failed_run` artifact, then transition to `green`.",
|
|
16
|
+
" - **`green`** — write the minimal source change to make the test pass; capture a `test_passed_run`; transition to `refactor`.",
|
|
17
|
+
" - **`refactor`** — improve the implementation without changing behavior; the test must still pass.",
|
|
18
|
+
"",
|
|
19
|
+
"**Iron law**: every transition needs a cited artifact. You cannot write source code without a failing test on file. You cannot transition `red→green` without a `test_failed_run` artifact for the test you intend to make pass. You cannot transition `green→refactor` without a `test_passed_run`. The validator will deny transitions with missing or mis-bound evidence.",
|
|
20
|
+
"",
|
|
21
|
+
"When in doubt, run the failing test once and record the artifact via the post-tool hooks — don't try to record artifacts directly."
|
|
22
|
+
].join("\n")
|
|
23
|
+
}
|
|
24
|
+
}] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { tddResumePrompt };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/prompts/triage.ts
|
|
2
|
+
function triagePrompt(args) {
|
|
3
|
+
return { messages: [{
|
|
4
|
+
role: "user",
|
|
5
|
+
content: {
|
|
6
|
+
type: "text",
|
|
7
|
+
text: [
|
|
8
|
+
`You are starting a triage of the most recent test run${args.project ? ` (project filter: \`${args.project}\`)` : ""}.`,
|
|
9
|
+
"",
|
|
10
|
+
"Steps:",
|
|
11
|
+
"",
|
|
12
|
+
`1. Call \`triage_brief\`${args.project ? ` with \`project: "${args.project}"\`` : ""} to get the orientation summary.`,
|
|
13
|
+
"2. For any failures with a `signature_hash`, call `failure_signature_get` to read the recurrence history. A signature seen many times across recent runs points at a class of bug, not a fluke.",
|
|
14
|
+
"3. Form a hypothesis about the most likely root cause. Cite the specific evidence (test error, stack frame) you base the hypothesis on.",
|
|
15
|
+
"4. Record the hypothesis with `hypothesis_record`, citing the test_error_id and stack_frame_id. Once recorded, you (or the next agent) can validate it against a fix.",
|
|
16
|
+
"",
|
|
17
|
+
"Be specific. Avoid generic explanations like \"the test environment is misconfigured\" without evidence — those rarely turn out to be the cause."
|
|
18
|
+
].join("\n")
|
|
19
|
+
}
|
|
20
|
+
}] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { triagePrompt };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/prompts/why-flaky.ts
|
|
2
|
+
function whyFlakyPrompt(args) {
|
|
3
|
+
const projectClause = args.project ? ` in project \`${args.project}\`` : "";
|
|
4
|
+
return { messages: [{
|
|
5
|
+
role: "user",
|
|
6
|
+
content: {
|
|
7
|
+
type: "text",
|
|
8
|
+
text: [
|
|
9
|
+
`Diagnose why the test \`${args.test}\` is flaky${projectClause}.`,
|
|
10
|
+
"",
|
|
11
|
+
"Steps:",
|
|
12
|
+
"",
|
|
13
|
+
`1. Call \`test_history\` for ${args.project ? `\`project: "${args.project}"\` and ` : ""}the named test to read the recent pass/fail pattern.`,
|
|
14
|
+
"2. For any recent failure with a `signature_hash`, call `failure_signature_get` to see whether the same signature has appeared in earlier runs. A repeating signature is structural; a one-off signature is environmental.",
|
|
15
|
+
"3. Look for these classic flake sources, in order:",
|
|
16
|
+
" - **Timing-based assertions** — `vi.useFakeTimers` not paired with `vi.useRealTimers`, or assertions that race with `setTimeout`/`setInterval`.",
|
|
17
|
+
" - **Shared state across tests** — module-level mutables, `globalThis`, file system fixtures not cleaned up.",
|
|
18
|
+
" - **External I/O** — network calls without mocks, real database connections, real disk writes.",
|
|
19
|
+
" - **Non-deterministic input** — `Date.now()`, `Math.random()`, environment-dependent paths.",
|
|
20
|
+
"",
|
|
21
|
+
"Decide: is this a **true flake** (intermittent based on timing/state) or an **environmental failure** (consistent given specific environment conditions)? They have different fixes.",
|
|
22
|
+
"",
|
|
23
|
+
"If you reach a conclusion, record it via `hypothesis_record` with the test_error_id and stack_frame_id of the most recent failure as evidence."
|
|
24
|
+
].join("\n")
|
|
25
|
+
}
|
|
26
|
+
}] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { whyFlakyPrompt };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/prompts/wrapup.ts
|
|
2
|
+
function wrapupPrompt(args) {
|
|
3
|
+
return { messages: [{
|
|
4
|
+
role: "user",
|
|
5
|
+
content: {
|
|
6
|
+
type: "text",
|
|
7
|
+
text: [
|
|
8
|
+
"Generate a wrapup for the current session.",
|
|
9
|
+
"",
|
|
10
|
+
`Call \`wrapup_prompt\` with \`kind: "${args.kind ?? "user_prompt_nudge"}"\`${args.since ? `, since: "${args.since}"` : ""} and read the returned markdown. The output is a short, human-readable summary of what happened in the session and what the user might want to do next.`,
|
|
11
|
+
"",
|
|
12
|
+
"This is identical content to what the post-Stop / post-SessionEnd hooks emit automatically — invoking it here is for moments when you want to surface the same summary on demand."
|
|
13
|
+
].join("\n")
|
|
14
|
+
}
|
|
15
|
+
}] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { wrapupPrompt };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { renderPatternsIndex, renderUpstreamIndex } from "./indexes.js";
|
|
2
|
+
import { decodePatternsManifest, decodeUpstreamManifest } from "./manifest-schema.js";
|
|
3
|
+
import { readPattern } from "./patterns.js";
|
|
4
|
+
import { readUpstreamDoc } from "./upstream-docs.js";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
//#region src/resources/index.ts
|
|
13
|
+
/**
|
|
14
|
+
* Convert the readonly ResourceAnnotations decoded from a manifest into
|
|
15
|
+
* the mutable shape the MCP SDK's Annotations type expects. The shape is
|
|
16
|
+
* structurally identical; only the readonly-ness differs.
|
|
17
|
+
*/
|
|
18
|
+
function toSdkAnnotations(a) {
|
|
19
|
+
const out = {};
|
|
20
|
+
if (a.audience !== void 0) out.audience = [...a.audience];
|
|
21
|
+
if (a.priority !== void 0) out.priority = a.priority;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function resolveContentRoots() {
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const builtBase = here;
|
|
27
|
+
const sourceBase = join(here, "..");
|
|
28
|
+
const base = existsSync(join(builtBase, "vendor")) ? builtBase : sourceBase;
|
|
29
|
+
return {
|
|
30
|
+
vendorRoot: join(base, "vendor", "vitest-docs"),
|
|
31
|
+
patternsRoot: join(base, "patterns")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function listManifestPages(vendorRoot) {
|
|
35
|
+
const manifestPath = join(vendorRoot, "manifest.json");
|
|
36
|
+
if (!existsSync(manifestPath)) return [];
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = await readFile(manifestPath, "utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const decoded = await Effect.runPromise(decodeUpstreamManifest(parsed).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
50
|
+
if (!decoded?.pages) return [];
|
|
51
|
+
return decoded.pages.map((page) => ({
|
|
52
|
+
relativePath: page.path,
|
|
53
|
+
title: page.title,
|
|
54
|
+
description: page.description,
|
|
55
|
+
...page.annotations ? { annotations: page.annotations } : {}
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
async function listPatternEntries(patternsRoot) {
|
|
59
|
+
const metaPath = join(patternsRoot, "_meta.json");
|
|
60
|
+
if (!existsSync(metaPath)) return [];
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = await readFile(metaPath, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const decoded = await Effect.runPromise(decodePatternsManifest(parsed).pipe(Effect.catchAll(() => Effect.succeed(null))));
|
|
74
|
+
if (!decoded) return [];
|
|
75
|
+
return decoded.patterns.map((p) => ({
|
|
76
|
+
slug: p.slug,
|
|
77
|
+
title: p.title,
|
|
78
|
+
summary: p.summary,
|
|
79
|
+
...p.annotations ? { annotations: p.annotations } : {}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function registerAllResources(server) {
|
|
83
|
+
const { vendorRoot, patternsRoot } = resolveContentRoots();
|
|
84
|
+
server.registerResource("vitest_docs_index", "vitest://docs/", {
|
|
85
|
+
title: "Vitest documentation: index",
|
|
86
|
+
description: "Use first when you need any Vitest API, configuration, or behavioral information and aren't sure which page covers it — lists every page in the vendored snapshot grouped by section (api, config, guide) so you can pick the right `vitest://docs/<path>` URI before fetching.",
|
|
87
|
+
mimeType: "text/markdown"
|
|
88
|
+
}, async (uri) => {
|
|
89
|
+
const result = await renderUpstreamIndex(vendorRoot);
|
|
90
|
+
return { contents: [{
|
|
91
|
+
uri: uri.href,
|
|
92
|
+
mimeType: result.mimeType,
|
|
93
|
+
text: result.content
|
|
94
|
+
}] };
|
|
95
|
+
});
|
|
96
|
+
server.registerResource("vitest_docs_page", new ResourceTemplate("vitest://docs/{+path}", { list: async () => {
|
|
97
|
+
return { resources: (await listManifestPages(vendorRoot)).map((page) => ({
|
|
98
|
+
name: `vitest_docs_${page.relativePath.replace(/\//g, "_")}`,
|
|
99
|
+
uri: `vitest://docs/${page.relativePath}`,
|
|
100
|
+
title: page.title ?? page.relativePath,
|
|
101
|
+
description: page.description ?? `Vitest docs page: ${page.relativePath}`,
|
|
102
|
+
mimeType: "text/markdown",
|
|
103
|
+
...page.annotations ? { annotations: toSdkAnnotations(page.annotations) } : {}
|
|
104
|
+
})) };
|
|
105
|
+
} }), {
|
|
106
|
+
title: "Vitest Documentation Page",
|
|
107
|
+
description: "A single page from the vendored vitest.dev docs.",
|
|
108
|
+
mimeType: "text/markdown"
|
|
109
|
+
}, async (uri, variables) => {
|
|
110
|
+
const path = variables.path;
|
|
111
|
+
const result = await readUpstreamDoc(vendorRoot, Array.isArray(path) ? path.join("/") : String(path));
|
|
112
|
+
return { contents: [{
|
|
113
|
+
uri: uri.href,
|
|
114
|
+
mimeType: result.mimeType,
|
|
115
|
+
text: result.content
|
|
116
|
+
}] };
|
|
117
|
+
});
|
|
118
|
+
server.registerResource("vitest_agent_patterns_index", "vitest-agent://patterns/", {
|
|
119
|
+
title: "vitest-agent patterns: index",
|
|
120
|
+
description: "Use first when you need a curated vitest-agent pattern and want to discover what's available — lists every pattern slug with its title and one-line summary so you can pick the right `vitest-agent://patterns/<slug>` URI before fetching.",
|
|
121
|
+
mimeType: "text/markdown"
|
|
122
|
+
}, async (uri) => {
|
|
123
|
+
const result = await renderPatternsIndex(patternsRoot);
|
|
124
|
+
return { contents: [{
|
|
125
|
+
uri: uri.href,
|
|
126
|
+
mimeType: result.mimeType,
|
|
127
|
+
text: result.content
|
|
128
|
+
}] };
|
|
129
|
+
});
|
|
130
|
+
server.registerResource("vitest_agent_pattern", new ResourceTemplate("vitest-agent://patterns/{slug}", { list: async () => {
|
|
131
|
+
return { resources: (await listPatternEntries(patternsRoot)).map((p) => ({
|
|
132
|
+
name: `vitest_agent_pattern_${p.slug.replace(/[^A-Za-z0-9]/g, "_")}`,
|
|
133
|
+
uri: `vitest-agent://patterns/${p.slug}`,
|
|
134
|
+
title: p.title,
|
|
135
|
+
description: p.summary,
|
|
136
|
+
mimeType: "text/markdown",
|
|
137
|
+
...p.annotations ? { annotations: toSdkAnnotations(p.annotations) } : {}
|
|
138
|
+
})) };
|
|
139
|
+
} }), {
|
|
140
|
+
title: "vitest-agent Pattern",
|
|
141
|
+
description: "A single curated pattern from the vitest-agent project.",
|
|
142
|
+
mimeType: "text/markdown"
|
|
143
|
+
}, async (uri, variables) => {
|
|
144
|
+
const slug = variables.slug;
|
|
145
|
+
const result = await readPattern(patternsRoot, Array.isArray(slug) ? slug[0] : String(slug));
|
|
146
|
+
return { contents: [{
|
|
147
|
+
uri: uri.href,
|
|
148
|
+
mimeType: result.mimeType,
|
|
149
|
+
text: result.content
|
|
150
|
+
}] };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { registerAllResources };
|