@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/bin/vitest-agent.js +4 -24
- package/commands/agent.js +31 -39
- package/commands/db.js +7 -7
- package/commands/doctor.js +2 -2
- package/commands/record.js +36 -40
- package/commands/triage.js +4 -4
- package/commands/wrapup.js +6 -6
- package/index.d.ts +3 -158
- package/index.js +2 -16
- package/main.d.ts +22 -0
- package/main.js +58 -0
- package/package.json +13 -7
- package/version.js +13 -0
- package/layers/CliLive.js +0 -29
- package/layers/SidecarLive.js +0 -33
- package/lib/internal-end-agent.js +0 -27
- package/lib/internal-register-agent.js +0 -81
- package/lib/record-session.js +0 -33
- package/lib/record-tdd-artifact.js +0 -104
- package/lib/record-turn.js +0 -44
- package/lib/record-workspace-changes.js +0 -50
- package/lib/resolve-session-for-recording.js +0 -71
- package/lib/sidecar-paths.js +0 -101
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": "
|
|
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.
|
|
42
|
-
"@effect/sql-sqlite-node": "4.0.0-rc.
|
|
43
|
-
"@effected/workspaces": "^0.
|
|
44
|
-
"@vitest-agent/
|
|
45
|
-
"@vitest-agent/
|
|
46
|
-
"
|
|
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 };
|
package/layers/SidecarLive.js
DELETED
|
@@ -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 };
|
package/lib/record-session.js
DELETED
|
@@ -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 };
|
package/lib/record-turn.js
DELETED
|
@@ -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 };
|