@vitest-agent/cli 2.2.18 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js ADDED
@@ -0,0 +1,58 @@
1
+ import { CURRENT_CLI_VERSION } from "./version.js";
2
+ import { agentCommand } from "./commands/agent.js";
3
+ import { dbCommand } from "./commands/db.js";
4
+ import { doctorCommand } from "./commands/doctor.js";
5
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
6
+ import * as NodeServices from "@effect/platform-node/NodeServices";
7
+ import { PathResolutionLive, PlatformLive, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectDir } from "@vitest-agent/engine";
8
+ import { formatFatalError } from "@vitest-agent/sdk";
9
+ import { Cause, Console, Effect } from "effect";
10
+ import { Command } from "effect/unstable/cli";
11
+
12
+ //#region src/main.ts
13
+ /**
14
+ * Assembled CLI program. Owns the process: resolves the data path, wires the
15
+ * engine's platform layers, and runs the root command via
16
+ * `NodeRuntime.runMain`.
17
+ *
18
+ * `bin.ts` is the published bin shim (`#!/usr/bin/env node` +
19
+ * `main()`); this module is also published as the `./main` subpath so a
20
+ * consumer can invoke the assembled program without going through the shim.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ const rootCommand = Command.make("vitest-agent").pipe(Command.withSubcommands([
25
+ dbCommand,
26
+ doctorCommand,
27
+ agentCommand
28
+ ]));
29
+ /**
30
+ * Assembles and runs the CLI program, taking over the process. Not
31
+ * re-exported from `index.ts` — a library consumer's import graph must not
32
+ * pull in the process-owning module.
33
+ *
34
+ * @public
35
+ */
36
+ const main = () => {
37
+ const env = process.env;
38
+ const logLevel = resolveLogLevel(env);
39
+ const logFile = resolveLogFile(env);
40
+ const projectDir = resolveProjectDir({
41
+ env,
42
+ cwd: process.cwd()
43
+ });
44
+ const cli = Command.run(rootCommand, { version: CURRENT_CLI_VERSION });
45
+ const program = resolveDataPath(projectDir).pipe(Effect.flatMap((dbPath) => cli.pipe(Effect.provide(PlatformLive({
46
+ dbPath,
47
+ env,
48
+ logLevel,
49
+ logFile
50
+ })))), Effect.provide(PathResolutionLive(projectDir)), Effect.provide(NodeServices.layer), Effect.catchCause((cause) => {
51
+ if (cause.reasons.filter(Cause.isDieReason).length > 0) return Console.error(`vitest-agent: ${formatFatalError(cause)}`).pipe(Effect.andThen(Effect.failCause(cause)));
52
+ return Effect.failCause(cause);
53
+ }));
54
+ NodeRuntime.runMain(program);
55
+ };
56
+
57
+ //#endregion
58
+ export { main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/cli",
3
- "version": "2.2.18",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "description": "On-demand CLI for vitest-agent. Reads cached test data and reports status, overview, coverage, history, trends, and cache health.",
6
6
  "keywords": [
@@ -32,18 +32,24 @@
32
32
  "import": "./index.js",
33
33
  "default": "./index.js"
34
34
  },
35
+ "./main": {
36
+ "types": "./main.d.ts",
37
+ "import": "./main.js",
38
+ "default": "./main.js"
39
+ },
35
40
  "./package.json": "./package.json"
36
41
  },
37
42
  "bin": {
38
43
  "vitest-agent": "bin/vitest-agent.js"
39
44
  },
40
45
  "dependencies": {
41
- "@effect/platform-node": "4.0.0-rc.112",
42
- "@effect/sql-sqlite-node": "4.0.0-rc.112",
43
- "@effected/workspaces": "^0.20.3",
44
- "@vitest-agent/sdk": "3.1.1",
45
- "@vitest-agent/sidecar": "2.1.18",
46
- "effect": "4.0.0-rc.112"
46
+ "@effect/platform-node": "4.0.0-rc.115",
47
+ "@effect/sql-sqlite-node": "4.0.0-rc.115",
48
+ "@effected/workspaces": "^0.21.1",
49
+ "@vitest-agent/engine": "0.1.0",
50
+ "@vitest-agent/sdk": "4.0.0",
51
+ "@vitest-agent/sidecar": "2.1.20",
52
+ "effect": "4.0.0-rc.115"
47
53
  },
48
54
  "engines": {
49
55
  "node": ">=24.11.0"
package/version.js ADDED
@@ -0,0 +1,13 @@
1
+ //#region src/version.ts
2
+ /**
3
+ * The version of this package, inlined at build time from
4
+ * `package.json#version` via rslib-builder's `__PACKAGE_VERSION__` substitution.
5
+ * Exported for version introspection by downstream tooling, and consumed by
6
+ * `main.ts` to back `Command.run`'s `version` option.
7
+ *
8
+ * @public
9
+ */
10
+ const CURRENT_CLI_VERSION = "3.0.0";
11
+
12
+ //#endregion
13
+ export { CURRENT_CLI_VERSION };
package/layers/CliLive.js DELETED
@@ -1,29 +0,0 @@
1
- import * as NodeServices from "@effect/platform-node/NodeServices";
2
- import { layer } from "@effect/sql-sqlite-node/SqliteClient";
3
- import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
4
- import { DataReaderLive, DataStoreLive, HistoryTrackerLive, LoggerLive, OutputPipelineLive, PROJECT_MIGRATIONS, ProjectDiscoveryLive } from "@vitest-agent/sdk";
5
- import { Layer } from "effect";
6
-
7
- //#region src/layers/CliLive.ts
8
- /**
9
- * Composition layer for the CLI runtime.
10
- *
11
- * Wires `DataReader`, `ProjectDiscovery`, `HistoryTracker`,
12
- * `OutputPipeline`, `SqliteClient`, the DB migrator, the Node platform
13
- * services, and `Logger` into a single layer the `vitest-agent`
14
- * bin provides to `Command.run`.
15
- *
16
- * @param dbPath - absolute path to the per-project `data.db`
17
- * @param logLevel - optional log level override; defaults to `Info`
18
- * @param logFile - optional path for structured log output
19
- * @public
20
- */
21
- const CliLive = (dbPath, logLevel, logFile) => {
22
- const SqliteLayer = layer({ filename: dbPath });
23
- const PlatformLayer = NodeServices.layer;
24
- const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(PROJECT_MIGRATIONS) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
25
- return Layer.mergeAll(ProjectDiscoveryLive, HistoryTrackerLive, OutputPipelineLive).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(DataStoreLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
26
- };
27
-
28
- //#endregion
29
- export { CliLive };
@@ -1,33 +0,0 @@
1
- import * as NodeServices from "@effect/platform-node/NodeServices";
2
- import { layer } from "@effect/sql-sqlite-node/SqliteClient";
3
- import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
4
- import { DataReaderLive, DataStoreLive, DiscoveryRegistryLive, LoggerLive, PROJECT_MIGRATIONS, PerClientSessionMapWriterLive, RunContextLive, registryMigration0001, sessionMapMigration0001 } from "@vitest-agent/sdk";
5
- import { Layer } from "effect";
6
-
7
- //#region src/layers/SidecarLive.ts
8
- /**
9
- * Build the sidecar Live layer for the supplied SQLite paths.
10
- *
11
- * Each store gets its own `SqlClient` connection (separate scopes,
12
- * independent migrators) so concurrent operations on the three
13
- * stores don't share lock state.
14
- *
15
- * @param paths - the three SQLite database paths to open
16
- * @public
17
- */
18
- const SidecarLive = (paths) => {
19
- const PlatformLayer = NodeServices.layer;
20
- const ProjectSqliteLayer = layer({ filename: paths.perProjectDbPath });
21
- const ProjectMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(PROJECT_MIGRATIONS) }).pipe(Layer.provide(Layer.merge(ProjectSqliteLayer, PlatformLayer)));
22
- const ProjectStoreLayer = Layer.mergeAll(DataStoreLive.pipe(Layer.provide(ProjectSqliteLayer)), DataReaderLive.pipe(Layer.provide(ProjectSqliteLayer)), ProjectMigratorLayer);
23
- const SessionMapSqliteLayer = layer({ filename: paths.sessionMapDbPath });
24
- const SessionMapMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": sessionMapMigration0001 }) }).pipe(Layer.provide(Layer.merge(SessionMapSqliteLayer, PlatformLayer)));
25
- const SessionMapLayer = Layer.mergeAll(PerClientSessionMapWriterLive.pipe(Layer.provide(SessionMapSqliteLayer)), SessionMapMigratorLayer);
26
- const RegistrySqliteLayer = layer({ filename: paths.registryDbPath });
27
- const RegistryMigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": registryMigration0001 }) }).pipe(Layer.provide(Layer.merge(RegistrySqliteLayer, PlatformLayer)));
28
- const RegistryLayer = Layer.mergeAll(DiscoveryRegistryLive.pipe(Layer.provide(RegistrySqliteLayer)), RegistryMigratorLayer);
29
- return Layer.mergeAll(ProjectStoreLayer, SessionMapLayer, RegistryLayer, RunContextLive).pipe(Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive()));
30
- };
31
-
32
- //#endregion
33
- export { SidecarLive };
@@ -1,27 +0,0 @@
1
- import { DataStore, PerClientSessionMapWriter } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
-
4
- //#region src/lib/internal-end-agent.ts
5
- /**
6
- * Sidecar `_internal end-agent` implementation.
7
- *
8
- * Sets `agents.ended_at` on the per-project store. For main-agent
9
- * stops (SessionEnd), the caller also passes `--host-session-id` so
10
- * the per-client session map's `ended_at` is updated and
11
- * `lookupByProjectDir` no longer returns the row as the active
12
- * session. For subagent stops (SubagentStop), the host session stays
13
- * open — only the subagent's `agents` row is closed.
14
- *
15
- * @packageDocumentation
16
- */
17
- /**
18
- * End an agent. Closes the `agents` row, optionally also closes the
19
- * `session_map` row.
20
- */
21
- const endAgentEffect = (input) => Effect.gen(function* () {
22
- yield* (yield* DataStore).endAgent(input.agentId, input.endedAt);
23
- if (input.hostSessionId !== void 0) yield* (yield* PerClientSessionMapWriter).endSession(input.hostSessionId, input.endedAt);
24
- });
25
-
26
- //#endregion
27
- export { endAgentEffect };
@@ -1,81 +0,0 @@
1
- import { DataReader, DataStore, PerClientSessionMapWriter, RunContextService, deriveIdempotencyKey } from "@vitest-agent/sdk";
2
- import { Effect, Option } from "effect";
3
-
4
- //#region src/lib/internal-register-agent.ts
5
- const deriveDefaultClientNonce = (input) => {
6
- const parent = input.parentAgentId ?? "__ROOT__";
7
- return `${input.hostSessionId}|${input.agentType}|${parent}`;
8
- };
9
- /**
10
- * End-to-end agent registration.
11
- *
12
- * Returns the canonical `agentId` + the conversation it belongs to +
13
- * the resolved `idempotencyKey`. The `idempotencyHit` flag is `true`
14
- * when the per-project store recovered an existing agent row instead
15
- * of inserting a new one.
16
- *
17
- * @param input - registration inputs including host identifiers and agent metadata
18
- * @returns an Effect resolving to `RegisterAgentOutput`
19
- * @public
20
- */
21
- const registerAgentEffect = (input) => Effect.gen(function* () {
22
- const sessionMap = yield* PerClientSessionMapWriter;
23
- const reader = yield* DataReader;
24
- const store = yield* DataStore;
25
- const ctx = yield* RunContextService;
26
- const conversationId = yield* sessionMap.mapConversation(input.transcriptPath);
27
- const sessionMapping = yield* sessionMap.mapSession({
28
- hostSessionId: input.hostSessionId,
29
- conversationId,
30
- projectKey: input.projectKey,
31
- projectDir: input.cwd
32
- });
33
- const existingSession = yield* reader.getSessionByChatId(input.hostSessionId);
34
- const sessionRowId = yield* Option.match(existingSession, {
35
- onNone: () => store.writeSession({
36
- chatId: input.hostSessionId,
37
- project: input.projectKey,
38
- cwd: input.cwd,
39
- agentKind: input.parentAgentId === void 0 ? "main" : "subagent",
40
- agentType: input.agentType,
41
- triageWasNonEmpty: false,
42
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
43
- conversationId
44
- }),
45
- onSome: (s) => Effect.succeed(s.id)
46
- });
47
- yield* store.setSessionConversationIdIfNull({
48
- sessionId: sessionRowId,
49
- conversationId
50
- });
51
- const agentContext = yield* ctx.captureAgentContext(input.cwd);
52
- const clientNonce = input.clientNonce ?? deriveDefaultClientNonce(input);
53
- const idempotencyKey = deriveIdempotencyKey({
54
- agentType: input.agentType,
55
- parentAgentId: input.parentAgentId ?? null,
56
- clientNonce
57
- });
58
- const result = yield* store.registerAgent({
59
- sessionId: sessionRowId,
60
- agentType: input.agentType,
61
- parentAgentId: input.parentAgentId ?? null,
62
- conversationId,
63
- startedAt: Math.floor(Date.now() / 1e3),
64
- ...agentContext.startGitBranch !== null && { startGitBranch: agentContext.startGitBranch },
65
- ...agentContext.startGitCommitSha !== null && { startGitCommitSha: agentContext.startGitCommitSha },
66
- ...agentContext.startWorktreeDir !== null && { startWorktreeDir: agentContext.startWorktreeDir },
67
- idempotencyKey,
68
- agentId: sessionMapping.mainAgentId
69
- });
70
- const isHit = result._tag === "IdempotencyHit";
71
- return {
72
- agentId: isHit ? result.existingAgentId : result.agentId,
73
- conversationId,
74
- mainAgentId: sessionMapping.mainAgentId,
75
- idempotencyKey,
76
- idempotencyHit: isHit
77
- };
78
- });
79
-
80
- //#endregion
81
- export { registerAgentEffect };
@@ -1,33 +0,0 @@
1
- import { DataReader, DataStore } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
-
4
- //#region src/lib/record-session.ts
5
- const recordSessionStart = (input) => Effect.gen(function* () {
6
- const reader = yield* DataReader;
7
- const store = yield* DataStore;
8
- let parentSessionId;
9
- if (input.parentChatId !== void 0) {
10
- const parent = yield* reader.getSessionByChatId(input.parentChatId);
11
- if (parent._tag === "Some") parentSessionId = parent.value.id;
12
- }
13
- return { sessionId: yield* store.upsertSession({
14
- chatId: input.chatId,
15
- project: input.project,
16
- cwd: input.cwd,
17
- agentKind: input.agentKind,
18
- ...input.agentType !== void 0 && { agentType: input.agentType },
19
- ...parentSessionId !== void 0 && { parentSessionId },
20
- triageWasNonEmpty: input.triageWasNonEmpty,
21
- startedAt: input.startedAt
22
- }) };
23
- });
24
- const recordSessionEnd = (input) => Effect.gen(function* () {
25
- const reader = yield* DataReader;
26
- const store = yield* DataStore;
27
- if ((yield* reader.getSessionByChatId(input.chatId))._tag === "None") return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${input.chatId}`));
28
- yield* store.endSession(input.chatId, input.endedAt, input.endReason);
29
- return { ok: true };
30
- });
31
-
32
- //#endregion
33
- export { recordSessionEnd, recordSessionStart };
@@ -1,104 +0,0 @@
1
- import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
2
- import { DataReader, DataStore } from "@vitest-agent/sdk";
3
- import { Effect, Option } from "effect";
4
-
5
- //#region src/lib/record-tdd-artifact.ts
6
- /**
7
- * Resolve (auto-opening a `spike` phase if needed) the current open
8
- * phase for `tddTaskId`, then write the artifact under it. Shared by
9
- * both `recordTddArtifactEffect` (chat-id → session → task resolution)
10
- * and `recordTddArtifactByTaskIdEffect` (explicit task-id escape hatch).
11
- */
12
- const writeArtifactUnderOpenPhase = (tddTaskId, input) => Effect.gen(function* () {
13
- const reader = yield* DataReader;
14
- const store = yield* DataStore;
15
- const phaseOpt = yield* reader.getCurrentTddPhase(tddTaskId);
16
- const phaseId = Option.isSome(phaseOpt) ? phaseOpt.value.id : (yield* store.writeTddPhase({
17
- tddTaskId,
18
- phase: "spike",
19
- startedAt: input.recordedAt,
20
- transitionReason: "auto-opened by record tdd-artifact (no prior phase)"
21
- })).id;
22
- return {
23
- id: yield* store.writeTddArtifact({
24
- phaseId,
25
- artifactKind: input.artifactKind,
26
- ...input.fileId !== void 0 && { fileId: input.fileId },
27
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
28
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
29
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
30
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
31
- ...input.suite !== void 0 && { suite: input.suite },
32
- recordedAt: input.recordedAt
33
- }),
34
- phaseId
35
- };
36
- });
37
- const recordTddArtifactEffect = (input) => Effect.gen(function* () {
38
- const reader = yield* DataReader;
39
- const session = yield* resolveSessionForRecording({
40
- chatId: input.chatId,
41
- recordedAt: input.recordedAt,
42
- ...input.project !== void 0 && { project: input.project },
43
- ...input.cwd !== void 0 && { cwd: input.cwd }
44
- });
45
- const openTdd = (yield* reader.listTddTasksForSession(session.id, {
46
- walkParents: true,
47
- walkConversation: true
48
- })).find((t) => t.endedAt === null);
49
- if (openTdd === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`No open TDD task under chat_id ${input.chatId}. Call tdd_task start first.`));
50
- return yield* writeArtifactUnderOpenPhase(openTdd.id, input);
51
- });
52
- /**
53
- * Explicit task-id escape hatch (issue #144). Bypasses `chat_id` →
54
- * session → task resolution entirely — the caller already knows which
55
- * TDD task the artifact belongs to (e.g. a hook seeded with
56
- * `VITEST_AGENT_TDD_TASK_ID` in a detached-session environment where
57
- * neither the parent walk nor the conversation-id fallback resolves
58
- * the right task). Fails loudly when the task does not exist or has
59
- * already ended — writing to a closed task's phase would silently
60
- * corrupt the evidence trail.
61
- */
62
- const recordTddArtifactByTaskIdEffect = (input) => Effect.gen(function* () {
63
- const taskOpt = yield* (yield* DataReader).getTddTaskById(input.tddTaskId);
64
- if (Option.isNone(taskOpt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`No TDD task found for id ${input.tddTaskId}.`));
65
- if (taskOpt.value.endedAt !== null) return yield* Effect.fail(/* @__PURE__ */ new Error(`TDD task ${input.tddTaskId} has already ended (endedAt=${taskOpt.value.endedAt}).`));
66
- return yield* writeArtifactUnderOpenPhase(input.tddTaskId, input);
67
- });
68
- /**
69
- * CLI-command-facing dispatcher (issue #144). Keeps `commands/record.ts`
70
- * a thin flag-parsing wrapper per this package's convention. `tddTaskId`
71
- * takes priority when both are supplied — it is the explicit escape
72
- * hatch and should never silently fall back to (weaker) session
73
- * resolution.
74
- */
75
- const dispatchRecordTddArtifactEffect = (input) => {
76
- if (input.tddTaskId !== void 0) return recordTddArtifactByTaskIdEffect({
77
- tddTaskId: input.tddTaskId,
78
- artifactKind: input.artifactKind,
79
- ...input.fileId !== void 0 && { fileId: input.fileId },
80
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
81
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
82
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
83
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
84
- ...input.suite !== void 0 && { suite: input.suite },
85
- recordedAt: input.recordedAt
86
- });
87
- if (input.chatId !== void 0) return recordTddArtifactEffect({
88
- chatId: input.chatId,
89
- artifactKind: input.artifactKind,
90
- ...input.fileId !== void 0 && { fileId: input.fileId },
91
- ...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
92
- ...input.testRunId !== void 0 && { testRunId: input.testRunId },
93
- ...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
94
- ...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
95
- ...input.suite !== void 0 && { suite: input.suite },
96
- recordedAt: input.recordedAt,
97
- ...input.cwd !== void 0 && { cwd: input.cwd },
98
- ...input.project !== void 0 && { project: input.project }
99
- });
100
- return Effect.fail(/* @__PURE__ */ new Error("record tdd-artifact requires either --chat-id or --tdd-task-id."));
101
- };
102
-
103
- //#endregion
104
- export { dispatchRecordTddArtifactEffect, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect };
@@ -1,44 +0,0 @@
1
- import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
2
- import { DataStore, TurnPayload } from "@vitest-agent/sdk";
3
- import { Effect, Schema } from "effect";
4
-
5
- //#region src/lib/record-turn.ts
6
- const parseAndValidateTurnPayload = (raw) => {
7
- let parsed;
8
- try {
9
- parsed = JSON.parse(raw);
10
- } catch (e) {
11
- return {
12
- ok: false,
13
- error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
14
- };
15
- }
16
- const decoded = Schema.decodeUnknownResult(TurnPayload)(parsed);
17
- if (decoded._tag === "Failure") return {
18
- ok: false,
19
- error: `Invalid TurnPayload: ${decoded.failure.message}`
20
- };
21
- return {
22
- ok: true,
23
- payload: decoded.success
24
- };
25
- };
26
- const recordTurnEffect = (input) => Effect.gen(function* () {
27
- const parse = parseAndValidateTurnPayload(input.payloadJson);
28
- if (!parse.ok) return yield* Effect.fail(new Error(parse.error));
29
- const session = yield* resolveSessionForRecording({
30
- chatId: input.chatId,
31
- recordedAt: input.occurredAt,
32
- ...input.project !== void 0 && { project: input.project },
33
- ...input.cwd !== void 0 && { cwd: input.cwd }
34
- });
35
- return { turnId: yield* (yield* DataStore).writeTurn({
36
- sessionId: session.id,
37
- type: parse.payload.type,
38
- payload: input.payloadJson,
39
- occurredAt: input.occurredAt
40
- }) };
41
- });
42
-
43
- //#endregion
44
- export { parseAndValidateTurnPayload, recordTurnEffect };
@@ -1,50 +0,0 @@
1
- import { DataStore } from "@vitest-agent/sdk";
2
- import { Effect } from "effect";
3
- import { SqlClient } from "effect/unstable/sql/SqlClient";
4
-
5
- //#region src/lib/record-workspace-changes.ts
6
- /**
7
- * `ProjectRunSummary` doesn't expose `lastRunId`, so we query it directly
8
- * via SqlClient. This is the run id of the most-recent test run for a
9
- * given project (or any project, when `project` is unspecified). Used
10
- * only as a best-effort association target for `run_changed_files`.
11
- */
12
- const findLatestRunId = (project) => Effect.gen(function* () {
13
- const sql = yield* SqlClient;
14
- const rows = project !== void 0 ? yield* sql`
15
- SELECT id FROM test_runs
16
- WHERE project = ${project}
17
- ORDER BY timestamp DESC LIMIT 1
18
- ` : yield* sql`
19
- SELECT id FROM test_runs
20
- ORDER BY timestamp DESC LIMIT 1
21
- `;
22
- return rows.length === 0 ? null : rows[0].id;
23
- }).pipe(Effect.orElseSucceed(() => null));
24
- const recordRunWorkspaceChangesEffect = (input) => Effect.gen(function* () {
25
- const store = yield* DataStore;
26
- yield* store.writeCommit({
27
- sha: input.sha,
28
- ...input.parentSha !== void 0 && { parentSha: input.parentSha },
29
- ...input.message !== void 0 && { message: input.message },
30
- ...input.author !== void 0 && { author: input.author },
31
- ...input.committedAt !== void 0 && { committedAt: input.committedAt },
32
- ...input.branch !== void 0 && { branch: input.branch }
33
- });
34
- const runId = yield* findLatestRunId(input.project);
35
- if (runId !== null && input.files.length > 0) yield* store.writeRunChangedFiles({
36
- runId,
37
- files: input.files.map((f) => ({
38
- filePath: f.filePath,
39
- changeKind: f.changeKind,
40
- commitSha: input.sha
41
- }))
42
- });
43
- return {
44
- sha: input.sha,
45
- fileRowsWritten: runId !== null ? input.files.length : 0
46
- };
47
- });
48
-
49
- //#endregion
50
- export { recordRunWorkspaceChangesEffect };
@@ -1,71 +0,0 @@
1
- import { DataReader, DataStore } from "@vitest-agent/sdk";
2
- import { Effect, Option } from "effect";
3
- import { readFileSync } from "node:fs";
4
-
5
- //#region src/lib/resolve-session-for-recording.ts
6
- /**
7
- * Shared session-resolution helper for hook-driven recording paths.
8
- *
9
- * Background: Claude Code can rotate the chat id (host `session_id`)
10
- * mid-window — after a continuation, compaction, or `/mcp` reconnect —
11
- * without the `SessionStart` hook firing for the new id. PostToolUse
12
- * hooks then deliver tool calls under a `chat_id` that has no row in
13
- * `sessions`, and the strict `getSessionByChatId` lookup fails with
14
- * "Unknown chat_id". A second flavour of the same bug: the
15
- * SubagentStart hook mints a synthetic per-dispatch key
16
- * (`<parentChatId>-subagent-<ts>-<pid>`) for the subagent row, but
17
- * subsequent PostToolUse hooks under that subagent receive the bare
18
- * parent chat id. Exact-match lookup of the bare id misses the
19
- * synthetic suffix.
20
- *
21
- * This resolver applies a three-step fallback that recovers either
22
- * case without crashing the recording path:
23
- *
24
- * 1. Exact match by `chat_id`.
25
- * 2. Prefix match `<chatId>-subagent-` to recover the synthetic
26
- * subagent row (most recent first).
27
- * 3. Idempotent bootstrap of a `main` session row keyed on the
28
- * input `chat_id`, so future calls in this window resolve via
29
- * step 1.
30
- *
31
- * Step 3 means recording paths can no longer fail with "Unknown
32
- * chat_id". Bootstrapped rows carry `agent_kind = "main"` and
33
- * `triage_was_non_empty = false` because the SessionStart triage
34
- * pipeline never ran for them; downstream metrics consumers should
35
- * treat triage-empty bootstrapped rows the same as any other
36
- * SessionStart-less row.
37
- *
38
- * @packageDocumentation
39
- */
40
- const resolveSessionForRecording = (input) => Effect.gen(function* () {
41
- const reader = yield* DataReader;
42
- const store = yield* DataStore;
43
- const exact = yield* reader.getSessionByChatId(input.chatId);
44
- if (Option.isSome(exact)) return exact.value;
45
- const synthetic = yield* reader.findSessionsByChatPrefix(`${input.chatId}-subagent-`);
46
- if (synthetic.length > 0) return synthetic[0];
47
- const cwd = input.cwd ?? process.cwd();
48
- const project = input.project ?? readPackageName(cwd) ?? "unknown";
49
- yield* store.upsertSession({
50
- chatId: input.chatId,
51
- project,
52
- cwd,
53
- agentKind: "main",
54
- triageWasNonEmpty: false,
55
- startedAt: input.recordedAt
56
- });
57
- const created = yield* reader.getSessionByChatId(input.chatId);
58
- return Option.getOrThrowWith(created, () => /* @__PURE__ */ new Error(`upsertSession succeeded but row not found for ${input.chatId}`));
59
- });
60
- const readPackageName = (cwd) => {
61
- try {
62
- const raw = readFileSync(`${cwd}/package.json`, "utf8");
63
- const parsed = JSON.parse(raw);
64
- return typeof parsed.name === "string" ? parsed.name : void 0;
65
- } catch {
66
- return;
67
- }
68
- };
69
-
70
- //#endregion
71
- export { resolveSessionForRecording };