@vitest-agent/engine 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
- import { AppDirs, AppDirs as AppDirs$1, AppDirsError, AppDirsError as AppDirsError$1, XdgEnvError } from "@effected/xdg";
1
+ import { AppDirs, AppDirs as AppDirs$1, AppDirsError, AppDirsError as AppDirsError$1, AppDirsSegmentError, XdgEnvError } from "@effected/xdg";
2
2
  import { Context, Effect, FileSystem, Layer, LogLevel, Option, Path, Schema } from "effect";
3
3
  import { ConfigFileShape } from "@effected/config-file";
4
- 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, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, Phase, ProjectIdentityError, ProjectIdentityNotResolvableError, RegistrationConflictError, ResolvedThresholds, RunRetentionPolicy, StrayOutput, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
4
+ 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, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, Phase, ProjectIdentityError, ProjectIdentityNotResolvableError, ProjectIdentitySourceError, RegistrationConflictError, ResolvedThresholds, RunRetentionPolicy, StrayOutput, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
5
5
  import { SqlClient } from "effect/sql/SqlClient";
6
6
  import { CiName, RuntimeEnv } from "@effected/env";
7
7
  import { WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspaceRootNotFoundError } from "@effected/workspaces";
8
8
  import { ChildProcessSpawner } from "effect/process";
9
9
  import * as NodeServices from "@effect/platform-node/NodeServices";
10
10
  import { SqliteClient } from "@effect/sql-sqlite-node/SqliteClient";
11
- import { StoreError, StoreMigrationError } from "@effected/store";
11
+ import { StoreError, StoreMigrationError, StoreShape } from "@effected/store";
12
12
  import { SqlError } from "effect/sql/SqlError";
13
13
  import { PlatformError } from "effect/PlatformError";
14
14
  /**
@@ -35,12 +35,19 @@ export declare class VitestAgentConfigFile extends VitestAgentConfigFile_base {}
35
35
  *
36
36
  * 1. The workspace root (when `projectDir` is inside a pnpm/npm/yarn workspace).
37
37
  * 2. The git repository root (when `projectDir` is inside a git repo).
38
- * 3. Walking upward from `projectDir`.
38
+ * 3. Walking upward from `projectDir`, stopping at the git repository root
39
+ * (inclusive, compared physically) so a config file above the repository
40
+ * is never picked up. Outside a repository the walk runs to the
41
+ * filesystem root.
39
42
  *
40
43
  * The first found file wins (`MergeStrategy.firstMatch()`). If no file is
41
44
  * present, downstream callers use `loadOrDefault(new VitestAgentConfig({}))`
42
45
  * to get an empty config.
43
46
  *
47
+ * The repository root comes from `git rev-parse --show-toplevel` through
48
+ * `@effected/git`, using a `Git` or `ChildProcessSpawner` service if the
49
+ * context has one; neither is required (see `repoRootCeiling`).
50
+ *
44
51
  * @param projectDir - Absolute path inside the user's workspace. Resolvers
45
52
  * anchor here rather than `process.cwd()` so the plugin-spawned MCP server
46
53
  * sees the right config even when invoked from elsewhere.
@@ -1386,6 +1393,10 @@ export declare const LoggerLive: (level?: LogLevel.LogLevel, logFile?: string, e
1386
1393
  * Resolve log level from option or environment variable.
1387
1394
  * Priority: explicit option \> `VITEST_REPORTER_LOG_LEVEL` in `env` \> undefined.
1388
1395
  *
1396
+ * Case-insensitive; `warning` is accepted for `Warn`. An unset or empty value,
1397
+ * or one that names no level, resolves to `undefined` (no diagnostics) without
1398
+ * a warning.
1399
+ *
1389
1400
  * @param env - the environment map to consult (the front end passes `process.env`)
1390
1401
  * @param option - explicit override
1391
1402
  * @public
@@ -1632,12 +1643,13 @@ declare const ProjectIdentity_base: Context.ServiceClass<ProjectIdentity, "vites
1632
1643
  */
1633
1644
  export declare class ProjectIdentity extends ProjectIdentity_base {}
1634
1645
  /**
1635
- * Effect environment type required by `collectCandidates` and the
1636
- * `ProjectIdentityLive` layer. Exported only so the layer's
1646
+ * Effect environment type required by the `ProjectIdentityLive` layer.
1647
+ * Exported only so the layer's
1637
1648
  * `Layer.Layer<ProjectIdentity, never, CandidateContext>` signature
1638
1649
  * has a named referent — consumers should provide the four named
1639
1650
  * services (FileSystem, WorkspaceDiscovery, VitestAgentConfigFile,
1640
- * CommandExecutor) rather than reference this type directly.
1651
+ * ChildProcessSpawner, which feeds the layer's own `@effected/git` `Git`)
1652
+ * rather than reference this type directly.
1641
1653
  *
1642
1654
  * @public
1643
1655
  */
@@ -1719,9 +1731,10 @@ export declare const RunContextService: typeof RunContext$;
1719
1731
  export type RunContextService = RunContext$;
1720
1732
  /**
1721
1733
  * Live layer. Requires `ChildProcessSpawner` (provided by
1722
- * `NodeServices.layer` at the entry point). Host-metadata probes read
1723
- * the injected `env` map — the env walk is pure and the map is captured
1724
- * once when the layer is built.
1734
+ * `NodeServices.layer` at the entry point), which feeds `@effected/git`'s
1735
+ * `Git` for branch / sha / dirty / upstream / toplevel. Every git failure is
1736
+ * recorded as `null`. Host metadata comes from `probeHostMetadataFromEnv`
1737
+ * over the injected `env` map, captured once when the layer is built.
1725
1738
  *
1726
1739
  * @param env - the environment map to probe (the front end passes `process.env`)
1727
1740
  * @public
@@ -1820,6 +1833,18 @@ declare const migration$3: Effect.Effect<void, import("effect/sql/SqlError").Sql
1820
1833
  * @public
1821
1834
  */
1822
1835
  type MigrationRecord = Record<string, Effect.Effect<void, SqlError, SqlClient>>;
1836
+ declare const RegistryStore_base: Context.ServiceClass<RegistryStore, "vitest-agent/RegistryStore", StoreShape>;
1837
+ /**
1838
+ * The global discovery registry (`<XDG data>/vitest-agent/registry.db`).
1839
+ * @internal
1840
+ */
1841
+ export declare class RegistryStore extends RegistryStore_base {}
1842
+ declare const SessionMapStore_base: Context.ServiceClass<SessionMapStore, "vitest-agent/SessionMapStore", StoreShape>;
1843
+ /**
1844
+ * The per-client session map (`sessions.db`, under the host's plugin data dir).
1845
+ * @internal
1846
+ */
1847
+ export declare class SessionMapStore extends SessionMapStore_base {}
1823
1848
  /**
1824
1849
  * A SQLite connection plus the migrator that brings it to the head of a
1825
1850
  * migration set. Returned by {@link makeSqliteStack}.
@@ -2001,7 +2026,9 @@ export type HookPathsError = XdgEnvError | AppDirsError$1 | PlatformError | Proj
2001
2026
  *
2002
2027
  * Precedence: the `CLAUDE_PLUGIN_DATA` env var, then
2003
2028
  * `VITEST_AGENT_SESSION_MAP_DIR`, then `~/.vitest-agent/` (`HOME`, then
2004
- * `USERPROFILE`). The chosen directory is created. Fails with
2029
+ * `USERPROFILE`). The two variables are read through `@effected/engine`'s
2030
+ * `LaunchContext.resolve`: a value is trimmed, and a blank or unsubstituted
2031
+ * `${...}` placeholder value counts as unset. The chosen directory is created. Fails with
2005
2032
  * `ProjectIdentityNotResolvableError` when no home directory is resolvable.
2006
2033
  *
2007
2034
  * @param env - the environment map to consult (the front end passes `process.env`)
@@ -2038,6 +2065,20 @@ export interface SidecarPaths {
2038
2065
  /** Absolute path to the global `registry.db`. */
2039
2066
  readonly registryDbPath: string;
2040
2067
  }
2068
+ /**
2069
+ * The per-client session map store (`sessions.db`) at `filename`, migrated,
2070
+ * with the shared per-connection settings applied on open.
2071
+ *
2072
+ * @internal
2073
+ */
2074
+ export declare const SessionMapStoreLive: (filename: string) => Layer.Layer<SessionMapStore, import("@effected/store").StoreError | import("@effected/store").StoreMigrationError, never>;
2075
+ /**
2076
+ * The global discovery registry store (`registry.db`) at `filename`, migrated,
2077
+ * with the shared per-connection settings applied on open.
2078
+ *
2079
+ * @internal
2080
+ */
2081
+ export declare const RegistryStoreLive: (filename: string) => Layer.Layer<RegistryStore, import("@effected/store").StoreError | import("@effected/store").StoreMigrationError, never>;
2041
2082
  /**
2042
2083
  * Build the sidecar Live layer for the supplied SQLite paths.
2043
2084
  *
@@ -2463,15 +2504,6 @@ export interface SessionContext {
2463
2504
  readonly conversationId: string;
2464
2505
  readonly mainAgentId: string;
2465
2506
  }
2466
- /**
2467
- * Parse `export KEY=value` lines from a session-env hook file into a
2468
- * plain record. Non-export lines are ignored.
2469
- *
2470
- * @param content - the raw text of a session-env hook file
2471
- * @returns a record of export names to unquoted values
2472
- * @public
2473
- */
2474
- export declare const parseSessionEnvExports: (content: string) => Record<string, string>;
2475
2507
  /**
2476
2508
  * The synchronous filesystem reads {@link recoverSessionContextFromSessionEnv}
2477
2509
  * performs. Each member throws on a missing or unreadable path, exactly as
@@ -2482,8 +2514,12 @@ export declare const parseSessionEnvExports: (content: string) => Record<string,
2482
2514
  export interface SessionEnvFileSystem {
2483
2515
  /** Entry names directly under `path` (`readdirSync`). */
2484
2516
  readonly readDirectory: (path: string) => ReadonlyArray<string>;
2485
- /** The UTF-8 text of the file at `path` (`readFileSync(path, "utf8")`). */
2486
- readonly readFile: (path: string) => string;
2517
+ /**
2518
+ * The raw bytes of the file at `path` (`readFileSync(path)`). Bytes, not
2519
+ * text: bash 3.2's `%q` mixes raw UTF-8 bytes with octal escapes inside
2520
+ * one `$'…'`, which only a byte-level parse reads back as bash does.
2521
+ */
2522
+ readonly readFile: (path: string) => Uint8Array;
2487
2523
  /** Modification time of the file at `path`, in epoch milliseconds (`statSync(path).mtimeMs`). */
2488
2524
  readonly mtimeMs: (path: string) => number;
2489
2525
  }
@@ -2616,10 +2652,21 @@ export declare function assembleManifest(rows: ManifestRow[], dbPath: string): C
2616
2652
  */
2617
2653
  export declare function assembleHistoryRecord(rows: HistoryRow[]): AssembledHistoryRecord;
2618
2654
  /**
2619
- * Ensure the SQLite database at `dbPath` is migrated. Runs migrations at
2620
- * most once per dbPath in the current process; concurrent calls share the
2621
- * same in-flight promise. Subsequent calls (after the first resolves) are
2622
- * no-ops.
2655
+ * Ensure the SQLite database at `dbPath` is migrated to the head of the
2656
+ * project migration set.
2657
+ *
2658
+ * Runs through `@effected/store`'s `Store.migrateOnce`: at most one
2659
+ * successful migration run per database per process, shared by every
2660
+ * concurrent caller (and by every copy of the kit loaded in the process).
2661
+ * The run is keyed on the real path of the file's directory, so a relative,
2662
+ * an absolute and a symlinked spelling of one database share it. A failed
2663
+ * run (for example `SQLITE_BUSY`) is not remembered: the next call tries
2664
+ * again. The connection opens with the same per-connection settings as
2665
+ * `makeSqliteStack` (`busy_timeout`, `synchronous = NORMAL`,
2666
+ * `foreign_keys = ON`).
2667
+ *
2668
+ * The parent directory must exist; `dbPath` must name a file (`":memory:"`
2669
+ * is a wiring defect).
2623
2670
  *
2624
2671
  * @param dbPath - the SQLite file to migrate
2625
2672
  * @param logLevel - the diagnostics level for the migrator's records; see `LoggerLive`
@@ -2629,12 +2676,6 @@ export declare function assembleHistoryRecord(rows: HistoryRow[]): AssembledHist
2629
2676
  * @public
2630
2677
  */
2631
2678
  export declare function ensureMigrated(dbPath: string, logLevel?: LogLevel.LogLevel, logFile?: string, env?: Readonly<Record<string, string | undefined>>): Promise<void>;
2632
- /**
2633
- * Reset the migration cache. Test-only.
2634
- *
2635
- * @internal
2636
- */
2637
- export declare function _resetMigrationCacheForTesting(): void;
2638
2679
  /** @public */
2639
2680
  export interface FailureSignatureInput {
2640
2681
  readonly error_name: string;
@@ -2647,6 +2688,64 @@ export interface FailureSignatureInput {
2647
2688
  export declare const normalizeAssertionShape: (message: string) => string | null;
2648
2689
  /** @public */
2649
2690
  export declare const computeFailureSignature: (input: FailureSignatureInput) => string;
2691
+ /**
2692
+ * Format an unexpected failure for stderr, with the issue URL appended.
2693
+ *
2694
+ * @remarks
2695
+ * Under Effect v4, `Effect.runPromise` / `Effect.runSync` reject with
2696
+ * `Cause.squash` of the failure: the error value itself, with no
2697
+ * FiberFailure wrapper and no `Cause` attached. So a `Cause` is used as it
2698
+ * is (one from `Effect.runPromiseExit` keeps every reason and the span
2699
+ * trail), and anything else is rendered as a defect, `Cause.die(err)`: an
2700
+ * unexpected throw is a bug to report, and a defect is the shape whose
2701
+ * document carries the stack. `@effected/cli`'s `CliFailure.toDoc` keeps
2702
+ * the program's own frames, hides `node_modules` and runtime frames,
2703
+ * follows an `Error.cause` chain, and renders a non-`Error` value as its
2704
+ * text.
2705
+ *
2706
+ * Total: `CliFailure.toDoc` guards the `stack`, `message`, `cause` and
2707
+ * `issue` reads itself, but wrapping a `Proxy` whose traps throw can still
2708
+ * throw, so any throw while formatting falls back to a fixed marker naming
2709
+ * the value's constructor.
2710
+ *
2711
+ * @param err - the thrown value, rejection reason, or `Cause`
2712
+ * @returns the formatted failure followed by the issue-report line
2713
+ * @public
2714
+ */
2715
+ export declare const formatFatalError: (err: unknown) => string;
2716
+ /**
2717
+ * The host identity a test run records: `host_source` / `host_value` /
2718
+ * `host_metadata` on `test_runs`.
2719
+ *
2720
+ * @public
2721
+ */
2722
+ export interface HostMetadataResult {
2723
+ readonly source: string | null;
2724
+ readonly value: string | null;
2725
+ readonly metadata: Record<string, unknown> | null;
2726
+ }
2727
+ /**
2728
+ * The host identity of an environment record, from `@effected/env`'s
2729
+ * `RuntimeEnv` snapshot (`session`, `ci` / `ciRun`, `termProgram`).
2730
+ *
2731
+ * Priority: the terminal session (`TMUX_PANE`, `WT_SESSION`, `WEZTERM_PANE`,
2732
+ * `KITTY_WINDOW_ID`, `TERM_SESSION_ID`), then the CI job (GitHub Actions run,
2733
+ * Buildkite, CircleCI, GitLab job). The first match is the source; when
2734
+ * nothing matches, all three fields are `null`. Every metadata object
2735
+ * carries `term_program` / `term_program_version` when they are set, then
2736
+ * the source's own keys (`tmux_socket` for a tmux pane, `ci`, `ci_provider`
2737
+ * and `github_run_attempt` for CI), in that key order.
2738
+ *
2739
+ * GNU screen (`STY`) is not a source: the snapshot is taken with `STY`
2740
+ * removed, so a screen session falls through to the next source exactly as
2741
+ * it did before the kit, and no `STY` row is ever written. The
2742
+ * `VSCODE_INJECTION` + parent-pid sources need `process.ppid` and are the
2743
+ * caller's.
2744
+ *
2745
+ * @param env - the environment record to read (the front end passes `process.env`)
2746
+ * @public
2747
+ */
2748
+ export declare const probeHostMetadataFromEnv: (env: Readonly<Record<string, string | undefined>>) => HostMetadataResult;
2650
2749
  /**
2651
2750
  * Run `effect` as a nested transaction that keeps the
2652
2751
  * {@link withPersistTransaction} caches honest.
@@ -2733,7 +2832,10 @@ export interface ResolveDataPathOptions {
2733
2832
  *
2734
2833
  * 1. `options.cacheDir` — programmatic override.
2735
2834
  * 2. `cacheDir` from `vitest-agent.config.toml`.
2736
- * 3. `<XDG data>/<normalized projectKey from config>/data.db`.
2835
+ * 3. `<XDG data>/<normalized projectKey from config>/data.db`. A
2836
+ * `projectKey` that normalizes to `.` or `..` fails with
2837
+ * `ProjectIdentitySourceError` (source `toml`) rather than writing
2838
+ * outside the data root.
2737
2839
  * 4. `<XDG data>/<projectKey resolved from cwd's package.json>/data.db`.
2738
2840
  * Source 4 prefers `repository.url` (canonicalized to `host__path`),
2739
2841
  * falling back to the normalized `name`. This matches the sidecar
@@ -2752,7 +2854,7 @@ export interface ResolveDataPathOptions {
2752
2854
  * @param options - Optional programmatic overrides.
2753
2855
  * @public
2754
2856
  */
2755
- export declare const resolveDataPath: (projectDir: string, options?: ResolveDataPathOptions) => Effect.Effect<string, import("@effected/xdg").AppDirsError | import("effect/PlatformError").PlatformError | import("@effected/config-file").ConfigReadError<import("@effected/config-file").ConfigCodecError>, AppDirs$1 | FileSystem.FileSystem | VitestAgentConfigFile>;
2857
+ export declare const resolveDataPath: (projectDir: string, options?: ResolveDataPathOptions) => Effect.Effect<string, import("@effected/xdg").AppDirsError | import("effect/PlatformError").PlatformError | ProjectIdentitySourceError | import("@effected/config-file").ConfigReadError<import("@effected/config-file").ConfigCodecError>, AppDirs$1 | FileSystem.FileSystem | VitestAgentConfigFile>;
2756
2858
  /**
2757
2859
  * Compute the project key for a workspace by reading `package.json`
2758
2860
  * fields directly. Returns the canonical `host__path` form when a git
@@ -2771,7 +2873,8 @@ export declare const resolveProjectKeyFromCwd: (cwd: string) => string;
2771
2873
  /**
2772
2874
  * {@link resolveProjectKeyFromCwd} read through the ambient
2773
2875
  * `FileSystem` service instead of `node:fs` — same upward walk to the
2774
- * nearest `package.json`, same precedence, same basename fallback.
2876
+ * nearest `package.json` (`@effected/walker`'s `Walker.ascend` +
2877
+ * `Walker.findUpward`), same precedence, same basename fallback.
2775
2878
  *
2776
2879
  * Never fails: a filesystem error while probing or reading is treated like
2777
2880
  * a missing or malformed `package.json`, exactly as the synchronous form
@@ -2786,7 +2889,8 @@ export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Eff
2786
2889
  * `projectDir`.
2787
2890
  *
2788
2891
  * The key is the root `package.json`'s `name`, normalized via
2789
- * `normalizeWorkspaceKey`. This becomes the directory segment under
2892
+ * `@effected/xdg`'s `AppDirs.segment` (a name that maps to `.` or `..` fails
2893
+ * with `AppDirsSegmentError`). This becomes the directory segment under
2790
2894
  * `$XDG_DATA_HOME/vitest-agent/` where the SQLite database lives.
2791
2895
  *
2792
2896
  * Fails with `WorkspaceRootNotFoundError` when no root workspace is
@@ -2806,7 +2910,7 @@ export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Eff
2806
2910
  * reporter's resolved `projectDir` (CLAUDE_PROJECT_DIR or process.cwd()).
2807
2911
  * @public
2808
2912
  */
2809
- export declare const resolveWorkspaceKey: (projectDir: string) => Effect.Effect<string, WorkspaceRootNotFoundError | WorkspaceDiscoveryFailure, WorkspaceDiscovery>;
2913
+ export declare const resolveWorkspaceKey: (projectDir: string) => Effect.Effect<string, WorkspaceRootNotFoundError | WorkspaceDiscoveryFailure | AppDirsSegmentError, WorkspaceDiscovery>;
2810
2914
  /** Build-time injected package version (see architecture.md D36). @public */
2811
2915
  export declare const CURRENT_ENGINE_VERSION: string;
2812
2916
  export { type AppDirs, type AppDirsError, type ArtifactKind, type ArtifactSuite, type FormatTriageOptions, type FormatWrapupOptions, type MigrationRecord, type Phase, type WrapupKind, migration as migration0001, migration$1 as migration0002, migration$2 as registryMigration0001, migration$3 as sessionMapMigration0001 };
package/index.js CHANGED
@@ -28,6 +28,7 @@ import { ProjectDiscoveryTest } from "./layers/ProjectDiscoveryTest.js";
28
28
  import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "./services/ProjectIdentity.js";
29
29
  import { ProjectIdentityLive, ProjectIdentityTest } from "./layers/ProjectIdentityLive.js";
30
30
  import { AgentContext, RunContext, RunContext$, RunContextService } from "./services/RunContext.js";
31
+ import { probeHostMetadataFromEnv } from "./utils/host-metadata.js";
31
32
  import { RunContextLive, RunContextTest } from "./layers/RunContextLive.js";
32
33
  import { formatStrayOutputLine, formatTriageEffect } from "./lib/format-triage.js";
33
34
  import { formatWrapupEffect } from "./lib/format-wrapup.js";
@@ -36,10 +37,11 @@ import migration$1 from "./migrations/0002_test_artifacts.js";
36
37
  import { PROJECT_MIGRATIONS } from "./migrations/index.js";
37
38
  import migration$2 from "./migrations/registry_0001_initial.js";
38
39
  import migration$3 from "./migrations/session_map_0001_initial.js";
40
+ import { RegistryStore, SessionMapStore } from "./stores.js";
39
41
  import { NodePlatformLayer, PlatformLive, makeSqliteStack } from "./platform.js";
40
42
  import { endAgentEffect } from "./programs/end-agent.js";
41
43
  import { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, REGISTRY_STORE_OPTIONS, SESSIONS_DB_FILENAME, resolveHookPaths, resolveSessionMapPath } from "./programs/hook-paths.js";
42
- import { SidecarPlatformLive } from "./programs/platform-sidecar.js";
44
+ import { RegistryStoreLive, SessionMapStoreLive, SidecarPlatformLive } from "./programs/platform-sidecar.js";
43
45
  import { recordSessionEnd, recordSessionStart } from "./programs/record-session.js";
44
46
  import { resolveSessionForRecording } from "./programs/resolve-session-for-recording.js";
45
47
  import { dispatchRecordTddArtifactEffect, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect } from "./programs/record-tdd-artifact.js";
@@ -47,14 +49,15 @@ import { parseAndValidateTurnPayload, recordTurnEffect } from "./programs/record
47
49
  import { recordRunWorkspaceChangesEffect } from "./programs/record-workspace-changes.js";
48
50
  import { IDEMPOTENCY_ROOT_SENTINEL, deriveIdempotencyKey } from "./services/idempotency.js";
49
51
  import { registerAgentEffect } from "./programs/register-agent.js";
50
- import { parseSessionEnvExports, recoverSessionContextFromSessionEnv } from "./programs/session-env.js";
52
+ import { recoverSessionContextFromSessionEnv } from "./programs/session-env.js";
51
53
  import { resolveProjectDir } from "./project-dir.js";
52
54
  import { assembleHistoryRecord, assembleManifest } from "./sql/assemblers.js";
53
- import { _resetMigrationCacheForTesting, ensureMigrated } from "./utils/ensure-migrated.js";
55
+ import { ensureMigrated } from "./utils/ensure-migrated.js";
54
56
  import { computeFailureSignature, normalizeAssertionShape } from "./utils/failure-signature.js";
57
+ import { formatFatalError } from "./utils/format-fatal-error.js";
55
58
  import { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect } from "./utils/resolve-project-key-from-cwd.js";
56
59
  import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js";
57
60
  import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
58
61
  import { CURRENT_ENGINE_VERSION } from "./version.js";
59
62
 
60
- export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, REGISTRY_STORE_OPTIONS, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatStrayOutputLine, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001, withPersistSavepoint, withPersistTransaction };
63
+ export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, REGISTRY_STORE_OPTIONS, RegistryStore, RegistryStoreLive, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SessionMapStore, SessionMapStoreLive, SidecarPlatformLive, VitestAgentConfigFile, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatFatalError, formatStrayOutputLine, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, probeHostMetadataFromEnv, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001, withPersistSavepoint, withPersistTransaction };
@@ -1,29 +1,55 @@
1
1
  import { VitestAgentConfigFile } from "../services/Config.js";
2
2
  import { ConfigFile, ConfigResolver, MergeStrategy } from "@effected/config-file";
3
3
  import { TomlCodec } from "@effected/config-file/toml";
4
+ import { Git } from "@effected/git";
4
5
  import { VitestAgentConfig } from "@vitest-agent/sdk";
6
+ import { Effect, Layer, Option } from "effect";
7
+ import { ChildProcessSpawner } from "effect/process";
5
8
 
6
9
  // src/layers/ConfigLive.ts
7
10
  const CONFIG_FILENAME = "vitest-agent.config.toml";
8
11
  /**
12
+ * The repository root git answers for `projectDir`, used as the upward
13
+ * walk's ceiling: the `Git` service when one is in context, else `Git.layer`
14
+ * over a `ChildProcessSpawner` when one is (every front end provides
15
+ * `NodeServices.layer`), else no answer. Neither is a requirement: outside
16
+ * a repository, with no `git` on `PATH`, or with no spawner at all, the walk
17
+ * runs to the filesystem root as it always did.
18
+ */
19
+ const repoRootCeiling = (projectDir) => Effect.gen(function* () {
20
+ const ask = (git) => git.repoRoot(projectDir).pipe(Effect.orElseSucceed(() => void 0));
21
+ const git = yield* Effect.serviceOption(Git);
22
+ if (Option.isSome(git)) return yield* ask(git.value);
23
+ const spawner = yield* Effect.serviceOption(ChildProcessSpawner.ChildProcessSpawner);
24
+ if (Option.isNone(spawner)) return void 0;
25
+ return yield* Effect.flatMap(Git, ask).pipe(Effect.provide(Git.layer), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner.value));
26
+ });
27
+ /**
9
28
  * Build the live `ConfigFile` layer for a given project directory.
10
29
  *
11
30
  * Resolves the optional `vitest-agent.config.toml` from (in order):
12
31
  *
13
32
  * 1. The workspace root (when `projectDir` is inside a pnpm/npm/yarn workspace).
14
33
  * 2. The git repository root (when `projectDir` is inside a git repo).
15
- * 3. Walking upward from `projectDir`.
34
+ * 3. Walking upward from `projectDir`, stopping at the git repository root
35
+ * (inclusive, compared physically) so a config file above the repository
36
+ * is never picked up. Outside a repository the walk runs to the
37
+ * filesystem root.
16
38
  *
17
39
  * The first found file wins (`MergeStrategy.firstMatch()`). If no file is
18
40
  * present, downstream callers use `loadOrDefault(new VitestAgentConfig({}))`
19
41
  * to get an empty config.
20
42
  *
43
+ * The repository root comes from `git rev-parse --show-toplevel` through
44
+ * `@effected/git`, using a `Git` or `ChildProcessSpawner` service if the
45
+ * context has one; neither is required (see `repoRootCeiling`).
46
+ *
21
47
  * @param projectDir - Absolute path inside the user's workspace. Resolvers
22
48
  * anchor here rather than `process.cwd()` so the plugin-spawned MCP server
23
49
  * sees the right config even when invoked from elsewhere.
24
50
  * @public
25
51
  */
26
- const ConfigLive = (projectDir) => ConfigFile.layer(VitestAgentConfigFile, {
52
+ const ConfigLive = (projectDir) => Layer.unwrap(Effect.map(repoRootCeiling(projectDir), (ceiling) => ConfigFile.layer(VitestAgentConfigFile, {
27
53
  schema: VitestAgentConfig,
28
54
  codec: TomlCodec,
29
55
  strategy: MergeStrategy.firstMatch(),
@@ -38,9 +64,10 @@ const ConfigLive = (projectDir) => ConfigFile.layer(VitestAgentConfigFile, {
38
64
  }),
39
65
  ConfigResolver.upwardWalk({
40
66
  filename: CONFIG_FILENAME,
41
- cwd: projectDir
67
+ cwd: projectDir,
68
+ ...ceiling !== void 0 && { ceiling }
42
69
  })
43
70
  ]
44
- });
71
+ })));
45
72
 
46
73
  export { ConfigLive };
@@ -72,7 +72,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
72
72
  const writeSettings = (hash, settings, envVars) => Effect.gen(function* () {
73
73
  yield* Effect.logDebug("writeSettings").pipe(Effect.annotateLogs({ hash }));
74
74
  yield* sql`INSERT OR IGNORE INTO settings (hash, vitest_version, pool, environment, test_timeout, hook_timeout, slow_test_threshold, max_concurrency, max_workers, isolate, bail, globals, file_parallelism, sequence_seed, coverage_provider) VALUES (${hash}, ${settings.vitestVersion}, ${settings.pool ?? null}, ${settings.environment ?? null}, ${settings.testTimeout ?? null}, ${settings.hookTimeout ?? null}, ${settings.slowTestThreshold ?? null}, ${settings.maxConcurrency ?? null}, ${settings.maxWorkers ?? null}, ${boolToInt(settings.isolate)}, ${settings.bail ?? null}, ${boolToInt(settings.globals)}, ${boolToInt(settings.fileParallelism)}, ${settings.sequenceSeed ?? null}, ${settings.coverageProvider ?? null})`;
75
- for (const [key, value] of Object.entries(envVars)) yield* sql`INSERT OR IGNORE INTO settings_env_vars (settings_hash, key, value) VALUES (${hash}, ${key}, ${value})`;
75
+ yield* sql`DELETE FROM settings_env_vars WHERE settings_hash = ${hash}`;
76
+ for (const [key, value] of Object.entries(envVars)) yield* sql`INSERT INTO settings_env_vars (settings_hash, key, value) VALUES (${hash}, ${key}, ${value})`;
76
77
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
77
78
  operation: "write",
78
79
  table: "settings",
@@ -3,11 +3,8 @@ import { Effect, Layer, Option } from "effect";
3
3
  import { Audience, CurrentRuntimeEnv } from "@effected/env";
4
4
 
5
5
  // src/layers/EnvironmentDetectorLive.ts
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
- };
6
+ /** The engine environment for a CI `@effected/env` names: GitHub Actions gets its own, every other provider is generic. */
7
+ const ciEnvironment = (ci) => ci === "github-actions" ? "ci-github" : "ci-generic";
11
8
  /**
12
9
  * Pure classification behind {@link EnvironmentDetectorLive}: an agent shell
13
10
  * wins, then GitHub Actions, then generic CI, else a terminal.
@@ -25,7 +22,7 @@ const classifyEnvironment = (runtime) => {
25
22
  case "agent": return "agent-shell";
26
23
  case "ci": return Option.match(runtime.ci, {
27
24
  onNone: () => "ci-generic",
28
- onSome: (ci) => CI_ENVIRONMENT[ci]
25
+ onSome: ciEnvironment
29
26
  });
30
27
  case "human": return "terminal";
31
28
  }
@@ -1,4 +1,4 @@
1
- import { Layer, LogLevel, Logger } from "effect";
1
+ import { Layer, Logger, Option } from "effect";
2
2
  import { CurrentRuntimeEnv } from "@effected/env";
3
3
  import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
4
4
  import * as NodePath from "@effect/platform-node/NodePath";
@@ -44,21 +44,14 @@ const LoggerLive = (level, logFile, env) => {
44
44
  }).pipe(Layer.provide(Layer.merge(NodeFileSystem.layer, NodePath.layer)));
45
45
  return env === void 0 ? sink : sink.pipe(Layer.provide(CurrentRuntimeEnv.layerFrom(env)));
46
46
  };
47
- const LEVEL_ALIASES = {
48
- warn: "Warn",
49
- warning: "Warn",
50
- error: "Error",
51
- info: "Info",
52
- debug: "Debug",
53
- trace: "Trace",
54
- fatal: "Fatal",
55
- all: "All",
56
- none: "None"
57
- };
58
47
  /**
59
48
  * Resolve log level from option or environment variable.
60
49
  * Priority: explicit option \> `VITEST_REPORTER_LOG_LEVEL` in `env` \> undefined.
61
50
  *
51
+ * Case-insensitive; `warning` is accepted for `Warn`. An unset or empty value,
52
+ * or one that names no level, resolves to `undefined` (no diagnostics) without
53
+ * a warning.
54
+ *
62
55
  * @param env - the environment map to consult (the front end passes `process.env`)
63
56
  * @param option - explicit override
64
57
  * @public
@@ -66,8 +59,7 @@ const LEVEL_ALIASES = {
66
59
  function resolveLogLevel(env, option) {
67
60
  const raw = option ?? env.VITEST_REPORTER_LOG_LEVEL;
68
61
  if (!raw) return void 0;
69
- const normalized = LEVEL_ALIASES[raw.toLowerCase()] ?? `${raw.charAt(0).toUpperCase()}${raw.slice(1).toLowerCase()}`;
70
- return LogLevel.values.includes(normalized) ? normalized : void 0;
62
+ return Option.getOrUndefined(CliLog.parseLevel(raw));
71
63
  }
72
64
  /**
73
65
  * Resolve log file from option or environment variable
@@ -1,36 +1,38 @@
1
1
  import { ProjectDiscovery } from "../services/ProjectDiscovery.js";
2
2
  import { DiscoveryError, NON_DISCOVERABLE_DIRS } from "@vitest-agent/sdk";
3
- import { Effect, FileSystem, Layer } from "effect";
3
+ import { Effect, FileSystem, Layer, Path } from "effect";
4
+ import { GlobPattern, GlobPatternOptions } from "@effected/glob";
5
+ import { descend } from "@effected/walker";
4
6
 
5
7
  // src/layers/ProjectDiscoveryLive.ts
6
- const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx)$/;
7
- const SKIP_DIRS = /* @__PURE__ */ new Set([
8
+ const SKIP_DIRS = [
8
9
  ...NON_DISCOVERABLE_DIRS,
9
10
  "coverage",
10
11
  ".turbo",
11
12
  ".vite"
12
- ]);
13
- function isTestFile(filePath) {
14
- return TEST_FILE_PATTERN.test(filePath);
15
- }
13
+ ];
14
+ const TEST_FILE_GLOB = "**/*.{test,spec}.{ts,tsx}";
15
+ const TEST_FILE_GLOB_OPTIONS = GlobPatternOptions.make({ dot: true });
16
16
  function testFileToSource(testFile) {
17
17
  return testFile.replace(/\.(test|spec)\.(ts|tsx)$/, ".$2");
18
18
  }
19
- function walkDir(fs, dir) {
20
- return Effect.gen(function* () {
21
- const entries = yield* fs.readDirectory(dir);
22
- const results = [];
23
- for (const entry of entries) {
24
- if (SKIP_DIRS.has(entry)) continue;
25
- const fullPath = `${dir}/${entry}`;
26
- if ((yield* fs.stat(fullPath)).type === "Directory") {
27
- const subFiles = yield* walkDir(fs, fullPath);
28
- results.push(...subFiles);
29
- } else results.push(fullPath);
30
- }
31
- return results;
19
+ /**
20
+ * Every test file under `rootDir`, as `${rootDir}/<relative>` paths sorted by
21
+ * relative path. `@effected/walker`'s `descend` follows directory symlinks
22
+ * (as the former `fs.stat`-based walk did) under its ancestor-chain cycle
23
+ * guard, so a link back to an ancestor is skipped instead of recursing until
24
+ * the OS answers `ELOOP` and failing the whole discovery.
25
+ */
26
+ const walkTestFiles = (rootDir) => Effect.gen(function* () {
27
+ const pattern = yield* GlobPattern.compile(TEST_FILE_GLOB, TEST_FILE_GLOB_OPTIONS);
28
+ const relative = yield* descend(pattern, {
29
+ cwd: rootDir,
30
+ prune: SKIP_DIRS,
31
+ followSymlinks: true
32
32
  });
33
- }
33
+ const base = rootDir.endsWith("/") ? rootDir.slice(0, -1) : rootDir;
34
+ return relative.map((rel) => `${base}/${rel}`);
35
+ }).pipe(Effect.provide(Path.layer));
34
36
  /** @public */
35
37
  const ProjectDiscoveryLive = Layer.effect(ProjectDiscovery, Effect.gen(function* () {
36
38
  const fs = yield* FileSystem.FileSystem;
@@ -44,11 +46,11 @@ const ProjectDiscoveryLive = Layer.effect(ProjectDiscovery, Effect.gen(function*
44
46
  })));
45
47
  return {
46
48
  discoverTestFiles: (rootDir) => Effect.gen(function* () {
47
- const testFiles = (yield* walkDir(fs, rootDir).pipe(Effect.mapError((error) => new DiscoveryError({
49
+ const testFiles = yield* walkTestFiles(rootDir).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.mapError((error) => new DiscoveryError({
48
50
  operation: "glob",
49
51
  path: rootDir,
50
52
  reason: String(error)
51
- })))).filter(isTestFile);
53
+ })));
52
54
  const entries = [];
53
55
  for (const testFile of testFiles) {
54
56
  const sourceFiles = yield* mapTestToSource(testFile);