@vitest-agent/mcp 2.4.12 → 2.4.13
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/index.d.ts +8 -0
- package/index.js +1 -1
- package/package.json +2 -2
- package/server.js +29 -57
- package/tools/coverage.js +32 -22
- package/tools/hypothesis.js +12 -1
- package/tools/inventory.js +13 -1
- package/tools/note.js +14 -1
- package/tools/run-tests.js +50 -20
- package/tools/tdd-artifact.js +3 -2
- package/tools/tdd-behavior.js +15 -1
- package/tools/tdd-goal.js +14 -1
- package/tools/tdd-phase-transition-request.js +28 -11
- package/tools/tdd-task.js +13 -1
- package/tools/test.js +12 -1
package/index.d.ts
CHANGED
|
@@ -245,6 +245,7 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
245
245
|
} | undefined;
|
|
246
246
|
readonly scoped: boolean;
|
|
247
247
|
readonly scopedFiles?: readonly string[] | undefined;
|
|
248
|
+
readonly totalFiles?: number | undefined;
|
|
248
249
|
readonly lowCoverage: readonly {
|
|
249
250
|
readonly file: string;
|
|
250
251
|
readonly summary: {
|
|
@@ -639,6 +640,7 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
639
640
|
} | undefined;
|
|
640
641
|
readonly scoped: boolean;
|
|
641
642
|
readonly scopedFiles?: readonly string[] | undefined;
|
|
643
|
+
readonly totalFiles?: number | undefined;
|
|
642
644
|
readonly lowCoverage: readonly {
|
|
643
645
|
readonly file: string;
|
|
644
646
|
readonly summary: {
|
|
@@ -679,12 +681,17 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
679
681
|
readonly sample?: string | undefined;
|
|
680
682
|
}[];
|
|
681
683
|
readonly truncated?: boolean | undefined;
|
|
684
|
+
readonly fromFailingTests?: {
|
|
685
|
+
readonly total: number;
|
|
686
|
+
readonly files: number;
|
|
687
|
+
} | undefined;
|
|
682
688
|
} | undefined;
|
|
683
689
|
};
|
|
684
690
|
readonly classifications: {
|
|
685
691
|
readonly [x: string]: string;
|
|
686
692
|
};
|
|
687
693
|
readonly discoveryLastScannedAt?: string | null | undefined;
|
|
694
|
+
readonly scopedNote?: string | null | undefined;
|
|
688
695
|
} | {
|
|
689
696
|
readonly kind: "timeout";
|
|
690
697
|
readonly timeoutSeconds: number;
|
|
@@ -1435,6 +1442,7 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1435
1442
|
readonly testRunId: number | null;
|
|
1436
1443
|
readonly testFirstFailureRunId: number | null;
|
|
1437
1444
|
readonly recordedAt: string;
|
|
1445
|
+
readonly suite: "bats" | "vitest";
|
|
1438
1446
|
}[];
|
|
1439
1447
|
};
|
|
1440
1448
|
meta: object;
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseSessionEnvExports, recoverSessionContextFromSessionEnv } from "./s
|
|
|
12
12
|
*
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
-
const CURRENT_MCP_VERSION = "2.4.
|
|
15
|
+
const CURRENT_MCP_VERSION = "2.4.13";
|
|
16
16
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { CURRENT_MCP_VERSION, McpLive, appRouter, buildMcpServer, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, parseSessionEnvExports, recoverSessionContextFromSessionEnv, startMcpServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Model Context Protocol server for vitest-agent. Exposes 53 tools for agent access to test data, TDD lifecycle, and session management.",
|
|
6
6
|
"keywords": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@effect/sql-sqlite-node": "4.0.0-rc.109",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
44
|
"@trpc/server": "^11.18.0",
|
|
45
|
-
"@vitest-agent/sdk": "2.
|
|
45
|
+
"@vitest-agent/sdk": "2.5.0",
|
|
46
46
|
"effect": "4.0.0-rc.109",
|
|
47
47
|
"zod": "^4.5.4"
|
|
48
48
|
},
|
package/server.js
CHANGED
|
@@ -9,9 +9,9 @@ import { FailureSignatureGetAsMarkdown, FailureSignatureGetResult } from "./tool
|
|
|
9
9
|
import { FileCoverageAsMarkdown, FileCoverageResult } from "./tools/file-coverage.js";
|
|
10
10
|
import { HelpResult } from "./tools/help.js";
|
|
11
11
|
import { TestHistoryAsMarkdown, TestHistoryResult } from "./tools/history.js";
|
|
12
|
-
import { HypothesisResult, formatHypothesisListMarkdown } from "./tools/hypothesis.js";
|
|
13
|
-
import { InventoryAsMarkdown, InventoryResult } from "./tools/inventory.js";
|
|
14
|
-
import { NoteResult, formatNoteListMarkdown } from "./tools/note.js";
|
|
12
|
+
import { HYPOTHESIS_ACTIONS, HypothesisResult, formatHypothesisListMarkdown } from "./tools/hypothesis.js";
|
|
13
|
+
import { INVENTORY_KINDS, InventoryAsMarkdown, InventoryResult } from "./tools/inventory.js";
|
|
14
|
+
import { NOTE_ACTIONS, NoteResult, formatNoteListMarkdown } from "./tools/note.js";
|
|
15
15
|
import { TestOverviewAsMarkdown, TestOverviewResult } from "./tools/overview.js";
|
|
16
16
|
import { PingResult } from "./tools/ping.js";
|
|
17
17
|
import { RegisterAgentResult } from "./tools/register-agent.js";
|
|
@@ -19,11 +19,11 @@ import { RunTestsAsMarkdown, RunTestsResult } from "./tools/run-tests.js";
|
|
|
19
19
|
import { SettingsListAsMarkdown, SettingsListResult } from "./tools/settings-list.js";
|
|
20
20
|
import { TestStatusAsMarkdown, TestStatusResult } from "./tools/status.js";
|
|
21
21
|
import { TddArtifactListAsMarkdown, TddArtifactListResult } from "./tools/tdd-artifact.js";
|
|
22
|
-
import { TddBehaviorResult } from "./tools/tdd-behavior.js";
|
|
23
|
-
import { TddGoalResult } from "./tools/tdd-goal.js";
|
|
22
|
+
import { TDD_BEHAVIOR_ACTIONS, TddBehaviorResult } from "./tools/tdd-behavior.js";
|
|
23
|
+
import { TDD_GOAL_ACTIONS, TddGoalResult } from "./tools/tdd-goal.js";
|
|
24
24
|
import { PhaseTransitionResult } from "./tools/tdd-phase-transition-request.js";
|
|
25
|
-
import { TddTaskAsMarkdown, TddTaskResult } from "./tools/tdd-task.js";
|
|
26
|
-
import { TestAsMarkdown, TestResult } from "./tools/test.js";
|
|
25
|
+
import { TDD_TASK_ACTIONS, TddTaskAsMarkdown, TddTaskResult } from "./tools/tdd-task.js";
|
|
26
|
+
import { TEST_ACTIONS, TestAsMarkdown, TestResult } from "./tools/test.js";
|
|
27
27
|
import { TestTrendsAsMarkdown, TestTrendsResult } from "./tools/trends.js";
|
|
28
28
|
import { TriageBriefResult } from "./tools/triage-brief.js";
|
|
29
29
|
import { TurnSearchAsMarkdown, TurnSearchResult } from "./tools/turn-search.js";
|
|
@@ -259,20 +259,17 @@ function buildMcpServer(ctx) {
|
|
|
259
259
|
return structuredResult(Schema.decodeSync(TestErrorsAsMarkdown)(data), data);
|
|
260
260
|
});
|
|
261
261
|
server.registerTool("test", {
|
|
262
|
-
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. structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
262
|
+
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. structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
263
263
|
inputSchema: strict({
|
|
264
|
-
action: z.enum(
|
|
265
|
-
"list",
|
|
266
|
-
"get",
|
|
267
|
-
"for_file"
|
|
268
|
-
]).describe("Inspection discriminator"),
|
|
264
|
+
action: z.enum(TEST_ACTIONS).describe("Inspection discriminator"),
|
|
269
265
|
project: z.optional(z.string()),
|
|
270
266
|
state: z.optional(z.string()).describe("list: filter by state"),
|
|
271
267
|
module: z.optional(z.string()).describe("list: filter by module path"),
|
|
272
268
|
limit: z.optional(z.coerce.number()).describe("list: max rows to return"),
|
|
273
269
|
fullName: z.optional(z.string()).describe("get: full test name"),
|
|
274
270
|
modulePath: z.optional(z.string()).describe("get: exact module path, disambiguating a fullName present in several files"),
|
|
275
|
-
filePath: z.optional(z.string()).describe("for_file: source file path")
|
|
271
|
+
filePath: z.optional(z.string()).describe("for_file: source file path"),
|
|
272
|
+
tag: z.optional(z.string()).describe("for_tag: tag name")
|
|
276
273
|
}),
|
|
277
274
|
outputSchema: effectToZodSchema(TestResult)
|
|
278
275
|
}, async (args) => {
|
|
@@ -290,10 +287,15 @@ function buildMcpServer(ctx) {
|
|
|
290
287
|
...args.project !== void 0 && { project: args.project },
|
|
291
288
|
...args.modulePath !== void 0 && { modulePath: args.modulePath }
|
|
292
289
|
});
|
|
293
|
-
else data = await caller.test({
|
|
290
|
+
else if (args.action === "for_file") data = await caller.test({
|
|
294
291
|
action: "for_file",
|
|
295
292
|
filePath: args.filePath
|
|
296
293
|
});
|
|
294
|
+
else data = await caller.test({
|
|
295
|
+
action: "for_tag",
|
|
296
|
+
tag: args.tag,
|
|
297
|
+
...args.project !== void 0 && { project: args.project }
|
|
298
|
+
});
|
|
297
299
|
return structuredResult(Schema.decodeSync(TestAsMarkdown)(data), data);
|
|
298
300
|
});
|
|
299
301
|
server.registerTool("file_coverage", {
|
|
@@ -326,14 +328,9 @@ function buildMcpServer(ctx) {
|
|
|
326
328
|
return structuredResult(Schema.decodeSync(CacheHealthAsMarkdown)(data), data);
|
|
327
329
|
});
|
|
328
330
|
server.registerTool("inventory", {
|
|
329
|
-
description: "Use to discover what exists in the workspace, with a kind discriminator: project / module / suite / session. structuredContent discriminates on `inventoryKind` (project, module, suite, session_detail, session_list) so callers can branch on the response shape without parsing markdown.",
|
|
331
|
+
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.",
|
|
330
332
|
inputSchema: strict({
|
|
331
|
-
kind: z.enum(
|
|
332
|
-
"project",
|
|
333
|
-
"module",
|
|
334
|
-
"suite",
|
|
335
|
-
"session"
|
|
336
|
-
]).describe("Inventory entity"),
|
|
333
|
+
kind: z.enum(INVENTORY_KINDS).describe("Inventory entity"),
|
|
337
334
|
id: z.optional(z.coerce.number()).describe("session: single-row lookup by id"),
|
|
338
335
|
project: z.optional(z.string()),
|
|
339
336
|
module: z.optional(z.string()).describe("suite: filter by module path"),
|
|
@@ -353,13 +350,17 @@ function buildMcpServer(ctx) {
|
|
|
353
350
|
...args.project !== void 0 && { project: args.project },
|
|
354
351
|
...args.module !== void 0 && { module: args.module }
|
|
355
352
|
});
|
|
356
|
-
else data = await caller.inventory({
|
|
353
|
+
else if (args.kind === "session") data = await caller.inventory({
|
|
357
354
|
kind: "session",
|
|
358
355
|
...args.id !== void 0 && { id: args.id },
|
|
359
356
|
...args.project !== void 0 && { project: args.project },
|
|
360
357
|
...args.agentKind !== void 0 && { agentKind: args.agentKind },
|
|
361
358
|
...args.limit !== void 0 && { limit: args.limit }
|
|
362
359
|
});
|
|
360
|
+
else data = await caller.inventory({
|
|
361
|
+
kind: "tag",
|
|
362
|
+
...args.project !== void 0 && { project: args.project }
|
|
363
|
+
});
|
|
363
364
|
return structuredResult(Schema.decodeSync(InventoryAsMarkdown)(data), data);
|
|
364
365
|
});
|
|
365
366
|
server.registerTool("settings_list", {
|
|
@@ -439,14 +440,7 @@ function buildMcpServer(ctx) {
|
|
|
439
440
|
server.registerTool("note", {
|
|
440
441
|
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`); list/search additionally render markdown in the text channel.",
|
|
441
442
|
inputSchema: strict({
|
|
442
|
-
action: z.enum(
|
|
443
|
-
"create",
|
|
444
|
-
"list",
|
|
445
|
-
"get",
|
|
446
|
-
"update",
|
|
447
|
-
"delete",
|
|
448
|
-
"search"
|
|
449
|
-
]).describe("CRUD discriminator"),
|
|
443
|
+
action: z.enum(NOTE_ACTIONS).describe("CRUD discriminator"),
|
|
450
444
|
id: z.optional(z.coerce.number()).describe("get/update/delete: note id"),
|
|
451
445
|
project: z.optional(z.string()),
|
|
452
446
|
title: z.optional(z.string()),
|
|
@@ -550,12 +544,7 @@ function buildMcpServer(ctx) {
|
|
|
550
544
|
server.registerTool("tdd_task", {
|
|
551
545
|
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.",
|
|
552
546
|
inputSchema: strict({
|
|
553
|
-
action: z.enum(
|
|
554
|
-
"start",
|
|
555
|
-
"end",
|
|
556
|
-
"get",
|
|
557
|
-
"resume"
|
|
558
|
-
]).describe("Lifecycle discriminator"),
|
|
547
|
+
action: z.enum(TDD_TASK_ACTIONS).describe("Lifecycle discriminator"),
|
|
559
548
|
tddTaskId: z.optional(z.coerce.number()).describe("end/get/resume: tdd task id"),
|
|
560
549
|
goal: z.optional(z.string()).describe("start: goal text"),
|
|
561
550
|
sessionId: z.optional(z.coerce.number()).describe("start: sessions.id (alternative to chatId)"),
|
|
@@ -638,13 +627,7 @@ function buildMcpServer(ctx) {
|
|
|
638
627
|
server.registerTool("tdd_goal", {
|
|
639
628
|
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.",
|
|
640
629
|
inputSchema: strict({
|
|
641
|
-
action: z.enum(
|
|
642
|
-
"create",
|
|
643
|
-
"update",
|
|
644
|
-
"delete",
|
|
645
|
-
"get",
|
|
646
|
-
"list"
|
|
647
|
-
]).describe("CRUD discriminator"),
|
|
630
|
+
action: z.enum(TDD_GOAL_ACTIONS).describe("CRUD discriminator"),
|
|
648
631
|
id: z.optional(z.coerce.number()).describe("update/delete/get: goal id"),
|
|
649
632
|
tddTaskId: z.optional(z.coerce.number()).describe("create/list: tdd task id"),
|
|
650
633
|
goal: z.optional(z.string()),
|
|
@@ -684,14 +667,7 @@ function buildMcpServer(ctx) {
|
|
|
684
667
|
server.registerTool("tdd_behavior", {
|
|
685
668
|
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.",
|
|
686
669
|
inputSchema: strict({
|
|
687
|
-
action: z.enum(
|
|
688
|
-
"create",
|
|
689
|
-
"update",
|
|
690
|
-
"delete",
|
|
691
|
-
"get",
|
|
692
|
-
"list_by_goal",
|
|
693
|
-
"list_by_tdd_task"
|
|
694
|
-
]).describe("CRUD discriminator"),
|
|
670
|
+
action: z.enum(TDD_BEHAVIOR_ACTIONS).describe("CRUD discriminator"),
|
|
695
671
|
id: z.optional(z.coerce.number()),
|
|
696
672
|
goalId: z.optional(z.coerce.number()),
|
|
697
673
|
tddTaskId: z.optional(z.coerce.number()),
|
|
@@ -769,11 +745,7 @@ function buildMcpServer(ctx) {
|
|
|
769
745
|
server.registerTool("hypothesis", {
|
|
770
746
|
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 as markdown.",
|
|
771
747
|
inputSchema: strict({
|
|
772
|
-
action: z.enum(
|
|
773
|
-
"record",
|
|
774
|
-
"validate",
|
|
775
|
-
"list"
|
|
776
|
-
]).describe("CRUD discriminator"),
|
|
748
|
+
action: z.enum(HYPOTHESIS_ACTIONS).describe("CRUD discriminator"),
|
|
777
749
|
sessionId: z.optional(z.coerce.number()).describe("list: filter by session id. record: dev/test fallback only — ignored when host context is recovered; never pass a tddTaskId value here"),
|
|
778
750
|
tddTaskId: z.optional(z.coerce.number()).describe("record: tdd task id returned by tdd_task action='start' — binds the hypothesis to that task's session deterministically"),
|
|
779
751
|
content: z.optional(z.string()).describe("Hypothesis content (action=record)"),
|
package/tools/coverage.js
CHANGED
|
@@ -3,11 +3,6 @@ import { CoverageReport, DataReader } from "@vitest-agent/sdk";
|
|
|
3
3
|
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/tools/coverage.ts
|
|
6
|
-
/**
|
|
7
|
-
* `test_coverage` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
6
|
const CoverageAvailable = Schema.Struct({
|
|
12
7
|
dataAvailable: Schema.Literal(true),
|
|
13
8
|
project: Schema.String,
|
|
@@ -22,34 +17,49 @@ const TestCoverageResult = Schema.Union([CoverageAvailable, CoverageAbsent]).ann
|
|
|
22
17
|
title: "test_coverage result",
|
|
23
18
|
description: "Per-project coverage report. Discriminate on `dataAvailable` for cold-start handling."
|
|
24
19
|
});
|
|
20
|
+
const METRICS = [
|
|
21
|
+
"statements",
|
|
22
|
+
"branches",
|
|
23
|
+
"functions",
|
|
24
|
+
"lines"
|
|
25
|
+
];
|
|
26
|
+
/** @internal */
|
|
27
|
+
const renderFileCoverageTable = (fileCoverage) => {
|
|
28
|
+
const rows = [
|
|
29
|
+
`### \`${fileCoverage.file}\``,
|
|
30
|
+
"",
|
|
31
|
+
"| Metric | Value |",
|
|
32
|
+
"| --- | --- |"
|
|
33
|
+
];
|
|
34
|
+
for (const metric of METRICS) rows.push(`| ${metric} | ${fileCoverage.summary[metric].toFixed(2)}% |`);
|
|
35
|
+
if (fileCoverage.uncoveredLines) rows.push(`| Uncovered lines | \`${fileCoverage.uncoveredLines}\` |`);
|
|
36
|
+
rows.push("");
|
|
37
|
+
return rows;
|
|
38
|
+
};
|
|
25
39
|
const formatTestCoverageMarkdown = (data) => {
|
|
26
40
|
if (!data.dataAvailable) return "No coverage data available. Run tests with coverage enabled.";
|
|
27
41
|
const lines = ["# Coverage Report", ""];
|
|
28
|
-
const { totals, thresholds } = data.coverage;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"functions",
|
|
34
|
-
"lines"
|
|
35
|
-
];
|
|
36
|
-
for (const metric of metrics) {
|
|
42
|
+
const { totals, thresholds, targets } = data.coverage;
|
|
43
|
+
const headerCols = targets ? "| Metric | Value | Enforced threshold | Target |" : "| Metric | Value | Enforced threshold |";
|
|
44
|
+
const sepCols = targets ? "| --- | --- | --- | --- |" : "| --- | --- | --- |";
|
|
45
|
+
lines.push("## Totals", "", headerCols, sepCols);
|
|
46
|
+
for (const metric of METRICS) {
|
|
37
47
|
const value = totals[metric];
|
|
38
48
|
const threshold = thresholds.global[metric];
|
|
39
49
|
const thresholdStr = threshold !== void 0 ? `${threshold}%` : "—";
|
|
40
50
|
const icon = threshold !== void 0 && value < threshold ? "❌" : "✅";
|
|
41
|
-
|
|
51
|
+
const targetStr = targets ? targets.global[metric] !== void 0 ? `${targets.global[metric]}%` : "—" : void 0;
|
|
52
|
+
lines.push(`| ${metric} | ${icon} ${value.toFixed(2)}% | ${thresholdStr} |${targetStr !== void 0 ? ` ${targetStr} |` : ""}`);
|
|
42
53
|
}
|
|
43
54
|
lines.push("");
|
|
44
55
|
if (data.coverage.lowCoverage.length > 0) {
|
|
45
|
-
lines.push("## Coverage Gaps", "", "Files below
|
|
46
|
-
for (const fileCoverage of data.coverage.lowCoverage)
|
|
47
|
-
lines.push(`### \`${fileCoverage.file}\``, "", "| Metric | Value |", "| --- | --- |");
|
|
48
|
-
for (const metric of metrics) lines.push(`| ${metric} | ${fileCoverage.summary[metric].toFixed(2)}% |`);
|
|
49
|
-
if (fileCoverage.uncoveredLines) lines.push(`| Uncovered lines | \`${fileCoverage.uncoveredLines}\` |`);
|
|
50
|
-
lines.push("");
|
|
51
|
-
}
|
|
56
|
+
lines.push("## Coverage Gaps", "", "Files below the enforced threshold (build-blocking):", "");
|
|
57
|
+
for (const fileCoverage of data.coverage.lowCoverage) lines.push(...renderFileCoverageTable(fileCoverage));
|
|
52
58
|
} else lines.push("✅ All files meet coverage thresholds.", "");
|
|
59
|
+
if (data.coverage.belowTarget && data.coverage.belowTarget.length > 0) {
|
|
60
|
+
lines.push("## Coverage Improvements Needed", "", "Files below the aspirational target (passing the enforced threshold):", "");
|
|
61
|
+
for (const fileCoverage of data.coverage.belowTarget) lines.push(...renderFileCoverageTable(fileCoverage));
|
|
62
|
+
}
|
|
53
63
|
return lines.join("\n");
|
|
54
64
|
};
|
|
55
65
|
const TestCoverageAsMarkdown = TestCoverageResult.pipe(Schema.decodeTo(Schema.String, {
|
package/tools/hypothesis.js
CHANGED
|
@@ -91,6 +91,17 @@ const HypothesisInput = Schema.Union([
|
|
|
91
91
|
ValidateVariant,
|
|
92
92
|
ListVariant
|
|
93
93
|
]);
|
|
94
|
+
/**
|
|
95
|
+
* Single source of truth for the `hypothesis` tool's `action`
|
|
96
|
+
* discriminant, consumed by `server.ts`'s served `z.enum(...)` so the
|
|
97
|
+
* MCP-SDK-side registration cannot drift from this tRPC input union
|
|
98
|
+
* (issue #335).
|
|
99
|
+
*/
|
|
100
|
+
const HYPOTHESIS_ACTIONS = [
|
|
101
|
+
"record",
|
|
102
|
+
"validate",
|
|
103
|
+
"list"
|
|
104
|
+
];
|
|
94
105
|
const hypothesis = idempotentProcedure.input(Schema.toStandardSchemaV1(HypothesisInput)).mutation(async ({ ctx, input }) => {
|
|
95
106
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
96
107
|
record: (variant) => Effect.gen(function* () {
|
|
@@ -157,4 +168,4 @@ const hypothesis = idempotentProcedure.input(Schema.toStandardSchemaV1(Hypothesi
|
|
|
157
168
|
});
|
|
158
169
|
|
|
159
170
|
//#endregion
|
|
160
|
-
export { HypothesisResult, formatHypothesisListMarkdown, hypothesis };
|
|
171
|
+
export { HYPOTHESIS_ACTIONS, HypothesisResult, formatHypothesisListMarkdown, hypothesis };
|
package/tools/inventory.js
CHANGED
|
@@ -255,6 +255,18 @@ const InventoryInput = Schema.Union([
|
|
|
255
255
|
SessionVariant,
|
|
256
256
|
TagVariant
|
|
257
257
|
]);
|
|
258
|
+
/**
|
|
259
|
+
* Single source of truth for the `inventory` tool's `kind` discriminant,
|
|
260
|
+
* consumed by `server.ts`'s served `z.enum(...)` so the MCP-SDK-side
|
|
261
|
+
* registration cannot drift from this tRPC input union (issue #335).
|
|
262
|
+
*/
|
|
263
|
+
const INVENTORY_KINDS = [
|
|
264
|
+
"project",
|
|
265
|
+
"module",
|
|
266
|
+
"suite",
|
|
267
|
+
"session",
|
|
268
|
+
"tag"
|
|
269
|
+
];
|
|
258
270
|
const inventory = publicProcedure.input(Schema.toStandardSchemaV1(InventoryInput)).query(async ({ ctx, input }) => {
|
|
259
271
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("kind")({
|
|
260
272
|
project: () => Effect.gen(function* () {
|
|
@@ -377,4 +389,4 @@ const inventory = publicProcedure.input(Schema.toStandardSchemaV1(InventoryInput
|
|
|
377
389
|
});
|
|
378
390
|
|
|
379
391
|
//#endregion
|
|
380
|
-
export { InventoryAsMarkdown, InventoryResult, formatInventoryMarkdown, inventory };
|
|
392
|
+
export { INVENTORY_KINDS, InventoryAsMarkdown, InventoryResult, formatInventoryMarkdown, inventory };
|
package/tools/note.js
CHANGED
|
@@ -152,6 +152,19 @@ const NoteInputUnion = Schema.Union([
|
|
|
152
152
|
DeleteVariant,
|
|
153
153
|
SearchVariant
|
|
154
154
|
]);
|
|
155
|
+
/**
|
|
156
|
+
* Single source of truth for the `note` tool's `action` discriminant,
|
|
157
|
+
* consumed by `server.ts`'s served `z.enum(...)` so the MCP-SDK-side
|
|
158
|
+
* registration cannot drift from this tRPC input union (issue #335).
|
|
159
|
+
*/
|
|
160
|
+
const NOTE_ACTIONS = [
|
|
161
|
+
"create",
|
|
162
|
+
"list",
|
|
163
|
+
"get",
|
|
164
|
+
"update",
|
|
165
|
+
"delete",
|
|
166
|
+
"search"
|
|
167
|
+
];
|
|
155
168
|
const note = publicProcedure.input(Schema.toStandardSchemaV1(NoteInputUnion)).mutation(async ({ ctx, input }) => {
|
|
156
169
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
157
170
|
create: (variant) => Effect.gen(function* () {
|
|
@@ -227,4 +240,4 @@ const note = publicProcedure.input(Schema.toStandardSchemaV1(NoteInputUnion)).mu
|
|
|
227
240
|
});
|
|
228
241
|
|
|
229
242
|
//#endregion
|
|
230
|
-
export { NoteResult, formatNoteListMarkdown, note };
|
|
243
|
+
export { NOTE_ACTIONS, NoteResult, formatNoteListMarkdown, note };
|
package/tools/run-tests.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { publicProcedure } from "../context.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { AgentReport, DataReader, DataStore, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries } from "@vitest-agent/sdk";
|
|
4
|
-
import { Effect, Schema, SchemaGetter, Semaphore } from "effect";
|
|
3
|
+
import { AgentReport, DataReader, DataStore, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote } from "@vitest-agent/sdk";
|
|
4
|
+
import { Data, Effect, Schema, SchemaGetter, Semaphore } from "effect";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
@@ -36,7 +36,8 @@ const RunTestsOk = Schema.Struct({
|
|
|
36
36
|
scope: RunTestsScope,
|
|
37
37
|
report: AgentReport.annotate({ description: "Full AgentReport including pass/fail counts and per-module errors." }),
|
|
38
38
|
classifications: Schema.Record(Schema.String, Schema.String).annotate({ description: "Per-test classification labels: stable, new-failure, persistent, flaky, recovered." }),
|
|
39
|
-
discoveryLastScannedAt: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "ISO timestamp of the most recent real disk scan performed by discoverProjects() in this process (issue #100). `null`/absent means discovery has not scanned disk in this process yet (e.g. a config that doesn't call AgentPlugin.discover()). A stale-looking test count is self-explaining when compared against this value." })
|
|
39
|
+
discoveryLastScannedAt: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "ISO timestamp of the most recent real disk scan performed by discoverProjects() in this process (issue #100). `null`/absent means discovery has not scanned disk in this process yet (e.g. a config that doesn't call AgentPlugin.discover()). A stale-looking test count is self-explaining when compared against this value." }),
|
|
40
|
+
scopedNote: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Set (issue #160) when this call was scoped to a subset of the project's test files (files/project/tags filter supplied). Vitest enforces coverage.thresholds against the whole-project denominator regardless of how many files ran, so a scoped run's threshold check is unreliable; this note explains why no coverage verdict should be trusted for this call. `null`/absent means the run was unscoped." })
|
|
40
41
|
}).annotate({ identifier: "RunTestsOk" });
|
|
41
42
|
const RunTestsTimeout = Schema.Struct({
|
|
42
43
|
kind: Schema.Literal("timeout"),
|
|
@@ -124,6 +125,12 @@ const makeCoverageDirOverride = () => {
|
|
|
124
125
|
coverage: { reportsDirectory: dir }
|
|
125
126
|
};
|
|
126
127
|
};
|
|
128
|
+
/**
|
|
129
|
+
* Typed wrapper for a rejected `vitest.start(...)` promise (issue #320).
|
|
130
|
+
* Keeps the run's Effect error channel a tagged union so the timeout
|
|
131
|
+
* branch can be recovered by tag instead of by string sentinel.
|
|
132
|
+
*/
|
|
133
|
+
var VitestStartFailure = class extends Data.TaggedError("VitestStartFailure") {};
|
|
127
134
|
const stdoutSinkStorage = new AsyncLocalStorage();
|
|
128
135
|
const stderrSinkStorage = new AsyncLocalStorage();
|
|
129
136
|
let _stdioPatched = false;
|
|
@@ -358,7 +365,7 @@ function formatRunTestsMarkdown(data) {
|
|
|
358
365
|
const rootLine = `\nProject root: \`${data.projectRoot}\``;
|
|
359
366
|
if (data.kind === "no-match") return `${formatNoMatchMarkdown(data.filter)}${rootLine}`;
|
|
360
367
|
const classMap = new Map(Object.entries(data.classifications));
|
|
361
|
-
return `${formatReportMarkdown(data.report, classMap)}${rootLine}`;
|
|
368
|
+
return `${formatReportMarkdown(data.report, classMap, data.scopedNote ?? void 0)}${rootLine}`;
|
|
362
369
|
}
|
|
363
370
|
/**
|
|
364
371
|
* Render the `no-match` filter context plus a remediation pointer aimed at
|
|
@@ -403,7 +410,7 @@ const RunTestsAsMarkdown = RunTestsResult.pipe(Schema.decodeTo(Schema.String, {
|
|
|
403
410
|
*
|
|
404
411
|
* @internal
|
|
405
412
|
*/
|
|
406
|
-
function formatReportMarkdown(report, classifications) {
|
|
413
|
+
function formatReportMarkdown(report, classifications, scopedNote) {
|
|
407
414
|
const lines = [];
|
|
408
415
|
const { summary } = report;
|
|
409
416
|
const collectionFailedFiles = report.failed.filter((m) => m.errors !== void 0 && m.errors.length > 0 && !m.tests.some((t) => t.state === "failed")).map((m) => m.file);
|
|
@@ -417,12 +424,19 @@ function formatReportMarkdown(report, classifications) {
|
|
|
417
424
|
if (summary.skipped > 0) headlineParts.push(`${summary.skipped} skipped`);
|
|
418
425
|
lines.push(`## ${status} Vitest -- ${headlineParts.join(", ")} (${summary.duration}ms)`);
|
|
419
426
|
if (report.project) lines.push(`\nProject: ${report.project}`);
|
|
427
|
+
if (scopedNote !== void 0) lines.push(`\n${scopedNote}`);
|
|
420
428
|
if (report.consoleLeaks !== void 0) {
|
|
421
429
|
const cl = report.consoleLeaks;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
430
|
+
if (cl.total > 0) {
|
|
431
|
+
const writes = `${cl.total} stray console write${cl.total === 1 ? "" : "s"}`;
|
|
432
|
+
const plural = cl.byFile.length !== 1 || cl.truncated === true;
|
|
433
|
+
const files = `${cl.byFile.length}${cl.truncated === true ? "+" : ""} file${plural ? "s" : ""}`;
|
|
434
|
+
lines.push(`\n⚠ ${writes} across ${files} (see consoleLeaks)`);
|
|
435
|
+
}
|
|
436
|
+
if (cl.fromFailingTests !== void 0) {
|
|
437
|
+
const { total: failingTotal } = cl.fromFailingTests;
|
|
438
|
+
lines.push(`\n${failingTotal} console write${failingTotal === 1 ? "" : "s"} from failing tests (not counted as leaks)`);
|
|
439
|
+
}
|
|
426
440
|
}
|
|
427
441
|
for (const mod of report.failed) {
|
|
428
442
|
lines.push(`\n### \u274C \`${mod.file}\``);
|
|
@@ -521,12 +535,22 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
521
535
|
stderr: nullStream
|
|
522
536
|
});
|
|
523
537
|
const localVitest = vitest;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
|
|
538
|
+
const startOutcome = await withStdioCaptured(nullStream, () => Effect.runPromise(Effect.tryPromise({
|
|
539
|
+
try: () => localVitest.start(files.length > 0 ? files : void 0),
|
|
540
|
+
catch: (cause) => new VitestStartFailure({ cause })
|
|
541
|
+
}).pipe(Effect.timeout(timeoutMs), Effect.map((value) => ({
|
|
542
|
+
outcome: "ok",
|
|
543
|
+
value
|
|
544
|
+
})), Effect.catchTag("TimeoutError", () => Effect.succeed({ outcome: "timeout" })), Effect.catchTag("VitestStartFailure", (e) => Effect.succeed({
|
|
545
|
+
outcome: "failed",
|
|
546
|
+
cause: e.cause
|
|
547
|
+
})))));
|
|
548
|
+
if (startOutcome.outcome === "timeout") return {
|
|
549
|
+
kind: "timeout",
|
|
550
|
+
timeoutSeconds: input.timeout ?? 120
|
|
551
|
+
};
|
|
552
|
+
if (startOutcome.outcome === "failed") throw startOutcome.cause;
|
|
553
|
+
const result = startOutcome.value;
|
|
530
554
|
const testModules = result.testModules;
|
|
531
555
|
const unhandledErrors = coerceErrors(result.unhandledErrors);
|
|
532
556
|
if (hasFilter && result.testModules.length === 0 && unhandledErrors.length === 0) return {
|
|
@@ -566,6 +590,15 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
566
590
|
invocationMethod: "mcp"
|
|
567
591
|
});
|
|
568
592
|
})).catch(() => void 0);
|
|
593
|
+
let scopedNote = null;
|
|
594
|
+
if (hasFilter) {
|
|
595
|
+
const testedFileCount = testModules.length;
|
|
596
|
+
let totalFileCount;
|
|
597
|
+
try {
|
|
598
|
+
totalFileCount = (await localVitest.globTestSpecifications()).length;
|
|
599
|
+
} catch {}
|
|
600
|
+
scopedNote = formatScopedCoverageNote(testedFileCount, totalFileCount);
|
|
601
|
+
}
|
|
569
602
|
return {
|
|
570
603
|
kind: "ok",
|
|
571
604
|
...project !== void 0 && { project },
|
|
@@ -577,15 +610,12 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
577
610
|
},
|
|
578
611
|
report,
|
|
579
612
|
classifications: classifications ? Object.fromEntries(classifications) : {},
|
|
580
|
-
discoveryLastScannedAt: readDiscoveryLastScannedAt() ?? null
|
|
613
|
+
discoveryLastScannedAt: readDiscoveryLastScannedAt() ?? null,
|
|
614
|
+
scopedNote
|
|
581
615
|
};
|
|
582
616
|
} catch (err) {
|
|
583
617
|
let message;
|
|
584
618
|
try {
|
|
585
|
-
if (err instanceof Error && err.message === "VITEST_TIMEOUT") return {
|
|
586
|
-
kind: "timeout",
|
|
587
|
-
timeoutSeconds: input.timeout ?? 120
|
|
588
|
-
};
|
|
589
619
|
message = err instanceof Error ? err.message : String(err);
|
|
590
620
|
} catch {
|
|
591
621
|
message = coerceErrorField(err, "message") ?? "<unserializable error>";
|
package/tools/tdd-artifact.js
CHANGED
|
@@ -45,7 +45,8 @@ const TddArtifactRow = Schema.Struct({
|
|
|
45
45
|
testCaseId: Schema.NullOr(Schema.Number),
|
|
46
46
|
testRunId: Schema.NullOr(Schema.Number),
|
|
47
47
|
testFirstFailureRunId: Schema.NullOr(Schema.Number),
|
|
48
|
-
recordedAt: Schema.String
|
|
48
|
+
recordedAt: Schema.String,
|
|
49
|
+
suite: Schema.Literals(["vitest", "bats"]).annotate({ description: "Which test runner produced this artifact — a bats run carries no test_case_id." })
|
|
49
50
|
}).annotate({ identifier: "TddArtifactListRow" });
|
|
50
51
|
const ArtifactFilters = Schema.Struct({
|
|
51
52
|
artifactKind: Schema.optional(ArtifactKindSchema),
|
|
@@ -73,7 +74,7 @@ const formatTddArtifactListMarkdown = (data) => {
|
|
|
73
74
|
if (data.count === 0) return `No artifacts recorded for tdd_task ${data.tddTaskId}${describeFilters(data.filters)}.`;
|
|
74
75
|
const lines = [`# Artifacts for tdd_task ${data.tddTaskId} (newest first, ${data.count} shown)`, ""];
|
|
75
76
|
for (const r of data.artifacts) {
|
|
76
|
-
const extras = [`phase=${r.phaseName} [phaseId=${r.phaseId}]`];
|
|
77
|
+
const extras = [`phase=${r.phaseName} [phaseId=${r.phaseId}]`, `suite=${r.suite}`];
|
|
77
78
|
if (r.behaviorId !== null) extras.push(`behaviorId=${r.behaviorId}`);
|
|
78
79
|
if (r.testCaseId !== null) extras.push(`testCaseId=${r.testCaseId}`);
|
|
79
80
|
if (r.testRunId !== null) extras.push(`testRunId=${r.testRunId}`);
|
package/tools/tdd-behavior.js
CHANGED
|
@@ -117,6 +117,20 @@ const TddBehaviorInput = Schema.Union([
|
|
|
117
117
|
ListByGoalVariant,
|
|
118
118
|
ListByTddTaskVariant
|
|
119
119
|
]);
|
|
120
|
+
/**
|
|
121
|
+
* Single source of truth for the `tdd_behavior` tool's `action`
|
|
122
|
+
* discriminant, consumed by `server.ts`'s served `z.enum(...)` so the
|
|
123
|
+
* MCP-SDK-side registration cannot drift from this tRPC input union
|
|
124
|
+
* (issue #335).
|
|
125
|
+
*/
|
|
126
|
+
const TDD_BEHAVIOR_ACTIONS = [
|
|
127
|
+
"create",
|
|
128
|
+
"update",
|
|
129
|
+
"delete",
|
|
130
|
+
"get",
|
|
131
|
+
"list_by_goal",
|
|
132
|
+
"list_by_tdd_task"
|
|
133
|
+
];
|
|
120
134
|
const tddBehavior = idempotentProcedure.input(Schema.toStandardSchemaV1(TddBehaviorInput)).mutation(async ({ ctx, input }) => {
|
|
121
135
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
122
136
|
create: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
|
|
@@ -192,4 +206,4 @@ const tddBehavior = idempotentProcedure.input(Schema.toStandardSchemaV1(TddBehav
|
|
|
192
206
|
});
|
|
193
207
|
|
|
194
208
|
//#endregion
|
|
195
|
-
export { TddBehaviorResult, tddBehavior };
|
|
209
|
+
export { TDD_BEHAVIOR_ACTIONS, TddBehaviorResult, tddBehavior };
|
package/tools/tdd-goal.js
CHANGED
|
@@ -101,6 +101,19 @@ const TddGoalInput = Schema.Union([
|
|
|
101
101
|
GetVariant,
|
|
102
102
|
ListVariant
|
|
103
103
|
]);
|
|
104
|
+
/**
|
|
105
|
+
* Single source of truth for the `tdd_goal` tool's `action`
|
|
106
|
+
* discriminant, consumed by `server.ts`'s served `z.enum(...)` so the
|
|
107
|
+
* MCP-SDK-side registration cannot drift from this tRPC input union
|
|
108
|
+
* (issue #335).
|
|
109
|
+
*/
|
|
110
|
+
const TDD_GOAL_ACTIONS = [
|
|
111
|
+
"create",
|
|
112
|
+
"update",
|
|
113
|
+
"delete",
|
|
114
|
+
"get",
|
|
115
|
+
"list"
|
|
116
|
+
];
|
|
104
117
|
const tddGoal = idempotentProcedure.input(Schema.toStandardSchemaV1(TddGoalInput)).mutation(async ({ ctx, input }) => {
|
|
105
118
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
106
119
|
create: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
|
|
@@ -160,4 +173,4 @@ const tddGoal = idempotentProcedure.input(Schema.toStandardSchemaV1(TddGoalInput
|
|
|
160
173
|
});
|
|
161
174
|
|
|
162
175
|
//#endregion
|
|
163
|
-
export { TddGoalResult, tddGoal };
|
|
176
|
+
export { TDD_GOAL_ACTIONS, TddGoalResult, tddGoal };
|
|
@@ -3,6 +3,12 @@ import { DataReader, DataStore, requiredArtifactForTransition, transitionEnforce
|
|
|
3
3
|
import { Effect, Option, Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/tools/tdd-phase-transition-request.ts
|
|
6
|
+
/**
|
|
7
|
+
* Lookback window for the missing_artifact_evidence cross-session
|
|
8
|
+
* diagnostic (issue #144). Wide enough to catch a subagent dispatch
|
|
9
|
+
* that ran for a few minutes before the agent hit the gate.
|
|
10
|
+
*/
|
|
11
|
+
const DIAGNOSTIC_WINDOW_MINUTES = 10;
|
|
6
12
|
const phaseLiteral = Schema.Literals([
|
|
7
13
|
"spike",
|
|
8
14
|
"red",
|
|
@@ -82,6 +88,7 @@ const tddPhaseTransitionRequest = publicProcedure.input(Schema.toStandardSchemaV
|
|
|
82
88
|
const currentOpt = yield* reader.getCurrentTddPhase(input.tddTaskId);
|
|
83
89
|
const currentPhase = Option.isSome(currentOpt) ? currentOpt.value.phase : "spike";
|
|
84
90
|
const phaseStartedAt = Option.isSome(currentOpt) ? currentOpt.value.startedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
91
|
+
const currentPhaseId = Option.isSome(currentOpt) ? currentOpt.value.id : null;
|
|
85
92
|
const goalOpt = yield* reader.getGoalById(input.goalId);
|
|
86
93
|
if (Option.isNone(goalOpt)) return {
|
|
87
94
|
accepted: false,
|
|
@@ -170,16 +177,24 @@ const tddPhaseTransitionRequest = publicProcedure.input(Schema.toStandardSchemaV
|
|
|
170
177
|
...scopeLookupByBehavior && { behaviorId: input.behaviorId },
|
|
171
178
|
limit: 1
|
|
172
179
|
});
|
|
173
|
-
if (recent.length === 0)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
if (recent.length === 0) {
|
|
181
|
+
const sinceIso = (/* @__PURE__ */ new Date(Date.now() - DIAGNOSTIC_WINDOW_MINUTES * 6e4)).toISOString();
|
|
182
|
+
const crossSessionCount = yield* reader.countRecentArtifactsInOtherSessionsOfConversation({
|
|
183
|
+
tddTaskId: input.tddTaskId,
|
|
184
|
+
sinceIso
|
|
185
|
+
});
|
|
186
|
+
const baseHint = `No '${kindToLookUp}' artifact has been recorded for tdd_task ${input.tddTaskId}. Artifacts are recorded by hooks observing your tool calls (Decision D7) — run the test (e.g. via run_tests) or make the file edit first; the post-tool-use hook will write the matching tdd_artifacts row and the next call to this tool will pick it up automatically.`;
|
|
187
|
+
return {
|
|
188
|
+
accepted: false,
|
|
189
|
+
phase: currentPhase,
|
|
190
|
+
denialReason: "missing_artifact_evidence",
|
|
191
|
+
remediation: {
|
|
192
|
+
suggestedTool: "run_tests",
|
|
193
|
+
suggestedArgs: {},
|
|
194
|
+
humanHint: 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
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
183
198
|
resolvedArtifactId = recent[0].id;
|
|
184
199
|
}
|
|
185
200
|
let citedArtifact;
|
|
@@ -205,11 +220,13 @@ const tddPhaseTransitionRequest = publicProcedure.input(Schema.toStandardSchemaV
|
|
|
205
220
|
test_case_authored_in_session: false,
|
|
206
221
|
test_run_id: null,
|
|
207
222
|
test_first_failure_run_id: null,
|
|
208
|
-
behavior_id: null
|
|
223
|
+
behavior_id: null,
|
|
224
|
+
suite: "vitest"
|
|
209
225
|
};
|
|
210
226
|
const result = validatePhaseTransition({
|
|
211
227
|
tdd_task_id: input.tddTaskId,
|
|
212
228
|
current_phase: currentPhase,
|
|
229
|
+
current_phase_id: currentPhaseId,
|
|
213
230
|
phase_started_at: phaseStartedAt,
|
|
214
231
|
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
215
232
|
requested_phase: input.requestedPhase,
|
package/tools/tdd-task.js
CHANGED
|
@@ -194,6 +194,18 @@ const TddTaskInput = Schema.Union([
|
|
|
194
194
|
GetVariant,
|
|
195
195
|
ResumeVariant
|
|
196
196
|
]);
|
|
197
|
+
/**
|
|
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).
|
|
202
|
+
*/
|
|
203
|
+
const TDD_TASK_ACTIONS = [
|
|
204
|
+
"start",
|
|
205
|
+
"end",
|
|
206
|
+
"get",
|
|
207
|
+
"resume"
|
|
208
|
+
];
|
|
197
209
|
const tddTask = idempotentProcedure.input(Schema.toStandardSchemaV1(TddTaskInput)).mutation(async ({ ctx, input }) => {
|
|
198
210
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
199
211
|
start: (variant) => Effect.gen(function* () {
|
|
@@ -294,4 +306,4 @@ const tddTask = idempotentProcedure.input(Schema.toStandardSchemaV1(TddTaskInput
|
|
|
294
306
|
});
|
|
295
307
|
|
|
296
308
|
//#endregion
|
|
297
|
-
export { TddTaskAsMarkdown, TddTaskResult, formatTddTaskMarkdown, tddTask };
|
|
309
|
+
export { TDD_TASK_ACTIONS, TddTaskAsMarkdown, TddTaskResult, formatTddTaskMarkdown, tddTask };
|
package/tools/test.js
CHANGED
|
@@ -220,6 +220,17 @@ const TestInput = Schema.Union([
|
|
|
220
220
|
ForFileVariant,
|
|
221
221
|
ForTagVariant
|
|
222
222
|
]);
|
|
223
|
+
/**
|
|
224
|
+
* Single source of truth for the `test` tool's `action` discriminant,
|
|
225
|
+
* consumed by `server.ts`'s served `z.enum(...)` so the MCP-SDK-side
|
|
226
|
+
* registration cannot drift from this tRPC input union (issue #335).
|
|
227
|
+
*/
|
|
228
|
+
const TEST_ACTIONS = [
|
|
229
|
+
"list",
|
|
230
|
+
"get",
|
|
231
|
+
"for_file",
|
|
232
|
+
"for_tag"
|
|
233
|
+
];
|
|
223
234
|
const test = publicProcedure.input(Schema.toStandardSchemaV1(TestInput)).query(async ({ ctx, input }) => {
|
|
224
235
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
225
236
|
list: (variant) => Effect.gen(function* () {
|
|
@@ -310,4 +321,4 @@ const test = publicProcedure.input(Schema.toStandardSchemaV1(TestInput)).query(a
|
|
|
310
321
|
});
|
|
311
322
|
|
|
312
323
|
//#endregion
|
|
313
|
-
export { TestAsMarkdown, TestResult, formatTestMarkdown, test };
|
|
324
|
+
export { TEST_ACTIONS, TestAsMarkdown, TestResult, formatTestMarkdown, test };
|