@vitest-agent/sdk 3.1.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +23 -19
  2. package/dispatch.d.ts +25 -6
  3. package/formatters/markdown.js +1 -0
  4. package/formatters/terminal.js +4 -3
  5. package/index.d.ts +448 -2341
  6. package/index.js +10 -68
  7. package/internal-inject-env.js +5 -7
  8. package/package.json +3 -19
  9. package/sidecar-dispatch.js +7 -5
  10. package/utils/format-console.js +9 -10
  11. package/utils/format-terminal.js +7 -7
  12. package/utils/posix-path.js +52 -0
  13. package/utils/test-location.js +11 -5
  14. package/version.js +17 -0
  15. package/layers/ConfigLive.js +0 -46
  16. package/layers/DataReaderLive.js +0 -1923
  17. package/layers/DataStoreLive.js +0 -1287
  18. package/layers/DetailResolverLive.js +0 -9
  19. package/layers/DiscoveryRegistryLive.js +0 -70
  20. package/layers/EnvironmentDetectorLive.js +0 -21
  21. package/layers/EnvironmentDetectorTest.js +0 -13
  22. package/layers/ExecutorResolverLive.js +0 -9
  23. package/layers/FormatSelectorLive.js +0 -20
  24. package/layers/HistoryTrackerLive.js +0 -49
  25. package/layers/HistoryTrackerTest.js +0 -25
  26. package/layers/LoggerLive.js +0 -64
  27. package/layers/OutputPipelineLive.js +0 -13
  28. package/layers/OutputRendererLive.js +0 -28
  29. package/layers/PathResolutionLive.js +0 -40
  30. package/layers/PerClientSessionMapLive.js +0 -145
  31. package/layers/ProjectDiscoveryLive.js +0 -68
  32. package/layers/ProjectDiscoveryTest.js +0 -12
  33. package/layers/ProjectIdentityLive.js +0 -96
  34. package/layers/RunContextLive.js +0 -81
  35. package/lib/format-triage.js +0 -103
  36. package/lib/format-wrapup.js +0 -67
  37. package/migrations/0001_initial.js +0 -824
  38. package/migrations/0002_test_artifacts.js +0 -30
  39. package/migrations/index.js +0 -26
  40. package/migrations/registry_0001_initial.js +0 -39
  41. package/migrations/session_map_0001_initial.js +0 -38
  42. package/services/Config.js +0 -15
  43. package/services/DataReader.js +0 -8
  44. package/services/DataStore.js +0 -15
  45. package/services/DetailResolver.js +0 -8
  46. package/services/DiscoveryRegistry.js +0 -8
  47. package/services/EnvironmentDetector.js +0 -8
  48. package/services/ExecutorResolver.js +0 -8
  49. package/services/FormatSelector.js +0 -8
  50. package/services/HistoryTracker.js +0 -20
  51. package/services/OutputRenderer.js +0 -8
  52. package/services/PerClientSessionMap.js +0 -22
  53. package/services/ProjectDiscovery.js +0 -8
  54. package/services/ProjectIdentity.js +0 -63
  55. package/services/RunContext.js +0 -53
  56. package/services/idempotency.js +0 -58
  57. package/sql/assemblers.js +0 -69
  58. package/testing/layers.js +0 -21
  59. package/testing.d.ts +0 -1862
  60. package/testing.js +0 -254
  61. package/utils/ensure-migrated.js +0 -52
  62. package/utils/failure-signature.js +0 -34
  63. package/utils/resolve-data-path.js +0 -65
  64. package/utils/resolve-project-key-from-cwd.js +0 -53
  65. package/utils/resolve-workspace-key.js +0 -41
@@ -1,96 +0,0 @@
1
- import { ProjectIdentityNotResolvableError } from "../errors/ProjectIdentityError.js";
2
- import { VitestAgentConfig } from "../schemas/Config.js";
3
- import { VitestAgentConfigFile } from "../services/Config.js";
4
- import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "../services/ProjectIdentity.js";
5
- import { Effect, FileSystem, Layer, Option } from "effect";
6
- import { WorkspaceDiscovery } from "@effected/workspaces";
7
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
8
-
9
- //#region src/layers/ProjectIdentityLive.ts
10
- const readGitRemote = (cwd) => Effect.gen(function* () {
11
- const trimmed = (yield* (yield* ChildProcessSpawner.ChildProcessSpawner).string(ChildProcess.make("git", [
12
- "config",
13
- "--get",
14
- "remote.origin.url"
15
- ]).pipe(ChildProcess.setCwd(cwd)))).trim();
16
- return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
17
- }).pipe(Effect.catch(() => Effect.succeed(Option.none())));
18
- const readPackageRepoUrl = (packageJsonPath) => Effect.gen(function* () {
19
- const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.catch(() => Effect.succeed("")));
20
- if (raw.length === 0) return Option.none();
21
- try {
22
- const repo = JSON.parse(raw).repository;
23
- if (typeof repo === "string" && repo.trim().length > 0) return Option.some(repo.trim());
24
- if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
25
- const url = repo.url.trim();
26
- return url.length > 0 ? Option.some(url) : Option.none();
27
- }
28
- } catch {}
29
- return Option.none();
30
- });
31
- const collectCandidates = (workspaceRoot, options) => Effect.gen(function* () {
32
- const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.catch(() => Effect.succeed(new VitestAgentConfig({}))));
33
- const gitRemote = yield* readGitRemote(workspaceRoot);
34
- const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.catch(() => Effect.succeed([])))).find((pkg) => pkg.isRootWorkspace);
35
- const packageJsonRepoUrl = root ? yield* readPackageRepoUrl(root.packageJsonPath) : Option.none();
36
- const candidates = {};
37
- if (options.projectId !== void 0) candidates.explicit = options.projectId;
38
- if (tomlConfig.projectKey !== void 0) candidates.toml = tomlConfig.projectKey;
39
- const remoteValue = Option.getOrUndefined(gitRemote);
40
- if (remoteValue !== void 0) candidates.gitRemote = remoteValue;
41
- const repoUrlValue = Option.getOrUndefined(packageJsonRepoUrl);
42
- if (repoUrlValue !== void 0) candidates.packageJsonRepoUrl = repoUrlValue;
43
- if (root?.name !== void 0) candidates.packageJsonName = root.name;
44
- return candidates;
45
- });
46
- const candidatesToTriedList = (candidates) => [
47
- {
48
- source: "explicit",
49
- reason: candidates.explicit ? "value rejected by normalizer" : "not provided in plugin options"
50
- },
51
- {
52
- source: "toml",
53
- reason: candidates.toml ? "value rejected by normalizer" : "not configured in vitest-agent.config.toml"
54
- },
55
- {
56
- source: "git-remote",
57
- reason: candidates.gitRemote ? "URL not canonicalizable" : "git command produced no remote.origin.url"
58
- },
59
- {
60
- source: "package-repository",
61
- reason: candidates.packageJsonRepoUrl ? "URL not canonicalizable" : "no repository.url in root package.json"
62
- },
63
- {
64
- source: "package-name",
65
- reason: candidates.packageJsonName ? "value rejected by normalizer" : "no name in root package.json"
66
- }
67
- ];
68
- /**
69
- * Live layer that wires the resolver into real I/O. Requires
70
- * `FileSystem`, `CommandExecutor`, `WorkspaceDiscovery`, and
71
- * `VitestAgentConfigFile`. The platform context comes from
72
- * `NodeServices.layer` at the entry point.
73
- * @public
74
- */
75
- const ProjectIdentityLive = Layer.effect(ProjectIdentity, Effect.gen(function* () {
76
- const fs = yield* FileSystem.FileSystem;
77
- const discovery = yield* WorkspaceDiscovery;
78
- const configFile = yield* VitestAgentConfigFile;
79
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
80
- return { resolve: (workspaceRoot, options = {}) => collectCandidates(workspaceRoot, options).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(WorkspaceDiscovery, discovery), Effect.provideService(VitestAgentConfigFile, configFile), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.flatMap((candidates) => {
81
- const result = resolveProjectIdentityFromCandidates(candidates);
82
- if (result === null) return Effect.fail(new ProjectIdentityNotResolvableError({ tried: candidatesToTriedList(candidates) }));
83
- return Effect.succeed(result);
84
- })) };
85
- }));
86
- /**
87
- * Build a Test layer that returns a fixed `ResolvedIdentity` (or
88
- * fails with `ProjectIdentityNotResolvableError`). Use in unit tests
89
- * that depend on `ProjectIdentity` but don't want to wire upstream
90
- * services.
91
- * @public
92
- */
93
- const ProjectIdentityTest = (result) => Layer.succeed(ProjectIdentity, { resolve: () => result instanceof ProjectIdentityNotResolvableError ? Effect.fail(result) : Effect.succeed(result) });
94
-
95
- //#endregion
96
- export { ProjectIdentityLive, ProjectIdentityTest };
@@ -1,81 +0,0 @@
1
- import { AgentContext, RunContext, RunContextService } from "../services/RunContext.js";
2
- import { probeHostMetadataFromEnv } from "../utils/probe-host-metadata.js";
3
- import { Effect, Layer, Option } from "effect";
4
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
5
-
6
- //#region src/layers/RunContextLive.ts
7
- const runGit = (spawner, cwd, args) => spawner.string(ChildProcess.make("git", args).pipe(ChildProcess.setCwd(cwd))).pipe(Effect.map((output) => {
8
- const trimmed = output.trim();
9
- return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
10
- }), Effect.catch(() => Effect.succeed(Option.none())));
11
- const captureRunContextEffect = (cwd, spawner, env) => Effect.gen(function* () {
12
- const branch = yield* runGit(spawner, cwd, [
13
- "rev-parse",
14
- "--abbrev-ref",
15
- "HEAD"
16
- ]);
17
- const sha = yield* runGit(spawner, cwd, ["rev-parse", "HEAD"]);
18
- const dirtyOut = yield* runGit(spawner, cwd, ["status", "--porcelain"]);
19
- const upstream = yield* runGit(spawner, cwd, [
20
- "rev-parse",
21
- "--abbrev-ref",
22
- "@{upstream}"
23
- ]);
24
- const worktree = yield* runGit(spawner, cwd, ["rev-parse", "--show-toplevel"]);
25
- const dirtyValue = Option.match(dirtyOut, {
26
- onNone: () => null,
27
- onSome: (out) => out.length > 0
28
- });
29
- const host = probeHostMetadataFromEnv(env);
30
- return new RunContext({
31
- gitBranch: Option.getOrNull(branch),
32
- gitCommitSha: Option.getOrNull(sha),
33
- gitDirty: dirtyValue,
34
- gitUpstream: Option.getOrNull(upstream),
35
- gitWorktreeDir: Option.getOrNull(worktree),
36
- hostSource: host.source,
37
- hostValue: host.value,
38
- hostMetadata: host.metadata
39
- });
40
- });
41
- const captureAgentContextEffect = (cwd, spawner) => Effect.gen(function* () {
42
- const branch = yield* runGit(spawner, cwd, [
43
- "rev-parse",
44
- "--abbrev-ref",
45
- "HEAD"
46
- ]);
47
- const sha = yield* runGit(spawner, cwd, ["rev-parse", "HEAD"]);
48
- const worktree = yield* runGit(spawner, cwd, ["rev-parse", "--show-toplevel"]);
49
- return new AgentContext({
50
- startGitBranch: Option.getOrNull(branch),
51
- startGitCommitSha: Option.getOrNull(sha),
52
- startWorktreeDir: Option.getOrNull(worktree)
53
- });
54
- });
55
- /**
56
- * Live layer. Requires `ChildProcessSpawner` (provided by
57
- * `NodeServices.layer` at the entry point). Reads `process.env`
58
- * directly for host-metadata probes — the env walk is pure but the
59
- * env source is process-global, captured once when the layer
60
- * constructs.
61
- * @public
62
- */
63
- const RunContextLive = Layer.effect(RunContextService, Effect.gen(function* () {
64
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
65
- return {
66
- captureRunContext: (cwd) => captureRunContextEffect(cwd, spawner, process.env),
67
- captureAgentContext: (cwd) => captureAgentContextEffect(cwd, spawner)
68
- };
69
- }));
70
- /**
71
- * Build a Test layer that returns fixed `RunContext` and
72
- * `AgentContext` values regardless of `cwd`.
73
- * @public
74
- */
75
- const RunContextTest = (fixture) => Layer.succeed(RunContextService, {
76
- captureRunContext: () => Effect.succeed(fixture.runContext),
77
- captureAgentContext: () => Effect.succeed(fixture.agentContext)
78
- });
79
-
80
- //#endregion
81
- export { RunContextLive, RunContextTest };
@@ -1,103 +0,0 @@
1
- import { DataReader } from "../services/DataReader.js";
2
- import { Effect, Option } from "effect";
3
-
4
- //#region src/lib/format-triage.ts
5
- /**
6
- * Generates an orientation triage markdown string for LLM agents.
7
- * Summarises recent test runs, active sessions, acceptance metrics,
8
- * and (forward-compat) the most recent TDD session.
9
- *
10
- * Error channel is `never` — all DataReader errors are swallowed and
11
- * replaced with empty defaults so the caller is guaranteed a string.
12
- * @public
13
- */
14
- const formatTriageEffect = (options = {}) => Effect.gen(function* () {
15
- const reader = yield* DataReader;
16
- const allProjects = yield* reader.getRunsByProject().pipe(Effect.orElseSucceed(() => []));
17
- const projects = options.project ? allProjects.filter((p) => p.project === options.project) : allProjects;
18
- const sessions = yield* reader.listSessions({}).pipe(Effect.orElseSucceed(() => []));
19
- const fallbackMetrics = {
20
- phaseEvidenceIntegrity: {
21
- total: 0,
22
- compliant: 0,
23
- ratio: 0
24
- },
25
- complianceHookResponsiveness: {
26
- total: 0,
27
- withFollowup: 0,
28
- ratio: 0
29
- },
30
- orientationUsefulness: {
31
- total: 0,
32
- referencedCount: 0,
33
- ratio: 0
34
- },
35
- antiPatternDetectionRate: {
36
- total: 0,
37
- cleanSessions: 0,
38
- ratio: 0
39
- }
40
- };
41
- const metrics = yield* reader.computeAcceptanceMetrics().pipe(Effect.orElseSucceed(() => fallbackMetrics));
42
- const openTddRaw = yield* reader.getTddTaskById(1).pipe(Effect.orElseSucceed(() => Option.none()));
43
- const lines = [];
44
- lines.push("## Vitest Agent Reporter — Orientation Triage");
45
- lines.push("");
46
- lines.push("### Available vitest-agent MCP tools (most useful)");
47
- lines.push("");
48
- lines.push("- `run_tests` — run Vitest programmatically; returns AgentReport + classifications");
49
- lines.push("- `test_errors` — failure detail (after a failing run)");
50
- lines.push("- `test_history` — failure classification series (after recurring failures)");
51
- lines.push("- `file_coverage` — per-file coverage gaps (after coverage drops)");
52
- lines.push("- `triage_brief` — orient on the current test landscape");
53
- lines.push("");
54
- if (projects.length > 0) {
55
- lines.push("### Recent Test Runs");
56
- lines.push("");
57
- for (const p of projects) {
58
- const status = p.lastResult ?? "unknown";
59
- const counts = `${p.passed} passed, ${p.failed} failed`;
60
- lines.push(`- **${p.project}** — ${status} (${counts})`);
61
- }
62
- lines.push("");
63
- } else {
64
- lines.push("### Recent Test Runs");
65
- lines.push("");
66
- lines.push("_No test runs recorded yet._");
67
- lines.push("");
68
- }
69
- lines.push("### Session Log");
70
- lines.push("");
71
- if (sessions.length > 0) for (const s of sessions) {
72
- const end = s.endedAt ? ` → ended ${s.endedAt}` : " → active";
73
- lines.push(`- session \`${s.chatId}\` (${s.agentKind}) started ${s.startedAt}${end}`);
74
- }
75
- else lines.push("_No session data recorded yet._");
76
- lines.push("");
77
- lines.push("### Acceptance Metrics");
78
- lines.push("");
79
- const pct = (r) => `${(r * 100).toFixed(0)}%`;
80
- lines.push(`- Phase evidence integrity: ${pct(metrics.phaseEvidenceIntegrity.ratio)} (${metrics.phaseEvidenceIntegrity.compliant}/${metrics.phaseEvidenceIntegrity.total})`);
81
- lines.push(`- Compliance hook responsiveness: ${pct(metrics.complianceHookResponsiveness.ratio)} (${metrics.complianceHookResponsiveness.withFollowup}/${metrics.complianceHookResponsiveness.total})`);
82
- lines.push(`- Orientation usefulness: ${pct(metrics.orientationUsefulness.ratio)} (${metrics.orientationUsefulness.referencedCount}/${metrics.orientationUsefulness.total})`);
83
- lines.push(`- Anti-pattern detection: ${pct(metrics.antiPatternDetectionRate.ratio)} (${metrics.antiPatternDetectionRate.cleanSessions}/${metrics.antiPatternDetectionRate.total})`);
84
- lines.push("");
85
- if (Option.isSome(openTddRaw)) {
86
- const tdd = openTddRaw.value;
87
- lines.push("### Open TDD Session");
88
- lines.push("");
89
- lines.push(`- Goal: ${tdd.goal}`);
90
- lines.push(`- Started: ${tdd.startedAt}`);
91
- lines.push(`- Phases recorded: ${tdd.phases.length}`);
92
- lines.push("");
93
- }
94
- const out = lines.join("\n");
95
- if (options.maxLines !== void 0) {
96
- const arr = out.split("\n");
97
- if (arr.length > options.maxLines) return arr.slice(0, options.maxLines).join("\n");
98
- }
99
- return out;
100
- });
101
-
102
- //#endregion
103
- export { formatTriageEffect };
@@ -1,67 +0,0 @@
1
- import { DataReader } from "../services/DataReader.js";
2
- import { Effect, Option } from "effect";
3
-
4
- //#region src/lib/format-wrapup.ts
5
- const FAILURE_PROMPT_PATTERN = /\b(?:test fail|fix\b[^.]*\btest|why\b[^.]*\bfail(?:ing)?|broken test)\b/i;
6
- /** @public */
7
- const formatWrapupEffect = (options) => Effect.gen(function* () {
8
- const reader = yield* DataReader;
9
- if (options.kind === "user_prompt_nudge") {
10
- const hint = options.userPromptHint ?? "";
11
- if (!FAILURE_PROMPT_PATTERN.test(hint)) return "";
12
- return [
13
- "<vitest-agent-nudge>",
14
- "Before fixing, consider checking with test_history (is this flaky?) and failure_signature_get (have we seen this failure before?).",
15
- "</vitest-agent-nudge>"
16
- ].join("\n");
17
- }
18
- let sessionId = null;
19
- let agentKind = "main";
20
- if (options.sessionId !== void 0) {
21
- sessionId = options.sessionId;
22
- const opt = yield* reader.getSessionById(options.sessionId).pipe(Effect.orElseSucceed(() => Option.none()));
23
- if (Option.isSome(opt)) agentKind = opt.value.agentKind;
24
- } else if (options.chatId !== void 0) {
25
- const opt = yield* reader.getSessionByChatId(options.chatId).pipe(Effect.orElseSucceed(() => Option.none()));
26
- if (Option.isSome(opt)) {
27
- sessionId = opt.value.id;
28
- agentKind = opt.value.agentKind;
29
- }
30
- }
31
- if (options.kind === "tdd_handoff") {
32
- if (sessionId === null) return "";
33
- const tddOpt = yield* reader.getTddTaskById(1).pipe(Effect.orElseSucceed(() => Option.none()));
34
- if (Option.isNone(tddOpt) || tddOpt.value.sessionId !== sessionId) return "";
35
- const tdd = tddOpt.value;
36
- return [
37
- `Subagent tdd-orchestrator (tddTaskId=${tdd.id}) finished.`,
38
- `Outcome: ${tdd.outcome ?? "in progress"}. Goal: "${tdd.goal}".`,
39
- `Phases: ${tdd.phases.length}. Artifacts: ${tdd.artifacts.length}.`,
40
- `[Read full summary: tdd_task({ action: "get", tddTaskId: ${tdd.id} })]`,
41
- `[Resume: /tdd resume:${tdd.id}]`
42
- ].join("\n");
43
- }
44
- if (sessionId === null) return "";
45
- const fileEditCount = (yield* reader.searchTurns({
46
- sessionId,
47
- limit: 50
48
- }).pipe(Effect.orElseSucceed(() => []))).filter((t) => t.type === "file_edit").length;
49
- const openHypotheses = (yield* reader.listHypotheses({
50
- sessionId,
51
- limit: 50
52
- }).pipe(Effect.orElseSucceed(() => []))).filter((h) => h.validationOutcome === null);
53
- if (fileEditCount === 0 && openHypotheses.length === 0) return "";
54
- const isSubagent = agentKind === "subagent";
55
- const lines = [];
56
- if (isSubagent) {
57
- if (fileEditCount > 0 && openHypotheses.length === 0) lines.push(`- ${fileEditCount} recent file edit${fileEditCount === 1 ? "" : "s"} but no hypotheses recorded. Did your changes have a hypothesis? Use \`hypothesis({ action: "record" })\` to externalize the why before continuing.`);
58
- if (openHypotheses.length > 0) lines.push(`- ${openHypotheses.length} open hypothesis(es). Use \`hypothesis({ action: "validate", id, outcome: "confirmed"|"refuted"|"abandoned" })\` to mark them.`);
59
- }
60
- if (options.kind === "session_end") lines.push("- Use `note({ action: \"create\" })` to record any insights worth carrying forward.");
61
- if (options.kind === "pre_compact") lines.push("- What from this session matters next? Record it now via `note({ action: \"create\" })` before context compaction.");
62
- if (lines.length === 0) return "";
63
- return [options.kind === "stop" ? "## Before you finish" : "## Session wrap-up", ...lines].join("\n");
64
- });
65
-
66
- //#endregion
67
- export { formatWrapupEffect };