@vitest-agent/mcp 4.2.2 → 4.3.1

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/tools/errors.js CHANGED
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
@@ -63,84 +62,6 @@ const TestErrorsResult = Schema.Struct({
63
62
  title: "test_errors result",
64
63
  description: "Structured payload of the `test_errors` MCP tool. Carries the cite-able test_errors.id and stack_frames.id values agents need for `hypothesis (action: record)`."
65
64
  });
66
- const TRUNCATION_LIMIT = 500;
67
- const truncate = (s) => s.length <= TRUNCATION_LIMIT ? {
68
- value: s,
69
- truncated: false
70
- } : {
71
- value: s.slice(0, TRUNCATION_LIMIT),
72
- truncated: true
73
- };
74
- /**
75
- * Pure markdown renderer. Exposed so the formatter tests can exercise
76
- * it without rebuilding a `Schema.encode` runtime.
77
- */
78
- const formatTestErrorsMarkdown = (data) => {
79
- if (data.errors.length === 0) return `No errors found for project \`${data.project}\`.`;
80
- const lines = [`# Test Errors — ${data.project}`, ""];
81
- for (const error of data.errors) {
82
- const name = error.name ?? "(unnamed)";
83
- const idTokens = `[testErrorId=${error.id}${error.topStackFrameId !== null ? ` topStackFrameId=${error.topStackFrameId}` : ""}]`;
84
- lines.push(`## ${name} ${idTokens}`);
85
- lines.push("");
86
- lines.push(`**Scope:** ${error.scope}`);
87
- if (error.testFullName !== null) lines.push(`**Test:** ${error.testFullName}`);
88
- if (error.moduleFile !== null) lines.push(`**File:** \`${error.moduleFile}\``);
89
- if (error.annotations.length > 0) {
90
- lines.push("**Annotations:**");
91
- for (const annotation of error.annotations) lines.push(`- [${annotation.type}] ${annotation.message}`);
92
- }
93
- lines.push("");
94
- lines.push("**Cite-able IDs (for `hypothesis (action: record)`):**");
95
- lines.push(`- citedTestErrorId: ${error.id}`);
96
- if (error.topStackFrameId !== null) lines.push(`- citedStackFrameId: ${error.topStackFrameId}`);
97
- else lines.push("- citedStackFrameId: (none — no stack frames recorded for this error)");
98
- lines.push("");
99
- lines.push("**Message:**");
100
- lines.push(`> ${error.message.split("\n").join("\n> ")}`);
101
- if (error.diff !== null) {
102
- lines.push("");
103
- lines.push("**Diff:**");
104
- lines.push("```diff");
105
- const t = truncate(error.diff);
106
- lines.push(t.value);
107
- if (t.truncated) lines.push("... (truncated)");
108
- lines.push("```");
109
- }
110
- if (error.stack !== null && error.diff === null) {
111
- lines.push("");
112
- lines.push("**Stack:**");
113
- lines.push("```");
114
- const t = truncate(error.stack);
115
- lines.push(t.value);
116
- if (t.truncated) lines.push("... (truncated)");
117
- lines.push("```");
118
- }
119
- lines.push("");
120
- }
121
- return lines.join("\n");
122
- };
123
- /**
124
- * One-way codec: structured `TestErrorsResult` → markdown text.
125
- *
126
- * `Schema.decodeTo`'s `decode` direction goes
127
- * `From.Type → To.Encoded`; in this transform `From = TestErrorsResult`
128
- * and `To = Schema.String`, so the resulting schema is
129
- * `Schema<string, TestErrorsResultType>` — its parsed Type is the
130
- * markdown string and its Encoded form is the structured row. That
131
- * means `Schema.decode(TestErrorsAsMarkdown)(data)` produces markdown
132
- * (the rendering direction) and `Schema.encode(...)` would attempt
133
- * the lossy reverse, which is forbidden here.
134
- *
135
- * Boundary callers should use
136
- * `Schema.decodeSync(TestErrorsAsMarkdown)(data)` to render. Test
137
- * suites can drive the same path without mocking anything — the
138
- * transform IS the rendering contract.
139
- */
140
- const TestErrorsAsMarkdown = TestErrorsResult.pipe(Schema.decodeTo(Schema.String, {
141
- decode: SchemaGetter.transform((data) => formatTestErrorsMarkdown(data)),
142
- encode: SchemaGetter.forbidden(() => "TestErrorsAsMarkdown is one-way: markdown cannot be parsed back to TestErrorsResult. Consume the procedure's structured output (or MCP structuredContent) directly.")
143
- }));
144
65
  /**
145
66
  * The `test_errors` tool's parameters.
146
67
  *
@@ -196,11 +117,11 @@ const handleTestErrors = (input) => Effect.gen(function* () {
196
117
  * @public
197
118
  */
198
119
  const testErrorsTool = Tool.make("test_errors", {
199
- description: "Use when a test fails and you need error detail, diffs, and the cite-able test_errors.id / stack_frames.id values needed by hypothesis (action: record). Returns both a markdown rendering (in content[].text) and a typed JSON object (in structuredContent) — agents should prefer structuredContent.errors[].",
120
+ description: "Use when a test fails and you need error detail, diffs, and the cite-able test_errors.id / stack_frames.id values needed by hypothesis (action: record). Returns a typed JSON object in structuredContent; read structuredContent.errors[].",
200
121
  parameters: TestErrorsInput,
201
122
  success: TestErrorsResult,
202
123
  dependencies: [DataReader]
203
- }).annotate(Tool.Title, "Test errors").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestErrorsMarkdown(encoded));
124
+ }).annotate(Tool.Title, "Test errors").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
204
125
 
205
126
  //#endregion
206
- export { TestErrorAnnotation, TestErrorRow, TestErrorsInput, TestErrorsResult, formatTestErrorsMarkdown, handleTestErrors, testErrorsTool };
127
+ export { TestErrorAnnotation, TestErrorRow, TestErrorsInput, TestErrorsResult, handleTestErrors, testErrorsTool };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
@@ -35,28 +34,6 @@ const FailureSignatureGetResult = Schema.Union([SignatureFound, SignatureMissing
35
34
  title: "failure_signature_get result",
36
35
  description: "Discriminate on `found`. Found rows carry first/last-seen timestamps and recent occurrences."
37
36
  });
38
- const formatFailureSignatureMarkdown = (data) => {
39
- if (!data.found) return `No failure signature found with hash=${data.requestedHash}.`;
40
- const lines = [
41
- `# Failure Signature \`${data.signatureHash}\``,
42
- "",
43
- `**Hash:** ${data.signatureHash}`,
44
- "",
45
- `- first_seen_at: ${data.firstSeenAt}`,
46
- `- last_seen_at: ${data.lastSeenAt ?? "unknown"}`,
47
- `- first_seen_run_id: ${data.firstSeenRunId ?? "unknown"}`,
48
- `- occurrence_count: ${data.occurrenceCount}`
49
- ];
50
- if (data.recentErrors.length > 0) {
51
- lines.push("", "## Recent Errors", "");
52
- for (const e of data.recentErrors) lines.push(`- run=${e.runId} name=${e.errorName ?? "(none)"}: ${e.message.slice(0, 120)}`);
53
- }
54
- return lines.join("\n");
55
- };
56
- const FailureSignatureGetAsMarkdown = FailureSignatureGetResult.pipe(Schema.decodeTo(Schema.String, {
57
- decode: SchemaGetter.transform((data) => formatFailureSignatureMarkdown(data)),
58
- encode: SchemaGetter.forbidden(() => "FailureSignatureGetAsMarkdown is one-way.")
59
- }));
60
37
  /**
61
38
  * The `failure_signature_get` tool's parameters.
62
39
  *
@@ -85,11 +62,11 @@ const handleFailureSignatureGet = (input) => Effect.gen(function* () {
85
62
  * @public
86
63
  */
87
64
  const failureSignatureGetTool = Tool.make("failure_signature_get", {
88
- description: "Use when you have a failure-signature hash and need its first-seen date and occurrence history. Returns markdown in content[] and a typed JSON object in structuredContent ({ found, signatureHash?, firstSeenAt?, occurrenceCount?, recentErrors?[] } or absent variant).",
65
+ description: "Use when you have a failure-signature hash and need its first-seen date and occurrence history. Returns a typed JSON object in structuredContent ({ found, signatureHash?, firstSeenAt?, occurrenceCount?, recentErrors?[] } or absent variant).",
89
66
  parameters: FailureSignatureGetInput,
90
67
  success: FailureSignatureGetResult,
91
68
  dependencies: [DataReader]
92
- }).annotate(Tool.Title, "Failure signature").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatFailureSignatureMarkdown(encoded));
69
+ }).annotate(Tool.Title, "Failure signature").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
93
70
 
94
71
  //#endregion
95
- export { FailureSignatureGetInput, FailureSignatureGetResult, failureSignatureGetTool, formatFailureSignatureMarkdown, handleFailureSignatureGet };
72
+ export { FailureSignatureGetInput, FailureSignatureGetResult, failureSignatureGetTool, handleFailureSignatureGet };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
  import { CoverageTotals, FileCoverageReport } from "@vitest-agent/sdk";
@@ -44,37 +43,6 @@ const FileCoverageResult = Schema.Union([
44
43
  title: "file_coverage result",
45
44
  description: "Per-file coverage with related tests. Discriminate on `dataAvailable` then on `matched`."
46
45
  });
47
- const formatFileCoverageMarkdown = (data) => {
48
- if (!data.dataAvailable) return "No coverage data available. Run tests with coverage enabled.";
49
- const lines = [`# Coverage: \`${data.filePath}\``, ""];
50
- const metrics = [
51
- "statements",
52
- "branches",
53
- "functions",
54
- "lines"
55
- ];
56
- if (data.matched) {
57
- lines.push("## Metrics", "", "| Metric | Value | Threshold |", "| --- | --- | --- |");
58
- for (const metric of metrics) {
59
- const value = data.report.summary[metric];
60
- const threshold = data.globalThresholds[metric];
61
- const thresholdStr = threshold !== void 0 ? `${threshold}%` : "—";
62
- const icon = threshold !== void 0 && value < threshold ? "❌" : "✅";
63
- lines.push(`| ${metric} | ${icon} ${value.toFixed(2)}% | ${thresholdStr} |`);
64
- }
65
- if (data.report.uncoveredLines) lines.push("", "## Uncovered Lines", "", `\`${data.report.uncoveredLines}\``);
66
- lines.push("", "## Next steps", "", "- Use test({ action: \"for_file\" }) to find tests covering this file", "- Write tests targeting the uncovered lines");
67
- } else lines.push("This file is not in the low-coverage list.", "", "Possible reasons:", "- File meets all coverage thresholds", "- File was not included in the coverage run", "- File path does not match any tracked source file", "", "## Project Coverage Totals", "", "| Metric | Value |", "| --- | --- |", `| statements | ${data.totals.statements.toFixed(2)}% |`, `| branches | ${data.totals.branches.toFixed(2)}% |`, `| functions | ${data.totals.functions.toFixed(2)}% |`, `| lines | ${data.totals.lines.toFixed(2)}% |`);
68
- if (data.relatedTestFiles.length > 0) {
69
- lines.push("", "## Tests Covering This File", "");
70
- for (const tf of data.relatedTestFiles) lines.push(`- \`${tf}\``);
71
- }
72
- return lines.join("\n");
73
- };
74
- const FileCoverageAsMarkdown = FileCoverageResult.pipe(Schema.decodeTo(Schema.String, {
75
- decode: SchemaGetter.transform((data) => formatFileCoverageMarkdown(data)),
76
- encode: SchemaGetter.forbidden(() => "FileCoverageAsMarkdown is one-way.")
77
- }));
78
46
  /**
79
47
  * The `file_coverage` tool's parameters.
80
48
  *
@@ -123,11 +91,11 @@ const handleFileCoverage = (input) => Effect.gen(function* () {
123
91
  * @public
124
92
  */
125
93
  const fileCoverageTool = Tool.make("file_coverage", {
126
- description: "Use when you need coverage for one source file: per-metric values, uncovered lines, and related tests. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, matched?, filePath, report?, totals?, relatedTestFiles[] }).",
94
+ description: "Use when you need coverage for one source file: per-metric values, uncovered lines, and related tests. Returns a typed JSON object in structuredContent ({ dataAvailable, matched?, filePath, report?, totals?, relatedTestFiles[] }).",
127
95
  parameters: FileCoverageInput,
128
96
  success: FileCoverageResult,
129
97
  dependencies: [DataReader]
130
- }).annotate(Tool.Title, "File coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatFileCoverageMarkdown(encoded));
98
+ }).annotate(Tool.Title, "File coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
131
99
 
132
100
  //#endregion
133
- export { FileCoverageInput, FileCoverageResult, fileCoverageTool, formatFileCoverageMarkdown, handleFileCoverage };
101
+ export { FileCoverageInput, FileCoverageResult, fileCoverageTool, handleFileCoverage };
package/tools/help.js CHANGED
@@ -1,4 +1,3 @@
1
- import { RenderText } from "../annotations.js";
2
1
  import { Effect, Schema } from "effect";
3
2
  import { Tool } from "effect/unstable/ai";
4
3
 
@@ -11,7 +10,7 @@ import { Tool } from "effect/unstable/ai";
11
10
  const HelpResult = Schema.Struct({ helpText: Schema.String.annotate({ description: "Markdown table of every MCP tool with parameters and a one-line description." }) }).annotate({
12
11
  identifier: "HelpResult",
13
12
  title: "help result",
14
- description: "Static help reference. Read structuredContent.helpText programmatically; the same string lives in content[].text for transcripts."
13
+ description: "Static help reference. Read structuredContent.helpText."
15
14
  });
16
15
  /**
17
16
  * The static markdown `help` returns. Exported so `__test__/help-drift.test.ts`
@@ -43,7 +42,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
43
42
  | \`file_coverage\` | \`filePath\`, \`project?\` | Per-file coverage with uncovered lines and related tests |
44
43
  | \`test_history\` | \`project\` | Flaky/persistent/recovered tests |
45
44
  | \`test_trends\` | \`project\`, \`limit?\` | Coverage trajectory over time |
46
- | \`test_errors\` | \`project\`, \`errorName?\`, \`format?\` (\`markdown\` \\| \`xml\`) | Errors with diffs, stacks, each row's \`annotations[]\` (the test annotations the author recorded), and the cite-able \`testErrorId\` / \`topStackFrameId\` values needed by \`hypothesis (action: record)\` |
45
+ | \`test_errors\` | \`project\`, \`errorName?\` | Errors with diffs, stacks, each row's \`annotations[]\` (the test annotations the author recorded), and the cite-able \`testErrorId\` / \`topStackFrameId\` values needed by \`hypothesis (action: record)\` |
47
46
  | \`test\` | \`action\` (\`list\`/\`get\`/\`for_file\`/\`for_tag\`/\`annotations\`/\`artifacts\`), plus per-action params | Consolidated test inspection: list/get/for_file/for_tag/annotations/artifacts |
48
47
 
49
48
  \`test\` actions:
@@ -169,16 +168,15 @@ Six framing-only prompts (\`prompts/get\`; Claude Code surfaces them as slash co
169
168
  - \`scope\` accepts: \`global\`, \`project\`, \`module\`, \`suite\`, \`test\`, \`note\`
170
169
  `;
171
170
  /**
172
- * The Effect-native `help` tool. Renders the help markdown as the text
173
- * channel via `RenderText` while `structuredContent.helpText` carries the
174
- * same string for programmatic readers.
171
+ * The Effect-native `help` tool. `structuredContent.helpText` carries the
172
+ * markdown reference.
175
173
  *
176
174
  * @public
177
175
  */
178
176
  const helpTool = Tool.make("help", {
179
- description: "List all available MCP tools with parameters. Read structuredContent.helpText programmatically; the same markdown lives in content[].text.",
177
+ description: "List all available MCP tools with parameters. The markdown reference is in structuredContent.helpText.",
180
178
  success: HelpResult
181
- }).annotate(Tool.Title, "Help").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.helpText);
179
+ }).annotate(Tool.Title, "Help").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
182
180
  /**
183
181
  * Handler for {@link helpTool}.
184
182
  *
package/tools/history.js CHANGED
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
  import { HistoryRecord } from "@vitest-agent/sdk";
@@ -54,41 +53,6 @@ const TestHistoryResult = Schema.Struct({
54
53
  title: "test_history result",
55
54
  description: "Per-project flaky/persistent/recovered test classifications computed from `test_runs` history."
56
55
  });
57
- const formatTestHistoryMarkdown = (data) => {
58
- if (!data.hasData) return `No history data available for project \`${data.project}\`. Run tests first.`;
59
- const lines = [`# Test History: ${data.project}`, ""];
60
- if (data.flaky.length > 0) {
61
- lines.push("## Flaky Tests", "", "Tests with mixed pass/fail results across recent runs:", "");
62
- for (const test of data.flaky) {
63
- const total = test.passCount + test.failCount;
64
- const passRate = total > 0 ? (test.passCount / total * 100).toFixed(0) : "0";
65
- lines.push(`### ⚠️ ${test.fullName}`, "", `- Module: \`${test.modulePath}\``, `- Pass rate: ${passRate}% (${test.passCount}/${total})`, `- Last state: ${test.lastState}`, `- Last run: ${new Date(test.lastTimestamp).toLocaleString()}`, "");
66
- }
67
- }
68
- if (data.persistent.length > 0) {
69
- lines.push("## Persistent Failures", "", "Tests that have failed in consecutive runs:", "");
70
- for (const failure of data.persistent) {
71
- lines.push(`### ❌ ${failure.fullName}`, "", `- Module: \`${failure.modulePath}\``, `- Consecutive failures: ${failure.consecutiveFailures}`, `- First failed: ${new Date(failure.firstFailedAt).toLocaleString()}`, `- Last failed: ${new Date(failure.lastFailedAt).toLocaleString()}`);
72
- if (failure.lastErrorMessage !== null) lines.push(`- Last error: ${failure.lastErrorMessage}`);
73
- lines.push("");
74
- }
75
- }
76
- if (data.recovered.length > 0) {
77
- lines.push("## Recovered Tests", "", "Tests that previously failed but are now passing:", "");
78
- for (const test of data.recovered) {
79
- const runViz = test.recentRuns.map((s) => s === "passed" ? "P" : "F").join("");
80
- lines.push(`- ✅ **${test.fullName}** (${test.modulePath}) — recent runs: \`${runViz}\``);
81
- }
82
- lines.push("");
83
- }
84
- if (data.flaky.length === 0 && data.persistent.length === 0 && data.recovered.length === 0) lines.push("✅ No flaky, persistent, or recently recovered tests.", "");
85
- lines.push(`_History updated: ${data.history.updatedAt}_`);
86
- return lines.join("\n");
87
- };
88
- const TestHistoryAsMarkdown = TestHistoryResult.pipe(Schema.decodeTo(Schema.String, {
89
- decode: SchemaGetter.transform((data) => formatTestHistoryMarkdown(data)),
90
- encode: SchemaGetter.forbidden(() => "TestHistoryAsMarkdown is one-way: markdown cannot be parsed back to TestHistoryResult.")
91
- }));
92
56
  /**
93
57
  * The `test_history` tool's parameters.
94
58
  *
@@ -147,11 +111,11 @@ const handleTestHistory = (input) => Effect.gen(function* () {
147
111
  * @public
148
112
  */
149
113
  const testHistoryTool = Tool.make("test_history", {
150
- description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns markdown in content[] and a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
114
+ description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
151
115
  parameters: TestHistoryInput,
152
116
  success: TestHistoryResult,
153
117
  dependencies: [DataReader]
154
- }).annotate(Tool.Title, "Test history").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestHistoryMarkdown(encoded));
118
+ }).annotate(Tool.Title, "Test history").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
155
119
 
156
120
  //#endregion
157
- export { TestHistoryInput, TestHistoryResult, formatTestHistoryMarkdown, handleTestHistory, testHistoryTool };
121
+ export { TestHistoryInput, TestHistoryResult, handleTestHistory, testHistoryTool };
@@ -1,4 +1,3 @@
1
- import { RenderText } from "../annotations.js";
2
1
  import { McpSession } from "../session.js";
3
2
  import { IdempotentReplayMarker } from "../utils/replay-marker.js";
4
3
  import { Effect, Match, Option, Schema } from "effect";
@@ -42,20 +41,8 @@ const HypothesisResult = Schema.Union([
42
41
  title: "hypothesis result",
43
42
  description: "Discriminate on `action`. record returns the new id; list returns the matching rows; validate returns an empty acknowledgement."
44
43
  });
45
- const formatHypothesisListMarkdown = (data) => {
46
- if (data.action !== "list") return JSON.stringify(data, null, 2);
47
- if (data.hypotheses.length === 0) return "No hypotheses matched.";
48
- const lines = ["# Hypotheses", ""];
49
- for (const h of data.hypotheses) {
50
- const status = h.validationOutcome ?? "open";
51
- lines.push(`- [${status}] id=${h.id} session=${h.sessionId}: ${h.content.slice(0, 120)}`);
52
- }
53
- return lines.join("\n");
54
- };
55
44
  /** Number-or-numeric-string id: LLM orchestrators routinely stringify numeric tool inputs. */
56
45
  const CoercibleId = Schema.Union([Schema.Finite, Schema.FiniteFromString]);
57
- /** The text channel: `list` renders markdown; `record` / `validate` render the pretty-printed JSON. */
58
- const renderHypothesisText = (data) => data.action === "list" ? formatHypothesisListMarkdown(data) : JSON.stringify(data, null, 2);
59
46
  const RecordVariant = Schema.Struct({
60
47
  action: Schema.Literal("record").annotate({ description: "CRUD discriminator" }),
61
48
  tddTaskId: Schema.optionalKey(CoercibleId).annotate({ description: "record: tdd task id returned by tdd_task action='start' — binds the hypothesis to that task's session deterministically" }),
@@ -173,7 +160,7 @@ const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminators
173
160
  * @public
174
161
  */
175
162
  const hypothesisTool = Tool.make("hypothesis", {
176
- 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.",
163
+ 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.",
177
164
  parameters: HypothesisInput,
178
165
  success: HypothesisResult,
179
166
  dependencies: [
@@ -181,7 +168,7 @@ const hypothesisTool = Tool.make("hypothesis", {
181
168
  DataStore,
182
169
  McpSession
183
170
  ]
184
- }).annotate(Tool.Title, "Hypothesis").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false).annotate(RenderText, (encoded) => renderHypothesisText(encoded));
171
+ }).annotate(Tool.Title, "Hypothesis").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
185
172
 
186
173
  //#endregion
187
- export { HypothesisInput, HypothesisResult, formatHypothesisListMarkdown, handleHypothesis, hypothesisTool };
174
+ export { HypothesisInput, HypothesisResult, handleHypothesis, hypothesisTool };
@@ -1,6 +1,5 @@
1
- import { RenderText } from "../annotations.js";
2
1
  import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
3
- import { Effect, Match, Option, Schema, SchemaGetter } from "effect";
2
+ import { Effect, Match, Option, Schema } from "effect";
4
3
  import { DataReader } from "@vitest-agent/engine";
5
4
  import { Tool } from "effect/unstable/ai";
6
5
 
@@ -129,98 +128,6 @@ const InventoryResult = Schema.Union([
129
128
  title: "inventory result",
130
129
  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)."
131
130
  });
132
- const formatInventoryMarkdown = (data) => {
133
- if (data.inventoryKind === "project") {
134
- if (data.count === 0) return "No projects found. Run tests first.";
135
- const lines = [
136
- "## Projects",
137
- "",
138
- "| Project | Last Run | Result | Total | Passed | Failed | Skipped |",
139
- "| --- | --- | --- | --- | --- | --- | --- |"
140
- ];
141
- for (const p of data.projects) {
142
- const lastRun = p.lastRun ? p.lastRun.split("T")[0] : "—";
143
- const result = p.lastResult ?? "—";
144
- lines.push(`| ${p.project} | ${lastRun} | ${result} | ${p.total} | ${p.passed} | ${p.failed} | ${p.skipped} |`);
145
- }
146
- return lines.join("\n");
147
- }
148
- if (data.inventoryKind === "module") {
149
- if (data.count === 0) return "No modules found. Run run_tests({}) to execute tests and populate the database.";
150
- const lines = ["## Modules", ""];
151
- for (const g of data.groups) {
152
- lines.push(`### ${g.project}`, "", "| ID | File | State | Tests | Duration |", "| --- | --- | --- | --- | --- |");
153
- for (const m of g.modules) {
154
- const duration = m.duration !== null ? `${m.duration}ms` : "—";
155
- lines.push(`| ${m.id} | ${m.file} | ${m.state} | ${m.testCount} | ${duration} |`);
156
- }
157
- lines.push("");
158
- }
159
- return lines.join("\n").trimEnd();
160
- }
161
- if (data.inventoryKind === "suite") {
162
- if (data.count === 0) return "No suites found. Run run_tests({}) to execute tests and populate the database.";
163
- const lines = ["## Suites", ""];
164
- for (const g of data.groups) {
165
- lines.push(`### ${g.project}`, "", "| ID | Name | Module | State | Tests |", "| --- | --- | --- | --- | --- |");
166
- for (const s of g.suites) lines.push(`| ${s.id} | ${s.name} | ${s.module} | ${s.state} | ${s.testCount} |`);
167
- lines.push("");
168
- }
169
- return lines.join("\n").trimEnd();
170
- }
171
- if (data.inventoryKind === "session_detail") {
172
- if (!data.found) return `No session with id=${data.id}.`;
173
- const s = data.session;
174
- const lines = [
175
- `# Session ${s.id}`,
176
- "",
177
- `- chatId: \`${s.chatId}\``,
178
- `- project: ${s.project}`,
179
- `- agentKind: ${s.agentKind}${s.agentType !== null ? ` (${s.agentType})` : ""}`,
180
- `- started: ${s.startedAt}`,
181
- `- ended: ${s.endedAt ?? "still open"}`,
182
- `- triageWasNonEmpty: ${s.triageWasNonEmpty}`
183
- ];
184
- if (s.parentSessionId !== null) lines.push(`- parentSessionId: ${s.parentSessionId}`);
185
- return lines.join("\n");
186
- }
187
- if (data.inventoryKind === "tag_scoped") {
188
- if (data.count === 0) return `No tags recorded for project \`${data.project}\`. Run run_tests({}) to populate.`;
189
- const lines = [
190
- `## Tags — ${data.project}`,
191
- "",
192
- "| Tag | Modules | Tests |",
193
- "| --- | --- | --- |"
194
- ];
195
- for (const t of data.tags) lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} |`);
196
- return lines.join("\n");
197
- }
198
- if (data.inventoryKind === "tag_unscoped") {
199
- if (data.count === 0) return "No tags recorded. Run run_tests({}) to populate.";
200
- const lines = [
201
- "## Tags",
202
- "",
203
- "| Tag | Modules | Tests | Projects |",
204
- "| --- | --- | --- | --- |"
205
- ];
206
- for (const t of data.tags) {
207
- const projectsBreakdown = t.byProject.map((p) => `${p.project} (${p.testCount})`).join(", ");
208
- lines.push(`| ${t.tag} | ${t.moduleCount} | ${t.testCount} | ${projectsBreakdown} |`);
209
- }
210
- return lines.join("\n");
211
- }
212
- if (data.count === 0) return "No sessions recorded yet.";
213
- const lines = ["# Sessions", ""];
214
- for (const s of data.sessions) {
215
- const ended = s.endedAt ? `ended ${s.endedAt}` : "open";
216
- lines.push(`- **${s.chatId}** [${s.agentKind}] project=${s.project} started=${s.startedAt} ${ended}`);
217
- }
218
- return lines.join("\n");
219
- };
220
- const InventoryAsMarkdown = InventoryResult.pipe(Schema.decodeTo(Schema.String, {
221
- decode: SchemaGetter.transform((data) => formatInventoryMarkdown(data)),
222
- encode: SchemaGetter.forbidden(() => "InventoryAsMarkdown is one-way.")
223
- }));
224
131
  const ProjectVariant = Schema.Struct({ kind: Schema.Literal("project") });
225
132
  const ModuleVariant = Schema.Struct({
226
133
  kind: Schema.Literal("module"),
@@ -389,7 +296,7 @@ const inventoryTool = Tool.make("inventory", {
389
296
  parameters: InventoryInput,
390
297
  success: InventoryResult,
391
298
  dependencies: [DataReader]
392
- }).annotate(Tool.Title, "Inventory").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatInventoryMarkdown(encoded));
299
+ }).annotate(Tool.Title, "Inventory").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
393
300
 
394
301
  //#endregion
395
- export { InventoryInput, InventoryResult, formatInventoryMarkdown, handleInventory, inventoryTool };
302
+ export { InventoryInput, InventoryResult, handleInventory, inventoryTool };
package/tools/note.js CHANGED
@@ -1,4 +1,3 @@
1
- import { RenderText } from "../annotations.js";
2
1
  import { Effect, Match, Option, Schema } from "effect";
3
2
  import { DataReader, DataStore } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
@@ -78,45 +77,6 @@ const NoteResult = Schema.Union([
78
77
  title: "note result",
79
78
  description: "Discriminate on `action`. `get` further discriminates on `found`."
80
79
  });
81
- const renderNoteTable = (notes) => {
82
- const lines = ["| ID | Title | Scope | Project | Created |", "| --- | --- | --- | --- | --- |"];
83
- for (const n of notes) {
84
- const proj = n.project ?? "—";
85
- const created = n.createdAt.split("T")[0];
86
- lines.push(`| ${n.id} | ${n.title} | ${n.scope} | ${proj} | ${created} |`);
87
- }
88
- return lines.join("\n");
89
- };
90
- /**
91
- * Markdown rendering used at the boundary for note list/search
92
- * results. Mutations (create/get/update/delete) get JSON-stringify
93
- * via `structuredJsonResult` instead of a markdown view.
94
- */
95
- const formatNoteListMarkdown = (data) => {
96
- if (data.action === "list") {
97
- if (data.notes.length === 0) return "No notes found. Use note({ action: \"create\", ... }) to add notes.";
98
- return [
99
- "## Notes",
100
- "",
101
- renderNoteTable(data.notes)
102
- ].join("\n");
103
- }
104
- if (data.action === "search") {
105
- if (data.notes.length === 0) return "No notes matched.";
106
- return [
107
- `## Notes matching "${data.query}"`,
108
- "",
109
- renderNoteTable(data.notes)
110
- ].join("\n");
111
- }
112
- return JSON.stringify(data, null, 2);
113
- };
114
- /**
115
- * The text channel: list/search render markdown; the mutation actions
116
- * (create/get/update/delete) render the pretty-printed JSON, exactly as
117
- * the old `structuredJsonResult` boundary did.
118
- */
119
- const renderNoteText = (data) => data.action === "list" || data.action === "search" ? formatNoteListMarkdown(data) : JSON.stringify(data, null, 2);
120
80
  const CreateVariant = Schema.Struct({
121
81
  action: Schema.Literal("create").annotate({ description: "CRUD discriminator" }),
122
82
  title: Schema.String,
@@ -251,11 +211,11 @@ const handleNote = (input) => Match.value(input).pipe(Match.discriminatorsExhaus
251
211
  * @public
252
212
  */
253
213
  const noteTool = Tool.make("note", {
254
- 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.",
214
+ 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`).",
255
215
  parameters: NoteParams,
256
216
  success: NoteResult,
257
217
  dependencies: [DataReader, DataStore]
258
- }).annotate(Tool.Title, "Note").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false).annotate(RenderText, (encoded) => renderNoteText(encoded));
218
+ }).annotate(Tool.Title, "Note").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
259
219
 
260
220
  //#endregion
261
- export { NoteParams, NoteResult, formatNoteListMarkdown, handleNote, noteTool };
221
+ export { NoteParams, NoteResult, handleNote, noteTool };
package/tools/overview.js CHANGED
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
2
- import { Effect, Option, Schema, SchemaGetter } from "effect";
1
+ import { Effect, Option, Schema } from "effect";
3
2
  import { DataReader } from "@vitest-agent/engine";
4
3
  import { Tool } from "effect/unstable/ai";
5
4
 
@@ -40,37 +39,6 @@ const TestOverviewResult = Schema.Union([OverviewAvailable, OverviewAbsent]).ann
40
39
  title: "test_overview result",
41
40
  description: "Per-project run metrics. Discriminate on `dataAvailable` for cold-start handling."
42
41
  });
43
- const iconForResult = (r) => {
44
- if (r === "passed") return "✅";
45
- if (r === "failed") return "❌";
46
- if (r === "interrupted") return "⚠️";
47
- return "⬜";
48
- };
49
- const formatTestOverviewMarkdown = (data) => {
50
- if (!data.dataAvailable) {
51
- if (data.reason === "project_filter_empty") return `No test data found for project \`${data.projectFilter ?? "(unknown)"}\`. Run tests first.`;
52
- return "No test data available. Run tests first.";
53
- }
54
- const lines = ["# Test Overview", ""];
55
- const projectGroups = /* @__PURE__ */ new Map();
56
- for (const run of data.runs) {
57
- const group = projectGroups.get(run.project) ?? [];
58
- group.push(run);
59
- projectGroups.set(run.project, group);
60
- }
61
- for (const [projectName, projectRuns] of projectGroups) {
62
- lines.push(`## ${projectName}`, "");
63
- for (const run of projectRuns) {
64
- const lastRun = run.lastRun ? new Date(run.lastRun).toLocaleString() : "never";
65
- lines.push(`### ${iconForResult(run.lastResult)} ${run.project}`, "", "| Metric | Count |", "| --- | --- |", `| Total | ${run.total} |`, `| Passed | ${run.passed} |`, `| Failed | ${run.failed} |`, `| Skipped | ${run.skipped} |`, `| Last run | ${lastRun} |`, "");
66
- }
67
- }
68
- return lines.join("\n");
69
- };
70
- const TestOverviewAsMarkdown = TestOverviewResult.pipe(Schema.decodeTo(Schema.String, {
71
- decode: SchemaGetter.transform((data) => formatTestOverviewMarkdown(data)),
72
- encode: SchemaGetter.forbidden(() => "TestOverviewAsMarkdown is one-way.")
73
- }));
74
42
  /**
75
43
  * The `test_overview` tool's parameters.
76
44
  *
@@ -108,11 +76,11 @@ const handleTestOverview = (input) => Effect.gen(function* () {
108
76
  * @public
109
77
  */
110
78
  const testOverviewTool = Tool.make("test_overview", {
111
- description: "Use when you want a summary of the test landscape with per-project run metrics. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, projectFilter?, runs[] } or absent variant).",
79
+ description: "Use when you want a summary of the test landscape with per-project run metrics. Returns a typed JSON object in structuredContent ({ dataAvailable, projectFilter?, runs[] } or absent variant).",
112
80
  parameters: TestOverviewInput,
113
81
  success: TestOverviewResult,
114
82
  dependencies: [DataReader]
115
- }).annotate(Tool.Title, "Test overview").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestOverviewMarkdown(encoded));
83
+ }).annotate(Tool.Title, "Test overview").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
116
84
 
117
85
  //#endregion
118
- export { TestOverviewInput, TestOverviewResult, formatTestOverviewMarkdown, handleTestOverview, testOverviewTool };
86
+ export { TestOverviewInput, TestOverviewResult, handleTestOverview, testOverviewTool };