@vitest-agent/engine 0.3.2 → 0.4.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/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Context, Effect, FileSystem, Layer, LogLevel, Option, Path, Schema } fr
2
2
  import { ConfigFileShape } from "@effected/config-file";
3
3
  import { Agent, AgentNotFoundError, AgentReport, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, CitedArtifact, CoverageBaselines, CoverageReport, DataStoreError, DetailLevel, DiscoveryError, Environment, Executor, FileCoverageReport, FormatterContext, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, OutputFormat, Phase, ProjectIdentityNotResolvableError, RegistrationConflictError, RenderedOutput, ResolvedThresholds, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
4
4
  import { SqlClient } from "effect/sql/SqlClient";
5
+ import { RuntimeEnv } from "@effected/env";
5
6
  import { WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspaceRootNotFoundError } from "@effected/workspaces";
6
7
  import { AppDirs, AppDirsError, XdgEnvError } from "@effected/xdg";
7
8
  import { ChildProcessSpawner } from "effect/process";
@@ -1206,14 +1207,24 @@ export declare class EnvironmentDetector extends EnvironmentDetector_base {}
1206
1207
  * Pure classification behind {@link EnvironmentDetectorLive}: an agent shell
1207
1208
  * wins, then GitHub Actions, then generic CI, else a terminal.
1208
1209
  *
1209
- * @param env - the environment map to consult
1210
- * @param agentShell - whether the process runs under an AI agent (`std-env`'s `isAgent`)
1210
+ * @remarks
1211
+ * The agent/CI/human split is `@effected/env`'s `Audience.detect`, so the
1212
+ * engine and every `@effected/cli` front end agree on who is running; this
1213
+ * only refines CI into GitHub Actions versus any other CI.
1214
+ *
1215
+ * @param runtime - the `@effected/env` runtime snapshot to classify
1211
1216
  * @public
1212
1217
  */
1213
- export declare const classifyEnvironment: (env: Record<string, string | undefined>, agentShell: boolean) => Environment;
1218
+ export declare const classifyEnvironment: (runtime: RuntimeEnv) => Environment;
1214
1219
  /**
1215
- * Live environment detector. CI detection reads `GITHUB_ACTIONS` / `CI`
1216
- * from the injected `env` map; agent detection comes from `std-env`.
1220
+ * Live environment detector over `@effected/env`'s `CurrentRuntimeEnv`, read
1221
+ * from the injected `env` map only: agent, GitHub Actions and generic CI
1222
+ * detection all come from `env`, never from the ambient process.
1223
+ *
1224
+ * @remarks
1225
+ * The snapshot is taken once, when the layer is built, through
1226
+ * `CurrentRuntimeEnv.layerFrom(env)`: a fresh layer per call and per use, so two
1227
+ * detectors built over different maps in one layer graph each see their own.
1217
1228
  *
1218
1229
  * @param env - the environment map to consult (the front end passes `process.env`)
1219
1230
  * @public
@@ -1313,15 +1324,35 @@ export declare const HistoryTrackerTest: {
1313
1324
  //#endregion
1314
1325
  //#region src/layers/LoggerLive.d.ts
1315
1326
  /**
1316
- * Create a structured JSON (NDJSON) logger layer for stderr.
1327
+ * Create a structured JSON (NDJSON) logger layer for stderr, over
1328
+ * `@effected/cli`'s `CliLog` in its diagnostics-only mode
1329
+ * (`format: "json"`, `plainLogger: false`): the already-resolved level is
1330
+ * passed as `level`, and no plain `CliLogger` is installed beside the sink, so
1331
+ * each record prints once and nothing reads the audience or the terminal.
1317
1332
  *
1318
1333
  * When level is undefined or `"None"`, installs an empty logger set (silent).
1319
- * When logFile is set, composes the stderr logger with a file logger.
1320
- * Uses `Logger.formatJson` / `Logger.formatStructured` for machine-readable
1321
- * output.
1334
+ * When logFile is set, `CliLog`'s asynchronous file sink also appends each
1335
+ * NDJSON line (the same shape as the stderr line) to that file; the first
1336
+ * write error prints one stderr line and disables the file sink.
1337
+ *
1338
+ * When `env` is given, it is built into `CurrentRuntimeEnv` for the layer's
1339
+ * own build context, which `CliLog` captures: under GitHub Actions every
1340
+ * stderr record is neutralized (no workflow command at line start, `##[`
1341
+ * escaped), including records logged from a fiber that carries no
1342
+ * `CurrentRuntimeEnv` of its own. Without `env`, records are sanitised but
1343
+ * not neutralized unless the logging fiber provides one.
1344
+ *
1345
+ * @remarks
1346
+ * The file sink needs `FileSystem` and `Path`. `CliLog` leaves them in `R`
1347
+ * (for `file: undefined` too), but they are provided here (Node) so the
1348
+ * public signature stays `Layer.Layer<never>` for every caller.
1349
+ *
1350
+ * @param level - the diagnostics level; see {@link resolveLogLevel}
1351
+ * @param logFile - a file to append NDJSON lines to; see {@link resolveLogFile}
1352
+ * @param env - the environment map to detect the runtime from (the front end passes `process.env`)
1322
1353
  * @public
1323
1354
  */
1324
- export declare const LoggerLive: (level?: LogLevel.LogLevel, logFile?: string) => Layer.Layer<never>;
1355
+ export declare const LoggerLive: (level?: LogLevel.LogLevel, logFile?: string, env?: Readonly<Record<string, string | undefined>>) => Layer.Layer<never>;
1325
1356
  /**
1326
1357
  * Resolve log level from option or environment variable.
1327
1358
  * Priority: explicit option \> `VITEST_REPORTER_LOG_LEVEL` in `env` \> undefined.
@@ -1835,6 +1866,15 @@ export interface PlatformOptions {
1835
1866
  readonly logLevel?: LogLevel.LogLevel | undefined;
1836
1867
  /** Optional path for structured NDJSON log output. */
1837
1868
  readonly logFile?: string | undefined;
1869
+ /**
1870
+ * Whether `PlatformLive` installs its own logger set (`LoggerLive`, from
1871
+ * `logLevel` / `logFile`). Defaults to `true`. Pass `false` when the caller
1872
+ * already owns the logger set (for example `@effected/cli`'s `CliLog`
1873
+ * installed by `CliRuntime.main`'s `env.log`): `LoggerLive` replaces the
1874
+ * installed loggers, so leaving it in would silence the caller's set.
1875
+ * `logLevel` and `logFile` are ignored when this is `false`.
1876
+ */
1877
+ readonly logger?: boolean | undefined;
1838
1878
  }
1839
1879
  /**
1840
1880
  * The services {@link PlatformLive} provides.
@@ -2583,9 +2623,15 @@ export declare function assembleHistoryRecord(rows: HistoryRow[]): AssembledHist
2583
2623
  * most once per dbPath in the current process; concurrent calls share the
2584
2624
  * same in-flight promise. Subsequent calls (after the first resolves) are
2585
2625
  * no-ops.
2626
+ *
2627
+ * @param dbPath - the SQLite file to migrate
2628
+ * @param logLevel - the diagnostics level for the migrator's records; see `LoggerLive`
2629
+ * @param logFile - a file to append NDJSON diagnostics to; see `LoggerLive`
2630
+ * @param env - the environment map `LoggerLive` detects the runtime from, so
2631
+ * records are neutralized under GitHub Actions (the front end passes `process.env`)
2586
2632
  * @public
2587
2633
  */
2588
- export declare function ensureMigrated(dbPath: string, logLevel?: LogLevel.LogLevel, logFile?: string): Promise<void>;
2634
+ export declare function ensureMigrated(dbPath: string, logLevel?: LogLevel.LogLevel, logFile?: string, env?: Readonly<Record<string, string | undefined>>): Promise<void>;
2589
2635
  /**
2590
2636
  * Reset the migration cache. Test-only.
2591
2637
  *
@@ -1,36 +1,56 @@
1
1
  import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
2
- import { Effect, Layer } from "effect";
3
- import { agent, isAgent } from "std-env";
2
+ import { Effect, Layer, Option } from "effect";
3
+ import { Audience, CurrentRuntimeEnv } from "@effected/env";
4
4
 
5
5
  //#region src/layers/EnvironmentDetectorLive.ts
6
- const isGitHub = (env) => env.GITHUB_ACTIONS === "true" || env.GITHUB_ACTIONS === "1";
7
- const isCI = (env) => isGitHub(env) || env.CI === "true";
6
+ /** The engine environment for each CI `@effected/env` names; exhaustive over `CiName`. */
7
+ const CI_ENVIRONMENT = {
8
+ "github-actions": "ci-github",
9
+ generic: "ci-generic"
10
+ };
8
11
  /**
9
12
  * Pure classification behind {@link EnvironmentDetectorLive}: an agent shell
10
13
  * wins, then GitHub Actions, then generic CI, else a terminal.
11
14
  *
12
- * @param env - the environment map to consult
13
- * @param agentShell - whether the process runs under an AI agent (`std-env`'s `isAgent`)
15
+ * @remarks
16
+ * The agent/CI/human split is `@effected/env`'s `Audience.detect`, so the
17
+ * engine and every `@effected/cli` front end agree on who is running; this
18
+ * only refines CI into GitHub Actions versus any other CI.
19
+ *
20
+ * @param runtime - the `@effected/env` runtime snapshot to classify
14
21
  * @public
15
22
  */
16
- const classifyEnvironment = (env, agentShell) => {
17
- if (agentShell) return "agent-shell";
18
- if (isGitHub(env)) return "ci-github";
19
- if (isCI(env)) return "ci-generic";
20
- return "terminal";
23
+ const classifyEnvironment = (runtime) => {
24
+ switch (Audience.detect(runtime)) {
25
+ case "agent": return "agent-shell";
26
+ case "ci": return Option.match(runtime.ci, {
27
+ onNone: () => "ci-generic",
28
+ onSome: (ci) => CI_ENVIRONMENT[ci]
29
+ });
30
+ case "human": return "terminal";
31
+ }
21
32
  };
22
33
  /**
23
- * Live environment detector. CI detection reads `GITHUB_ACTIONS` / `CI`
24
- * from the injected `env` map; agent detection comes from `std-env`.
34
+ * Live environment detector over `@effected/env`'s `CurrentRuntimeEnv`, read
35
+ * from the injected `env` map only: agent, GitHub Actions and generic CI
36
+ * detection all come from `env`, never from the ambient process.
37
+ *
38
+ * @remarks
39
+ * The snapshot is taken once, when the layer is built, through
40
+ * `CurrentRuntimeEnv.layerFrom(env)`: a fresh layer per call and per use, so two
41
+ * detectors built over different maps in one layer graph each see their own.
25
42
  *
26
43
  * @param env - the environment map to consult (the front end passes `process.env`)
27
44
  * @public
28
45
  */
29
- const EnvironmentDetectorLive = (env) => Layer.succeed(EnvironmentDetector, {
30
- detect: () => Effect.sync(() => classifyEnvironment(env, isAgent)),
31
- isAgent: Effect.sync(() => isAgent),
32
- agentName: Effect.sync(() => agent ?? void 0)
33
- });
46
+ const EnvironmentDetectorLive = (env) => Layer.effect(EnvironmentDetector, Effect.map(CurrentRuntimeEnv, (runtime) => {
47
+ const environment = classifyEnvironment(runtime);
48
+ return {
49
+ detect: () => Effect.succeed(environment),
50
+ isAgent: Effect.succeed(Option.isSome(runtime.agent)),
51
+ agentName: Effect.succeed(Option.getOrUndefined(runtime.agent))
52
+ };
53
+ })).pipe(Layer.provide(CurrentRuntimeEnv.layerFrom(env)));
34
54
 
35
55
  //#endregion
36
56
  export { EnvironmentDetectorLive, classifyEnvironment };
@@ -2,8 +2,20 @@ import { ExecutorResolver } from "../services/ExecutorResolver.js";
2
2
  import { Effect, Layer } from "effect";
3
3
 
4
4
  //#region src/layers/ExecutorResolverLive.ts
5
+ /**
6
+ * The `@effected/env` audience each environment implies. An `Executor` is the
7
+ * same three-way split as an `AudienceKind` (`human` / `agent` / `ci`); typing
8
+ * the table as `AudienceKind` and returning it as `Executor` keeps the two
9
+ * unions from drifting apart at compile time.
10
+ */
11
+ const AUDIENCE_OF = {
12
+ "agent-shell": "agent",
13
+ terminal: "human",
14
+ "ci-github": "ci",
15
+ "ci-generic": "ci"
16
+ };
5
17
  /** @public */
6
- const ExecutorResolverLive = Layer.succeed(ExecutorResolver, { resolve: (env) => Effect.succeed(env === "agent-shell" ? "agent" : env === "terminal" ? "human" : "ci") });
18
+ const ExecutorResolverLive = Layer.succeed(ExecutorResolver, { resolve: (env) => Effect.succeed(AUDIENCE_OF[env]) });
7
19
 
8
20
  //#endregion
9
21
  export { ExecutorResolverLive };
@@ -1,33 +1,48 @@
1
- import { Layer, LogLevel, Logger, References } from "effect";
2
- import { appendFileSync } from "node:fs";
1
+ import { Layer, LogLevel, Logger } from "effect";
2
+ import { CurrentRuntimeEnv } from "@effected/env";
3
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
4
+ import * as NodePath from "@effect/platform-node/NodePath";
5
+ import { CliLog } from "@effected/cli";
3
6
 
4
7
  //#region src/layers/LoggerLive.ts
5
8
  /**
6
- * Create a structured JSON (NDJSON) logger layer for stderr.
9
+ * Create a structured JSON (NDJSON) logger layer for stderr, over
10
+ * `@effected/cli`'s `CliLog` in its diagnostics-only mode
11
+ * (`format: "json"`, `plainLogger: false`): the already-resolved level is
12
+ * passed as `level`, and no plain `CliLogger` is installed beside the sink, so
13
+ * each record prints once and nothing reads the audience or the terminal.
7
14
  *
8
15
  * When level is undefined or `"None"`, installs an empty logger set (silent).
9
- * When logFile is set, composes the stderr logger with a file logger.
10
- * Uses `Logger.formatJson` / `Logger.formatStructured` for machine-readable
11
- * output.
16
+ * When logFile is set, `CliLog`'s asynchronous file sink also appends each
17
+ * NDJSON line (the same shape as the stderr line) to that file; the first
18
+ * write error prints one stderr line and disables the file sink.
19
+ *
20
+ * When `env` is given, it is built into `CurrentRuntimeEnv` for the layer's
21
+ * own build context, which `CliLog` captures: under GitHub Actions every
22
+ * stderr record is neutralized (no workflow command at line start, `##[`
23
+ * escaped), including records logged from a fiber that carries no
24
+ * `CurrentRuntimeEnv` of its own. Without `env`, records are sanitised but
25
+ * not neutralized unless the logging fiber provides one.
26
+ *
27
+ * @remarks
28
+ * The file sink needs `FileSystem` and `Path`. `CliLog` leaves them in `R`
29
+ * (for `file: undefined` too), but they are provided here (Node) so the
30
+ * public signature stays `Layer.Layer<never>` for every caller.
31
+ *
32
+ * @param level - the diagnostics level; see {@link resolveLogLevel}
33
+ * @param logFile - a file to append NDJSON lines to; see {@link resolveLogFile}
34
+ * @param env - the environment map to detect the runtime from (the front end passes `process.env`)
12
35
  * @public
13
36
  */
14
- const LoggerLive = (level, logFile) => {
37
+ const LoggerLive = (level, logFile, env) => {
15
38
  if (!level || level === "None") return Logger.layer([]);
16
- const stderrLogger = Logger.formatJson.pipe(Logger.withConsoleError);
17
- const fileLogger = logFile ? Logger.formatStructured.pipe(Logger.map((entry) => {
18
- const line = JSON.stringify({
19
- timestamp: entry.timestamp,
20
- level: entry.level,
21
- message: entry.message,
22
- ...entry.annotations
23
- });
24
- try {
25
- appendFileSync(logFile, `${line}\n`);
26
- } catch {}
27
- return line;
28
- })) : void 0;
29
- const loggers = fileLogger ? [stderrLogger, fileLogger] : [stderrLogger];
30
- return Layer.merge(Logger.layer(loggers), Layer.succeed(References.MinimumLogLevel, level));
39
+ const sink = CliLog.layer({
40
+ format: "json",
41
+ plainLogger: false,
42
+ level,
43
+ file: logFile === void 0 ? void 0 : { path: logFile }
44
+ }).pipe(Layer.provide(Layer.merge(NodeFileSystem.layer, NodePath.layer)));
45
+ return env === void 0 ? sink : sink.pipe(Layer.provide(CurrentRuntimeEnv.layerFrom(env)));
31
46
  };
32
47
  const LEVEL_ALIASES = {
33
48
  warn: "Warn",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/engine",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "Shared services, layers, SQLite data layer, migrations and platform resolution for the vitest-agent CLI and MCP server.",
6
6
  "keywords": [
@@ -40,20 +40,21 @@
40
40
  "./package.json": "./package.json"
41
41
  },
42
42
  "dependencies": {
43
- "@effect/platform-node": "4.0.0-rc.118",
44
- "@effect/sql-sqlite-node": "4.0.0-rc.118",
45
- "@effected/config-file": "^0.13.1",
46
- "@effected/engine": "^0.2.0",
47
- "@effected/glob": "^0.9.0",
48
- "@effected/jsonc": "^0.14.0",
49
- "@effected/toml": "^0.10.0",
50
- "@effected/walker": "^0.14.0",
51
- "@effected/workspaces": "^0.30.3",
52
- "@effected/xdg": "^0.8.2",
53
- "@effected/yaml": "^0.18.0",
54
- "@vitest-agent/sdk": "5.1.3",
55
- "effect": "4.0.0-rc.118",
56
- "std-env": "^4.2.0"
43
+ "@effect/platform-node": "^4.0.0",
44
+ "@effect/sql-sqlite-node": "^4.0.0",
45
+ "@effected/cli": "^0.11.0",
46
+ "@effected/config-file": "^0.14.0",
47
+ "@effected/engine": "^0.3.0",
48
+ "@effected/env": "^0.1.0",
49
+ "@effected/glob": "^0.10.0",
50
+ "@effected/jsonc": "^0.15.0",
51
+ "@effected/toml": "^0.11.0",
52
+ "@effected/walker": "^0.15.0",
53
+ "@effected/workspaces": "^0.31.0",
54
+ "@effected/xdg": "^0.9.0",
55
+ "@effected/yaml": "^0.19.0",
56
+ "@vitest-agent/sdk": "5.2.0",
57
+ "effect": "^4.0.0"
57
58
  },
58
59
  "engines": {
59
60
  "node": ">=24.11.0"
package/platform.js CHANGED
@@ -51,7 +51,8 @@ const makeSqliteStack = (filename, migrations = PROJECT_MIGRATIONS) => {
51
51
  */
52
52
  const PlatformLive = (options) => {
53
53
  const { SqliteLayer, MigratorLayer } = makeSqliteStack(options.dbPath);
54
- return Layer.mergeAll(ProjectDiscoveryLive, HistoryTrackerLive, OutputPipelineLive(options.env)).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(DataStoreLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(NodePlatformLayer), Layer.provideMerge(LoggerLive(options.logLevel, options.logFile)));
54
+ const platform = Layer.mergeAll(ProjectDiscoveryLive, HistoryTrackerLive, OutputPipelineLive(options.env)).pipe(Layer.provideMerge(DataReaderLive), Layer.provideMerge(DataStoreLive), Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(NodePlatformLayer));
55
+ return options.logger === false ? platform : platform.pipe(Layer.provideMerge(LoggerLive(options.logLevel, options.logFile, options.env)));
55
56
  };
56
57
 
57
58
  //#endregion
@@ -1,5 +1,5 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
1
  import { join, resolve } from "node:path";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
3
 
4
4
  //#region src/programs/session-env.ts
5
5
  const EXPORT_LINE = /^export ([A-Z_][A-Z0-9_]*)=(.*)$/;
@@ -19,16 +19,22 @@ const getCache = () => {
19
19
  * most once per dbPath in the current process; concurrent calls share the
20
20
  * same in-flight promise. Subsequent calls (after the first resolves) are
21
21
  * no-ops.
22
+ *
23
+ * @param dbPath - the SQLite file to migrate
24
+ * @param logLevel - the diagnostics level for the migrator's records; see `LoggerLive`
25
+ * @param logFile - a file to append NDJSON diagnostics to; see `LoggerLive`
26
+ * @param env - the environment map `LoggerLive` detects the runtime from, so
27
+ * records are neutralized under GitHub Actions (the front end passes `process.env`)
22
28
  * @public
23
29
  */
24
- function ensureMigrated(dbPath, logLevel, logFile) {
30
+ function ensureMigrated(dbPath, logLevel, logFile, env) {
25
31
  const cache = getCache();
26
32
  const cached = cache.get(dbPath);
27
33
  if (cached) return cached;
28
34
  const { SqliteLayer, MigratorLayer } = makeSqliteStack(dbPath);
29
35
  const program = Effect.gen(function* () {
30
36
  yield* SqlClient;
31
- }).pipe(Effect.provide(MigratorLayer), Effect.provide(Layer.merge(SqliteLayer, NodePlatformLayer)), Effect.provide(LoggerLive(logLevel, logFile)));
37
+ }).pipe(Effect.provide(MigratorLayer), Effect.provide(Layer.merge(SqliteLayer, NodePlatformLayer)), Effect.provide(LoggerLive(logLevel, logFile, env)));
32
38
  const promise = Effect.runPromise(program);
33
39
  cache.set(dbPath, promise);
34
40
  promise.catch(() => {});
@@ -1,7 +1,7 @@
1
1
  import { gitUrlToProjectKey, normalizeWorkspaceKey } from "@vitest-agent/sdk";
2
2
  import { Effect, FileSystem } from "effect";
3
- import { existsSync, readFileSync } from "node:fs";
4
3
  import { dirname, join } from "node:path";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
 
6
6
  //#region src/utils/resolve-project-key-from-cwd.ts
7
7
  const PACKAGE_JSON = "package.json";
package/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  /** Build-time injected package version (see architecture.md D36). @public */
3
- const CURRENT_ENGINE_VERSION = "0.3.2";
3
+ const CURRENT_ENGINE_VERSION = "0.4.0";
4
4
 
5
5
  //#endregion
6
6
  export { CURRENT_ENGINE_VERSION };