@vitest-agent/engine 1.2.0 → 2.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,8 +1,8 @@
1
1
  import { Context, Effect, FileSystem, Layer, LogLevel, Option, Path, Schema } from "effect";
2
2
  import { ConfigFileShape } from "@effected/config-file";
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, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, Phase, ProjectIdentityNotResolvableError, RegistrationConflictError, ResolvedThresholds, RunRetentionPolicy, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
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, 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
4
  import { SqlClient } from "effect/sql/SqlClient";
5
- import { RuntimeEnv } from "@effected/env";
5
+ import { CiName, RuntimeEnv } from "@effected/env";
6
6
  import { WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspaceRootNotFoundError } from "@effected/workspaces";
7
7
  import { AppDirs, AppDirsError, XdgEnvError } from "@effected/xdg";
8
8
  import { ChildProcessSpawner } from "effect/process";
@@ -141,6 +141,13 @@ export interface TestRunInput {
141
141
  readonly hostSource?: string | null;
142
142
  readonly hostValue?: string | null;
143
143
  readonly hostMetadata?: Record<string, unknown> | null;
144
+ /**
145
+ * Output the run wrote straight to the terminal past Vitest's console
146
+ * capture (issue 578). A run-level signal: every project row of one
147
+ * invocation carries the same value. Omitted or `null` when the run
148
+ * recorded nothing, which the row stores as NULL in every `stray_*` column.
149
+ */
150
+ readonly strayOutput?: StrayOutput | null;
144
151
  }
145
152
  /** @public */
146
153
  export interface ModuleInput {
@@ -615,6 +622,32 @@ export interface ProjectRunSummary {
615
622
  readonly passed: number;
616
623
  readonly failed: number;
617
624
  readonly skipped: number;
625
+ /** The latest run's stray output (issue 578); `null` when it recorded none. */
626
+ readonly strayOutput: StrayOutput | null;
627
+ }
628
+ /**
629
+ * Options for `DataReader.getStrayOutputHistory`.
630
+ * @public
631
+ */
632
+ export interface StrayOutputHistoryOptions {
633
+ /** Only runs of this project; every project otherwise. */
634
+ readonly project?: string;
635
+ /** How many of the most recent runs to consider. Default 10. */
636
+ readonly limit?: number;
637
+ }
638
+ /**
639
+ * How often recent runs wrote stray output (issue 578). A "run" is one
640
+ * reporter invocation: stray output is a run-level signal that every project
641
+ * row of an invocation repeats, so rows are counted once per `invocation_id`.
642
+ * @public
643
+ */
644
+ export interface StrayOutputHistory {
645
+ /** Runs considered: the most recent ones, up to the requested limit. */
646
+ readonly runs: number;
647
+ /** How many of them recorded any stray output. */
648
+ readonly runsWithStrayOutput: number;
649
+ /** The most recent run's stray output, when that run recorded any. */
650
+ readonly latest: StrayOutput | null;
618
651
  }
619
652
  /** @public */
620
653
  export interface FlakyTest {
@@ -973,6 +1006,8 @@ export interface TestArtifactRow {
973
1006
  declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/DataReader", {
974
1007
  readonly getLatestRun: (project: string) => Effect.Effect<Option.Option<AgentReport>, DataStoreError>;
975
1008
  readonly getRunsByProject: () => Effect.Effect<ReadonlyArray<ProjectRunSummary>, DataStoreError>;
1009
+ /** How often the most recent runs wrote stray output; see {@link StrayOutputHistory}. */
1010
+ readonly getStrayOutputHistory: (options?: StrayOutputHistoryOptions) => Effect.Effect<StrayOutputHistory, DataStoreError>;
976
1011
  readonly getHistory: (project: string, options?: HistoryQueryOptions) => Effect.Effect<HistoryRecord, DataStoreError>;
977
1012
  readonly getBaselines: (project: string) => Effect.Effect<Option.Option<CoverageBaselines>, DataStoreError>;
978
1013
  readonly getTrends: (project: string, limit?: number) => Effect.Effect<Option.Option<TrendRecord>, DataStoreError>;
@@ -1205,8 +1240,21 @@ declare const EnvironmentDetector_base: Context.ServiceClass<EnvironmentDetector
1205
1240
  readonly detect: () => Effect.Effect<Environment>;
1206
1241
  readonly isAgent: Effect.Effect<boolean>;
1207
1242
  readonly agentName: Effect.Effect<string | undefined>;
1243
+ readonly ci: Effect.Effect<Option.Option<CiName>>;
1208
1244
  }>;
1209
- /** @public */
1245
+ /**
1246
+ * Who is running the program and where.
1247
+ *
1248
+ * @remarks
1249
+ * `detect` is the console audience: an agent marker wins over CI, so an
1250
+ * agent inside GitHub Actions reads `agent-shell`. `ci` is the CI vendor
1251
+ * independently of that precedence, so a caller can keep GitHub-only
1252
+ * extras (the step summary, annotations, the `github-actions` reporter)
1253
+ * on for the humans reviewing the job even when the console audience is
1254
+ * the agent (issue #624).
1255
+ *
1256
+ * @public
1257
+ */
1210
1258
  export declare class EnvironmentDetector extends EnvironmentDetector_base {}
1211
1259
  /**
1212
1260
  * Pure classification behind {@link EnvironmentDetectorLive}: an agent shell
@@ -1235,9 +1283,21 @@ export declare const classifyEnvironment: (runtime: RuntimeEnv) => Environment;
1235
1283
  * @public
1236
1284
  */
1237
1285
  export declare const EnvironmentDetectorLive: (env: Record<string, string | undefined>) => Layer.Layer<EnvironmentDetector>;
1238
- /** @public */
1286
+ /**
1287
+ * A fixed `EnvironmentDetector` for tests.
1288
+ *
1289
+ * @remarks
1290
+ * `ci` defaults to the vendor the environment implies (`ci-github` is
1291
+ * GitHub Actions, `ci-generic` another CI, the rest none). Pass
1292
+ * `options.ci` to decouple the two, as the live detector does for an agent
1293
+ * running inside GitHub Actions: `layer("agent-shell", { ci: "github-actions" })`.
1294
+ *
1295
+ * @public
1296
+ */
1239
1297
  export declare const EnvironmentDetectorTest: {
1240
- layer: (env?: Environment) => Layer.Layer<EnvironmentDetector, never, never>;
1298
+ layer: (env?: Environment, options?: {
1299
+ readonly ci?: CiName;
1300
+ }) => Layer.Layer<EnvironmentDetector, never, never>;
1241
1301
  };
1242
1302
  declare const ExecutorResolver_base: Context.ServiceClass<ExecutorResolver, "vitest-agent/ExecutorResolver", {
1243
1303
  readonly resolve: (env: Environment) => Effect.Effect<Executor>;
@@ -1556,12 +1616,18 @@ export interface ResolveProjectIdentityOptions {
1556
1616
  */
1557
1617
  export declare const resolveProjectIdentityFromCandidates: (candidates: ProjectIdentityCandidates) => ResolvedIdentity | null;
1558
1618
  declare const ProjectIdentity_base: Context.ServiceClass<ProjectIdentity, "vitest-agent/ProjectIdentity", {
1559
- readonly resolve: (workspaceRoot: string, options?: ResolveProjectIdentityOptions) => Effect.Effect<ResolvedIdentity, ProjectIdentityNotResolvableError>;
1619
+ readonly resolve: (workspaceRoot: string, options?: ResolveProjectIdentityOptions) => Effect.Effect<ResolvedIdentity, ProjectIdentityError>;
1560
1620
  }>;
1561
1621
  /**
1562
1622
  * Service tag. The Live layer (in `layers/ProjectIdentityLive.ts`)
1563
1623
  * wires up the I/O sources; tests provide a stub that returns a
1564
1624
  * pre-built `ResolvedIdentity`.
1625
+ *
1626
+ * `resolve` fails with `ProjectIdentityNotResolvableError` when every
1627
+ * source is absent or empty, and with `ProjectIdentitySourceError` when a
1628
+ * source is present but unusable (a malformed `vitest-agent.config.toml`,
1629
+ * an unreadable root `package.json`): a broken config is reported, never
1630
+ * silently replaced by the default identity (issue 321).
1565
1631
  * @public
1566
1632
  */
1567
1633
  export declare class ProjectIdentity extends ProjectIdentity_base {}
@@ -1581,17 +1647,21 @@ export type CandidateContext = FileSystem.FileSystem | WorkspaceDiscovery | Vite
1581
1647
  * `FileSystem`, `CommandExecutor`, `WorkspaceDiscovery`, and
1582
1648
  * `VitestAgentConfigFile`. The platform context comes from
1583
1649
  * `NodeServices.layer` at the entry point.
1650
+ *
1651
+ * Absent sources fall through (no config file, no workspace root, no
1652
+ * `repository` field, no git remote); a present source that cannot be read
1653
+ * or decoded fails with `ProjectIdentitySourceError`.
1584
1654
  * @public
1585
1655
  */
1586
1656
  export declare const ProjectIdentityLive: Layer.Layer<ProjectIdentity, never, CandidateContext>;
1587
1657
  /**
1588
1658
  * Build a Test layer that returns a fixed `ResolvedIdentity` (or
1589
- * fails with `ProjectIdentityNotResolvableError`). Use in unit tests
1659
+ * fails with the given `ProjectIdentityError`). Use in unit tests
1590
1660
  * that depend on `ProjectIdentity` but don't want to wire upstream
1591
1661
  * services.
1592
1662
  * @public
1593
1663
  */
1594
- export declare const ProjectIdentityTest: (result: ResolvedIdentity | ProjectIdentityNotResolvableError) => Layer.Layer<ProjectIdentity, never, never>;
1664
+ export declare const ProjectIdentityTest: (result: ResolvedIdentity | ProjectIdentityError) => Layer.Layer<ProjectIdentity, never, never>;
1595
1665
  declare const RunContext_base: Schema.Class<RunContext, Schema.Struct<{
1596
1666
  readonly gitBranch: Schema.NullOr<Schema.String>;
1597
1667
  readonly gitCommitSha: Schema.NullOr<Schema.String>;
@@ -1671,6 +1741,13 @@ interface FormatTriageOptions {
1671
1741
  readonly project?: string;
1672
1742
  readonly maxLines?: number;
1673
1743
  }
1744
+ /**
1745
+ * The "stray output" line of the Recent Test Runs section, or `undefined`
1746
+ * when none of the recent runs wrote past Vitest's console capture (issue
1747
+ * 578). Pure, so the wording is testable without a database.
1748
+ * @public
1749
+ */
1750
+ export declare const formatStrayOutputLine: (history: StrayOutputHistory) => string | undefined;
1674
1751
  /**
1675
1752
  * Generates an orientation triage markdown string for LLM agents.
1676
1753
  * Summarises recent test runs, active sessions, acceptance metrics,
@@ -1717,6 +1794,7 @@ export declare const PROJECT_MIGRATIONS: {
1717
1794
  readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1718
1795
  readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1719
1796
  readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1797
+ readonly "0004_stray_output": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1720
1798
  };
1721
1799
  /**
1722
1800
  * Migration for the global discovery registry SQLite at
@@ -2674,7 +2752,7 @@ export interface ResolveDataPathOptions {
2674
2752
  * @param options - Optional programmatic overrides.
2675
2753
  * @public
2676
2754
  */
2677
- export declare const resolveDataPath: (projectDir: string, options?: ResolveDataPathOptions) => Effect.Effect<any, import("@effected/xdg").AppDirsError | import("effect/PlatformError").PlatformError | import("@effected/config-file").ConfigReadError<import("@effected/config-file").ConfigCodecError>, AppDirs | FileSystem.FileSystem | VitestAgentConfigFile>;
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 | FileSystem.FileSystem | VitestAgentConfigFile>;
2678
2756
  /**
2679
2757
  * Compute the project key for a workspace by reading `package.json`
2680
2758
  * fields directly. Returns the canonical `host__path` form when a git
package/index.js CHANGED
@@ -29,7 +29,7 @@ import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "./service
29
29
  import { ProjectIdentityLive, ProjectIdentityTest } from "./layers/ProjectIdentityLive.js";
30
30
  import { AgentContext, RunContext, RunContext$, RunContextService } from "./services/RunContext.js";
31
31
  import { RunContextLive, RunContextTest } from "./layers/RunContextLive.js";
32
- import { formatTriageEffect } from "./lib/format-triage.js";
32
+ import { formatStrayOutputLine, formatTriageEffect } from "./lib/format-triage.js";
33
33
  import { formatWrapupEffect } from "./lib/format-wrapup.js";
34
34
  import migration from "./migrations/0001_initial.js";
35
35
  import migration$1 from "./migrations/0002_test_artifacts.js";
@@ -57,4 +57,4 @@ import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js
57
57
  import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
58
58
  import { CURRENT_ENGINE_VERSION } from "./version.js";
59
59
 
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, 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 };
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 };
@@ -1,5 +1,6 @@
1
1
  import { VitestAgentConfigFile } from "../services/Config.js";
2
- import { ConfigFile, ConfigResolver, MergeStrategy, TomlCodec } from "@effected/config-file";
2
+ import { ConfigFile, ConfigResolver, MergeStrategy } from "@effected/config-file";
3
+ import { TomlCodec } from "@effected/config-file/toml";
3
4
  import { VitestAgentConfig } from "@vitest-agent/sdk";
4
5
 
5
6
  // src/layers/ConfigLive.ts
@@ -1,10 +1,25 @@
1
1
  import { DataReader } from "../services/DataReader.js";
2
2
  import { historyKey } from "../services/HistoryTracker.js";
3
- import { DataStoreError, extractSqlReason } from "@vitest-agent/sdk";
4
- import { Effect, Layer, Option } from "effect";
3
+ import { DataStoreError, StrayOutputSample, extractSqlReason } from "@vitest-agent/sdk";
4
+ import { Effect, Layer, Option, Schema } from "effect";
5
5
  import { SqlClient } from "effect/sql/SqlClient";
6
6
 
7
7
  // src/layers/DataReaderLive.ts
8
+ const decodeStraySamples = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Array(StrayOutputSample)));
9
+ /**
10
+ * The run's stray output from its `test_runs` row, or `null` when the row
11
+ * recorded none (every `stray_*` column NULL: a run that wrote nothing, a
12
+ * passthrough-mode run, or a row older than migration 0004). Pure; the
13
+ * inverse is `DataStoreLive`'s `strayOutputColumns`. A samples column that
14
+ * does not decode reads as no samples rather than failing the row.
15
+ */
16
+ const strayOutputFromRow = (row) => row.stray_total === null ? null : {
17
+ total: row.stray_total,
18
+ stdout: row.stray_stdout ?? 0,
19
+ stderr: row.stray_stderr ?? 0,
20
+ bytes: row.stray_bytes ?? 0,
21
+ samples: Option.getOrElse(decodeStraySamples(row.stray_samples ?? "[]"), () => [])
22
+ };
8
23
  /** @public */
9
24
  const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
10
25
  const sql = yield* SqlClient;
@@ -32,7 +47,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
32
47
  });
33
48
  const getLatestRun = (project) => Effect.gen(function* () {
34
49
  yield* Effect.logDebug("getLatestRun").pipe(Effect.annotateLogs({ project }));
35
- const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project
50
+ const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project, stray_total, stray_stdout, stray_stderr, stray_bytes, stray_samples
36
51
  FROM test_runs
37
52
  WHERE project = ${project}
38
53
  ORDER BY timestamp DESC LIMIT 1`;
@@ -102,6 +117,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
102
117
  WHERE run_id = ${run.id} AND scope = 'unhandled'`;
103
118
  const failedFiles = failedModules.map((m) => m.file_path);
104
119
  const projectName = run.project;
120
+ const strayOutput = strayOutputFromRow(run);
105
121
  const coverageRunId = yield* resolveCoverageRunId(run.id);
106
122
  const fileCovRows = yield* sql`SELECT f.path AS file_path, fc.statements, fc.branches, fc.functions, fc.lines,
107
123
  fc.uncovered_lines, fc.tier
@@ -179,7 +195,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
179
195
  ...e.diff != null ? { diff: e.diff } : {}
180
196
  })),
181
197
  failedFiles,
182
- ...coverage ? { coverage } : {}
198
+ ...coverage ? { coverage } : {},
199
+ ...strayOutput !== null ? { strayOutput } : {}
183
200
  };
184
201
  return Option.some(report);
185
202
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -196,7 +213,12 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
196
213
  t1.total,
197
214
  t1.passed,
198
215
  t1.failed,
199
- t1.skipped
216
+ t1.skipped,
217
+ t1.stray_total,
218
+ t1.stray_stdout,
219
+ t1.stray_stderr,
220
+ t1.stray_bytes,
221
+ t1.stray_samples
200
222
  FROM test_runs t1
201
223
  INNER JOIN (
202
224
  SELECT project, MAX(timestamp) as max_ts
@@ -210,13 +232,43 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
210
232
  total: r.total,
211
233
  passed: r.passed,
212
234
  failed: r.failed,
213
- skipped: r.skipped
235
+ skipped: r.skipped,
236
+ strayOutput: strayOutputFromRow(r)
214
237
  }));
215
238
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
216
239
  operation: "read",
217
240
  table: "test_runs",
218
241
  reason: extractSqlReason(e)
219
242
  })));
243
+ const getStrayOutputHistory = (options = {}) => Effect.gen(function* () {
244
+ const limit = options.limit ?? 10;
245
+ yield* Effect.logDebug("getStrayOutputHistory").pipe(Effect.annotateLogs({
246
+ project: options.project ?? "*",
247
+ limit
248
+ }));
249
+ const rows = yield* sql`SELECT
250
+ MAX(timestamp) AS timestamp,
251
+ MAX(stray_total) AS stray_total,
252
+ MAX(stray_stdout) AS stray_stdout,
253
+ MAX(stray_stderr) AS stray_stderr,
254
+ MAX(stray_bytes) AS stray_bytes,
255
+ MAX(stray_samples) AS stray_samples
256
+ FROM test_runs
257
+ ${options.project !== void 0 ? sql`WHERE project = ${options.project}` : sql``}
258
+ GROUP BY invocation_id
259
+ ORDER BY timestamp DESC
260
+ LIMIT ${limit}`;
261
+ const recorded = rows.map(strayOutputFromRow);
262
+ return {
263
+ runs: rows.length,
264
+ runsWithStrayOutput: recorded.filter((s) => s !== null).length,
265
+ latest: recorded[0] ?? null
266
+ };
267
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
268
+ operation: "read",
269
+ table: "test_runs",
270
+ reason: extractSqlReason(e)
271
+ })));
220
272
  const getHistory = (project, options) => Effect.gen(function* () {
221
273
  yield* Effect.logDebug("getHistory").pipe(Effect.annotateLogs({
222
274
  project,
@@ -267,7 +319,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
267
319
  project,
268
320
  updatedAt: tests.reduce((latest, t) => {
269
321
  const ts = t.runs[0]?.timestamp ?? "";
270
- return ts > latest ? ts : latest;
322
+ if (ts > latest) return ts;
323
+ return latest;
271
324
  }, "") || (/* @__PURE__ */ new Date()).toISOString(),
272
325
  tests
273
326
  };
@@ -800,8 +853,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
800
853
  const manifest = {
801
854
  updatedAt: rows.reduce((latest, r) => {
802
855
  if (!r.last_run) return latest;
803
- if (!latest) return r.last_run;
804
- return r.last_run > latest ? r.last_run : latest;
856
+ if (!latest || r.last_run > latest) return r.last_run;
857
+ return latest;
805
858
  }, null) ?? (/* @__PURE__ */ new Date()).toISOString(),
806
859
  cacheDir: dbPath,
807
860
  projects
@@ -1878,6 +1931,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1878
1931
  return {
1879
1932
  getLatestRun,
1880
1933
  getRunsByProject,
1934
+ getStrayOutputHistory,
1881
1935
  getHistory,
1882
1936
  getBaselines,
1883
1937
  getTrends,
@@ -7,6 +7,24 @@ import { Buffer } from "node:buffer";
7
7
  import { randomUUID } from "node:crypto";
8
8
 
9
9
  // src/layers/DataStoreLive.ts
10
+ /**
11
+ * The five `test_runs.stray_*` column values for a run's stray output: all
12
+ * NULL when the run recorded nothing, so a reader cannot mistake "nothing
13
+ * recorded" for "zero lines" (pure; the inverse lives in `DataReaderLive`).
14
+ */
15
+ const strayOutputColumns = (stray) => stray == null ? {
16
+ total: null,
17
+ stdout: null,
18
+ stderr: null,
19
+ bytes: null,
20
+ samples: null
21
+ } : {
22
+ total: stray.total,
23
+ stdout: stray.stdout,
24
+ stderr: stray.stderr,
25
+ bytes: stray.bytes,
26
+ samples: JSON.stringify(stray.samples)
27
+ };
10
28
  const isLegalLifecycleTransition = (from, to) => {
11
29
  if (from === to) return true;
12
30
  if (from === "done" || from === "abandoned") return false;
@@ -70,7 +88,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
70
88
  const conversationId = input.conversationId ?? null;
71
89
  const gitDirtyInt = input.gitDirty === void 0 || input.gitDirty === null ? null : input.gitDirty ? 1 : 0;
72
90
  const hostMetadataJson = input.hostMetadata == null ? null : JSON.stringify(input.hostMetadata);
73
- yield* sql`INSERT INTO test_runs (invocation_id, project, settings_hash, timestamp, commit_sha, branch, reason, duration, total, passed, failed, skipped, scoped, snapshot_added, snapshot_matched, snapshot_unmatched, snapshot_updated, snapshot_unchecked, snapshot_total, snapshot_failure, snapshot_did_update, snapshot_files_added, snapshot_files_removed, snapshot_files_unmatched, snapshot_files_updated, actor_type, agent_id, conversation_id, git_branch, git_commit_sha, git_dirty, git_upstream, git_worktree_dir, host_source, host_value, host_metadata) VALUES (${input.invocationId}, ${input.project}, ${input.settingsHash}, ${input.timestamp}, ${input.commitSha ?? null}, ${input.branch ?? null}, ${input.reason}, ${input.duration}, ${input.total}, ${input.passed}, ${input.failed}, ${input.skipped}, ${input.scoped ? 1 : 0}, ${input.snapshotAdded ?? 0}, ${input.snapshotMatched ?? 0}, ${input.snapshotUnmatched ?? 0}, ${input.snapshotUpdated ?? 0}, ${input.snapshotUnchecked ?? 0}, ${input.snapshotTotal ?? 0}, ${boolToInt(input.snapshotFailure) ?? 0}, ${boolToInt(input.snapshotDidUpdate) ?? 0}, ${input.snapshotFilesAdded ?? 0}, ${input.snapshotFilesRemoved ?? 0}, ${input.snapshotFilesUnmatched ?? 0}, ${input.snapshotFilesUpdated ?? 0}, ${actorType}, ${agentId}, ${conversationId}, ${input.gitBranch ?? null}, ${input.gitCommitSha ?? null}, ${gitDirtyInt}, ${input.gitUpstream ?? null}, ${input.gitWorktreeDir ?? null}, ${input.hostSource ?? null}, ${input.hostValue ?? null}, ${hostMetadataJson})`;
91
+ const stray = strayOutputColumns(input.strayOutput);
92
+ yield* sql`INSERT INTO test_runs (invocation_id, project, settings_hash, timestamp, commit_sha, branch, reason, duration, total, passed, failed, skipped, scoped, snapshot_added, snapshot_matched, snapshot_unmatched, snapshot_updated, snapshot_unchecked, snapshot_total, snapshot_failure, snapshot_did_update, snapshot_files_added, snapshot_files_removed, snapshot_files_unmatched, snapshot_files_updated, actor_type, agent_id, conversation_id, git_branch, git_commit_sha, git_dirty, git_upstream, git_worktree_dir, host_source, host_value, host_metadata, stray_total, stray_stdout, stray_stderr, stray_bytes, stray_samples) VALUES (${input.invocationId}, ${input.project}, ${input.settingsHash}, ${input.timestamp}, ${input.commitSha ?? null}, ${input.branch ?? null}, ${input.reason}, ${input.duration}, ${input.total}, ${input.passed}, ${input.failed}, ${input.skipped}, ${input.scoped ? 1 : 0}, ${input.snapshotAdded ?? 0}, ${input.snapshotMatched ?? 0}, ${input.snapshotUnmatched ?? 0}, ${input.snapshotUpdated ?? 0}, ${input.snapshotUnchecked ?? 0}, ${input.snapshotTotal ?? 0}, ${boolToInt(input.snapshotFailure) ?? 0}, ${boolToInt(input.snapshotDidUpdate) ?? 0}, ${input.snapshotFilesAdded ?? 0}, ${input.snapshotFilesRemoved ?? 0}, ${input.snapshotFilesUnmatched ?? 0}, ${input.snapshotFilesUpdated ?? 0}, ${actorType}, ${agentId}, ${conversationId}, ${input.gitBranch ?? null}, ${input.gitCommitSha ?? null}, ${gitDirtyInt}, ${input.gitUpstream ?? null}, ${input.gitWorktreeDir ?? null}, ${input.hostSource ?? null}, ${input.hostValue ?? null}, ${hostMetadataJson}, ${stray.total}, ${stray.stdout}, ${stray.stderr}, ${stray.bytes}, ${stray.samples})`;
74
93
  return (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
75
94
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
76
95
  operation: "write",
@@ -48,7 +48,8 @@ const EnvironmentDetectorLive = (env) => Layer.effect(EnvironmentDetector, Effec
48
48
  return {
49
49
  detect: () => Effect.succeed(environment),
50
50
  isAgent: Effect.succeed(Option.isSome(runtime.agent)),
51
- agentName: Effect.succeed(Option.getOrUndefined(runtime.agent))
51
+ agentName: Effect.succeed(Option.getOrUndefined(runtime.agent)),
52
+ ci: Effect.succeed(runtime.ci)
52
53
  };
53
54
  })).pipe(Layer.provide(CurrentRuntimeEnv.layerFrom(env)));
54
55
 
@@ -1,12 +1,30 @@
1
1
  import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
2
- import { Effect, Layer } from "effect";
2
+ import { Effect, Layer, Option } from "effect";
3
3
 
4
4
  // src/layers/EnvironmentDetectorTest.ts
5
- /** @public */
6
- const EnvironmentDetectorTest = { layer: (env = "terminal") => Layer.succeed(EnvironmentDetector, {
5
+ /** The CI vendor each environment implies when a test does not say otherwise. */
6
+ const CI_FOR_ENVIRONMENT = {
7
+ "ci-github": Option.some("github-actions"),
8
+ "ci-generic": Option.some("generic"),
9
+ "agent-shell": Option.none(),
10
+ terminal: Option.none()
11
+ };
12
+ /**
13
+ * A fixed `EnvironmentDetector` for tests.
14
+ *
15
+ * @remarks
16
+ * `ci` defaults to the vendor the environment implies (`ci-github` is
17
+ * GitHub Actions, `ci-generic` another CI, the rest none). Pass
18
+ * `options.ci` to decouple the two, as the live detector does for an agent
19
+ * running inside GitHub Actions: `layer("agent-shell", { ci: "github-actions" })`.
20
+ *
21
+ * @public
22
+ */
23
+ const EnvironmentDetectorTest = { layer: (env = "terminal", options = {}) => Layer.succeed(EnvironmentDetector, {
7
24
  detect: () => Effect.succeed(env),
8
25
  isAgent: Effect.succeed(env === "agent-shell"),
9
- agentName: Effect.succeed(env === "agent-shell" ? "test-agent" : void 0)
26
+ agentName: Effect.succeed(env === "agent-shell" ? "test-agent" : void 0),
27
+ ci: Effect.succeed(options.ci !== void 0 ? Option.some(options.ci) : CI_FOR_ENVIRONMENT[env])
10
28
  }) };
11
29
 
12
30
  export { EnvironmentDetectorTest };
@@ -1,11 +1,21 @@
1
1
  import { VitestAgentConfigFile } from "../services/Config.js";
2
2
  import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "../services/ProjectIdentity.js";
3
- import { ProjectIdentityNotResolvableError, VitestAgentConfig } from "@vitest-agent/sdk";
3
+ import { ProjectIdentityNotResolvableError, ProjectIdentitySourceError, VitestAgentConfig } from "@vitest-agent/sdk";
4
4
  import { Effect, FileSystem, Layer, Option } from "effect";
5
5
  import { WorkspaceDiscovery } from "@effected/workspaces";
6
6
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
7
7
 
8
8
  // src/layers/ProjectIdentityLive.ts
9
+ /** The one-line text of a kit failure, for `ProjectIdentitySourceError.detail`. */
10
+ const detailOf = (cause) => cause instanceof Error && cause.message.length > 0 ? cause.message : String(cause);
11
+ /**
12
+ * The git remote is an OPTIONAL source: a repository with no
13
+ * `remote.origin.url`, a directory that is not a git checkout, or a machine
14
+ * with no `git` on `PATH` are all "no remote" rather than failures. `git
15
+ * config --get` exits non-zero for an unset key, so every spawner failure
16
+ * folds to `Option.none()` on purpose — this is the one catch in this module
17
+ * that stays broad, and it is a policy, not an accident.
18
+ */
9
19
  const readGitRemote = (cwd) => Effect.gen(function* () {
10
20
  const trimmed = (yield* (yield* ChildProcessSpawner.ChildProcessSpawner).string(ChildProcess.make("git", [
11
21
  "config",
@@ -14,23 +24,55 @@ const readGitRemote = (cwd) => Effect.gen(function* () {
14
24
  ]).pipe(ChildProcess.setCwd(cwd)))).trim();
15
25
  return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
16
26
  }).pipe(Effect.catch(() => Effect.succeed(Option.none())));
27
+ /**
28
+ * The `repository` URL of the root `package.json` discovery named.
29
+ *
30
+ * A file that is not there (`NotFound`) is an absent source. Any other
31
+ * filesystem failure (permission denied, an I/O error) and a body that is not
32
+ * JSON are `ProjectIdentitySourceError`s: the file exists and says
33
+ * something, and silently reading it as "no repository" would hand the
34
+ * operator the wrong identity.
35
+ */
17
36
  const readPackageRepoUrl = (packageJsonPath) => Effect.gen(function* () {
18
- const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.catch(() => Effect.succeed("")));
19
- if (raw.length === 0) return Option.none();
20
- try {
21
- const repo = JSON.parse(raw).repository;
22
- if (typeof repo === "string" && repo.trim().length > 0) return Option.some(repo.trim());
23
- if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
24
- const url = repo.url.trim();
25
- return url.length > 0 ? Option.some(url) : Option.none();
26
- }
27
- } catch {}
37
+ const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.map(Option.some), Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(new ProjectIdentitySourceError({
38
+ source: "package-json",
39
+ path: packageJsonPath,
40
+ detail: detailOf(error),
41
+ cause: error
42
+ }))));
43
+ if (Option.isNone(raw)) return Option.none();
44
+ const parsed = yield* Effect.try({
45
+ try: () => JSON.parse(raw.value),
46
+ catch: (cause) => new ProjectIdentitySourceError({
47
+ source: "package-json",
48
+ path: packageJsonPath,
49
+ detail: detailOf(cause),
50
+ cause
51
+ })
52
+ });
53
+ if (parsed === null || typeof parsed !== "object") return Option.none();
54
+ const repo = parsed.repository;
55
+ if (typeof repo === "string" && repo.trim().length > 0) return Option.some(repo.trim());
56
+ if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
57
+ const url = repo.url.trim();
58
+ return url.length > 0 ? Option.some(url) : Option.none();
59
+ }
28
60
  return Option.none();
29
61
  });
30
62
  const collectCandidates = (workspaceRoot, options) => Effect.gen(function* () {
31
- const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.catch(() => Effect.succeed(new VitestAgentConfig({}))));
63
+ const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.mapError((cause) => new ProjectIdentitySourceError({
64
+ source: "toml",
65
+ path: cause._tag === "ConfigValidationError" ? Option.getOrElse(cause.path, () => workspaceRoot) : cause.path ?? workspaceRoot,
66
+ detail: detailOf(cause),
67
+ cause
68
+ })));
32
69
  const gitRemote = yield* readGitRemote(workspaceRoot);
33
- const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.catch(() => Effect.succeed([])))).find((pkg) => pkg.isRootWorkspace);
70
+ const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.catchTag("WorkspaceRootNotFoundError", () => Effect.succeed([])), Effect.mapError((cause) => new ProjectIdentitySourceError({
71
+ source: "workspace",
72
+ path: cause._tag === "WorkspacePatternError" ? cause.root : cause.path,
73
+ detail: detailOf(cause),
74
+ cause
75
+ })))).find((pkg) => pkg.isRootWorkspace);
34
76
  const packageJsonRepoUrl = root ? yield* readPackageRepoUrl(root.packageJsonPath) : Option.none();
35
77
  const candidates = {};
36
78
  if (options.projectId !== void 0) candidates.explicit = options.projectId;
@@ -69,6 +111,10 @@ const candidatesToTriedList = (candidates) => [
69
111
  * `FileSystem`, `CommandExecutor`, `WorkspaceDiscovery`, and
70
112
  * `VitestAgentConfigFile`. The platform context comes from
71
113
  * `NodeServices.layer` at the entry point.
114
+ *
115
+ * Absent sources fall through (no config file, no workspace root, no
116
+ * `repository` field, no git remote); a present source that cannot be read
117
+ * or decoded fails with `ProjectIdentitySourceError`.
72
118
  * @public
73
119
  */
74
120
  const ProjectIdentityLive = Layer.effect(ProjectIdentity, Effect.gen(function* () {
@@ -84,11 +130,11 @@ const ProjectIdentityLive = Layer.effect(ProjectIdentity, Effect.gen(function* (
84
130
  }));
85
131
  /**
86
132
  * Build a Test layer that returns a fixed `ResolvedIdentity` (or
87
- * fails with `ProjectIdentityNotResolvableError`). Use in unit tests
133
+ * fails with the given `ProjectIdentityError`). Use in unit tests
88
134
  * that depend on `ProjectIdentity` but don't want to wire upstream
89
135
  * services.
90
136
  * @public
91
137
  */
92
- const ProjectIdentityTest = (result) => Layer.succeed(ProjectIdentity, { resolve: () => result instanceof ProjectIdentityNotResolvableError ? Effect.fail(result) : Effect.succeed(result) });
138
+ const ProjectIdentityTest = (result) => Layer.succeed(ProjectIdentity, { resolve: () => result instanceof ProjectIdentityNotResolvableError || result instanceof ProjectIdentitySourceError ? Effect.fail(result) : Effect.succeed(result) });
93
139
 
94
140
  export { ProjectIdentityLive, ProjectIdentityTest };
@@ -2,6 +2,19 @@ import { DataReader } from "../services/DataReader.js";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
4
  // src/lib/format-triage.ts
5
+ const STRAY_OUTPUT_WINDOW = 10;
6
+ const describeStrayOutput = (stray) => {
7
+ const lines = `${stray.total} line${stray.total === 1 ? "" : "s"}`;
8
+ const sample = stray.samples[0];
9
+ return sample === void 0 ? `${lines}, ${stray.bytes} bytes` : `${lines}, ${stray.bytes} bytes; first: \`${sample.text}\``;
10
+ };
11
+ /**
12
+ * The "stray output" line of the Recent Test Runs section, or `undefined`
13
+ * when none of the recent runs wrote past Vitest's console capture (issue
14
+ * 578). Pure, so the wording is testable without a database.
15
+ * @public
16
+ */
17
+ const formatStrayOutputLine = (history) => history.runsWithStrayOutput === 0 ? void 0 : `- Stray output in ${history.runsWithStrayOutput} of the last ${history.runs} run${history.runs === 1 ? "" : "s"}${history.latest === null ? "" : ` (latest: ${describeStrayOutput(history.latest)})`} — tests or child processes writing straight to the terminal`;
5
18
  /**
6
19
  * Generates an orientation triage markdown string for LLM agents.
7
20
  * Summarises recent test runs, active sessions, acceptance metrics,
@@ -16,6 +29,14 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
16
29
  const allProjects = yield* reader.getRunsByProject().pipe(Effect.orElseSucceed(() => []));
17
30
  const projects = options.project ? allProjects.filter((p) => p.project === options.project) : allProjects;
18
31
  const sessions = yield* reader.listSessions({}).pipe(Effect.orElseSucceed(() => []));
32
+ const strayHistory = yield* reader.getStrayOutputHistory({
33
+ ...options.project !== void 0 && { project: options.project },
34
+ limit: STRAY_OUTPUT_WINDOW
35
+ }).pipe(Effect.orElseSucceed(() => ({
36
+ runs: 0,
37
+ runsWithStrayOutput: 0,
38
+ latest: null
39
+ })));
19
40
  const fallbackMetrics = {
20
41
  phaseEvidenceIntegrity: {
21
42
  total: 0,
@@ -59,6 +80,8 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
59
80
  const counts = `${p.passed} passed, ${p.failed} failed`;
60
81
  lines.push(`- **${p.project}** — ${status} (${counts})`);
61
82
  }
83
+ const strayLine = formatStrayOutputLine(strayHistory);
84
+ if (strayLine !== void 0) lines.push(strayLine);
62
85
  lines.push("");
63
86
  } else {
64
87
  lines.push("### Recent Test Runs");
@@ -99,4 +122,4 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
99
122
  return out;
100
123
  });
101
124
 
102
- export { formatTriageEffect };
125
+ export { formatStrayOutputLine, formatTriageEffect };
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql/SqlClient";
3
+
4
+ // src/migrations/0004_stray_output.ts
5
+ /** @internal */
6
+ const migration = Effect.gen(function* () {
7
+ const sql = yield* SqlClient;
8
+ yield* sql`ALTER TABLE test_runs ADD COLUMN stray_total INTEGER`;
9
+ yield* sql`ALTER TABLE test_runs ADD COLUMN stray_stdout INTEGER`;
10
+ yield* sql`ALTER TABLE test_runs ADD COLUMN stray_stderr INTEGER`;
11
+ yield* sql`ALTER TABLE test_runs ADD COLUMN stray_bytes INTEGER`;
12
+ yield* sql`ALTER TABLE test_runs ADD COLUMN stray_samples TEXT`;
13
+ });
14
+
15
+ export { migration as default };
@@ -1,6 +1,7 @@
1
1
  import migration from "./0001_initial.js";
2
2
  import migration$1 from "./0002_test_artifacts.js";
3
3
  import migration$2 from "./0003_run_retention.js";
4
+ import migration$3 from "./0004_stray_output.js";
4
5
 
5
6
  // src/migrations/index.ts
6
7
  /**
@@ -23,7 +24,8 @@ import migration$2 from "./0003_run_retention.js";
23
24
  const PROJECT_MIGRATIONS = {
24
25
  "0001_initial": migration,
25
26
  "0002_test_artifacts": migration$1,
26
- "0003_run_retention": migration$2
27
+ "0003_run_retention": migration$2,
28
+ "0004_stray_output": migration$3
27
29
  };
28
30
 
29
31
  export { PROJECT_MIGRATIONS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/engine",
3
- "version": "1.2.0",
3
+ "version": "2.0.1",
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": [
@@ -42,20 +42,20 @@
42
42
  "dependencies": {
43
43
  "@effect/platform-node": "^4.0.6",
44
44
  "@effect/sql-sqlite-node": "^4.0.6",
45
- "@effected/app": "^0.26.0",
46
- "@effected/cli": "^0.22.0",
47
- "@effected/config-file": "^0.19.1",
45
+ "@effected/app": "^0.27.0",
46
+ "@effected/cli": "^0.22.1",
47
+ "@effected/config-file": "^0.20.0",
48
48
  "@effected/engine": "^0.8.0",
49
49
  "@effected/env": "^0.5.0",
50
50
  "@effected/glob": "^0.14.0",
51
51
  "@effected/jsonc": "^0.20.0",
52
- "@effected/store": "^0.18.0",
52
+ "@effected/store": "^0.19.0",
53
53
  "@effected/toml": "^0.16.0",
54
- "@effected/walker": "^0.19.0",
55
- "@effected/workspaces": "^0.38.0",
56
- "@effected/xdg": "^0.14.1",
57
- "@effected/yaml": "^0.25.0",
58
- "@vitest-agent/sdk": "7.1.0",
54
+ "@effected/walker": "^0.20.0",
55
+ "@effected/workspaces": "^0.38.1",
56
+ "@effected/xdg": "^0.15.0",
57
+ "@effected/yaml": "^0.25.1",
58
+ "@vitest-agent/sdk": "8.0.0",
59
59
  "effect": "^4.0.6"
60
60
  },
61
61
  "engines": {
@@ -1,7 +1,19 @@
1
1
  import { Context } from "effect";
2
2
 
3
3
  // src/services/EnvironmentDetector.ts
4
- /** @public */
4
+ /**
5
+ * Who is running the program and where.
6
+ *
7
+ * @remarks
8
+ * `detect` is the console audience: an agent marker wins over CI, so an
9
+ * agent inside GitHub Actions reads `agent-shell`. `ci` is the CI vendor
10
+ * independently of that precedence, so a caller can keep GitHub-only
11
+ * extras (the step summary, annotations, the `github-actions` reporter)
12
+ * on for the humans reviewing the job even when the console audience is
13
+ * the agent (issue #624).
14
+ *
15
+ * @public
16
+ */
5
17
  var EnvironmentDetector = class extends Context.Service()("vitest-agent/EnvironmentDetector") {};
6
18
 
7
19
  export { EnvironmentDetector };
@@ -54,6 +54,12 @@ const resolveProjectIdentityFromCandidates = (candidates) => {
54
54
  * Service tag. The Live layer (in `layers/ProjectIdentityLive.ts`)
55
55
  * wires up the I/O sources; tests provide a stub that returns a
56
56
  * pre-built `ResolvedIdentity`.
57
+ *
58
+ * `resolve` fails with `ProjectIdentityNotResolvableError` when every
59
+ * source is absent or empty, and with `ProjectIdentitySourceError` when a
60
+ * source is present but unusable (a malformed `vitest-agent.config.toml`,
61
+ * an unreadable root `package.json`): a broken config is reported, never
62
+ * silently replaced by the default identity (issue 321).
57
63
  * @public
58
64
  */
59
65
  var ProjectIdentity = class extends Context.Service()("vitest-agent/ProjectIdentity") {};
package/sql/assemblers.js CHANGED
@@ -11,7 +11,8 @@ import { historyKey } from "../services/HistoryTracker.js";
11
11
  function assembleManifest(rows, dbPath) {
12
12
  return {
13
13
  updatedAt: rows.reduce((latest, row) => {
14
- return row.timestamp > latest ? row.timestamp : latest;
14
+ if (row.timestamp > latest) return row.timestamp;
15
+ return latest;
15
16
  }, (/* @__PURE__ */ new Date(0)).toISOString()),
16
17
  cacheDir: dbPath,
17
18
  projects: rows.map((row) => {
package/testing.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { Agent, Agent as Agent$1, AgentId, AgentNotFoundError, AgentNotFoundError as AgentNotFoundError$1, AgentReport, AgentReport as AgentReport$1, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactKind as ArtifactKind$2, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, ArtifactSuite as ArtifactSuite$2, BehaviorDetail, BehaviorDetail as BehaviorDetail$1, BehaviorNotFoundError, BehaviorNotFoundError as BehaviorNotFoundError$1, BehaviorRow, BehaviorRow as BehaviorRow$1, BehaviorStatus, BehaviorStatus as BehaviorStatus$1, CacheManifest, CacheManifest as CacheManifest$1, ChatId, CitedArtifact, CitedArtifact as CitedArtifact$1, CoverageBaselines, CoverageBaselines as CoverageBaselines$1, CoverageReport, CoverageReport as CoverageReport$1, DataStoreError, DataStoreError as DataStoreError$1, FileCoverageReport, FileCoverageReport as FileCoverageReport$1, GoalDetail, GoalDetail as GoalDetail$1, GoalNotFoundError, GoalNotFoundError as GoalNotFoundError$1, GoalRow, GoalRow as GoalRow$1, GoalStatus, GoalStatus as GoalStatus$1, HistoryRecord, HistoryRecord as HistoryRecord$1, IdempotencyHit, IdempotencyHit as IdempotencyHit$1, IllegalStatusTransitionEntity, IllegalStatusTransitionError, IllegalStatusTransitionError as IllegalStatusTransitionError$1, Phase, Phase as Phase$1, RegistrationConflictError, RegistrationConflictError as RegistrationConflictError$1, ResolvedThresholds, ResolvedThresholds as ResolvedThresholds$1, RunRetentionPolicy, RunRetentionPolicy as RunRetentionPolicy$1, TddTaskAlreadyEndedError, TddTaskAlreadyEndedError as TddTaskAlreadyEndedError$1, TddTaskEndOutcome, TddTaskNotFoundError, TddTaskNotFoundError as TddTaskNotFoundError$1, TrendEntry, TrendEntry as TrendEntry$1, TrendRecord, TrendRecord as TrendRecord$1 } from "@vitest-agent/sdk";
1
+ import { Agent, Agent as Agent$1, AgentId, AgentNotFoundError, AgentNotFoundError as AgentNotFoundError$1, AgentReport, AgentReport as AgentReport$1, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactKind as ArtifactKind$2, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, ArtifactSuite as ArtifactSuite$2, BehaviorDetail, BehaviorDetail as BehaviorDetail$1, BehaviorNotFoundError, BehaviorNotFoundError as BehaviorNotFoundError$1, BehaviorRow, BehaviorRow as BehaviorRow$1, BehaviorStatus, BehaviorStatus as BehaviorStatus$1, CacheManifest, CacheManifest as CacheManifest$1, ChatId, CitedArtifact, CitedArtifact as CitedArtifact$1, CoverageBaselines, CoverageBaselines as CoverageBaselines$1, CoverageReport, CoverageReport as CoverageReport$1, DataStoreError, DataStoreError as DataStoreError$1, FileCoverageReport, FileCoverageReport as FileCoverageReport$1, GoalDetail, GoalDetail as GoalDetail$1, GoalNotFoundError, GoalNotFoundError as GoalNotFoundError$1, GoalRow, GoalRow as GoalRow$1, GoalStatus, GoalStatus as GoalStatus$1, HistoryRecord, HistoryRecord as HistoryRecord$1, IdempotencyHit, IdempotencyHit as IdempotencyHit$1, IllegalStatusTransitionEntity, IllegalStatusTransitionError, IllegalStatusTransitionError as IllegalStatusTransitionError$1, Phase, Phase as Phase$1, RegistrationConflictError, RegistrationConflictError as RegistrationConflictError$1, ResolvedThresholds, ResolvedThresholds as ResolvedThresholds$1, RunRetentionPolicy, RunRetentionPolicy as RunRetentionPolicy$1, StrayOutput, TddTaskAlreadyEndedError, TddTaskAlreadyEndedError as TddTaskAlreadyEndedError$1, TddTaskEndOutcome, TddTaskNotFoundError, TddTaskNotFoundError as TddTaskNotFoundError$1, TrendEntry, TrendEntry as TrendEntry$1, TrendRecord, TrendRecord as TrendRecord$1 } from "@vitest-agent/sdk";
2
2
  import { Context, Effect, Layer, Option } from "effect";
3
3
  import "@effect/platform-node/NodeServices";
4
4
  import "@effect/sql-sqlite-node/SqliteClient";
5
5
  import { StoreError, StoreMigrationError } from "@effected/store";
6
6
  import "effect/sql/SqlClient";
7
7
  import { SqlError } from "effect/sql/SqlError";
8
+ import "@effected/env";
8
9
  /** @public */
9
10
  interface CreateGoalInput {
10
11
  readonly tddTaskId: number;
@@ -99,6 +100,13 @@ interface TestRunInput {
99
100
  readonly hostSource?: string | null;
100
101
  readonly hostValue?: string | null;
101
102
  readonly hostMetadata?: Record<string, unknown> | null;
103
+ /**
104
+ * Output the run wrote straight to the terminal past Vitest's console
105
+ * capture (issue 578). A run-level signal: every project row of one
106
+ * invocation carries the same value. Omitted or `null` when the run
107
+ * recorded nothing, which the row stores as NULL in every `stray_*` column.
108
+ */
109
+ readonly strayOutput?: StrayOutput | null;
102
110
  }
103
111
  /** @public */
104
112
  interface ModuleInput {
@@ -566,6 +574,32 @@ interface ProjectRunSummary {
566
574
  readonly passed: number;
567
575
  readonly failed: number;
568
576
  readonly skipped: number;
577
+ /** The latest run's stray output (issue 578); `null` when it recorded none. */
578
+ readonly strayOutput: StrayOutput | null;
579
+ }
580
+ /**
581
+ * Options for `DataReader.getStrayOutputHistory`.
582
+ * @public
583
+ */
584
+ interface StrayOutputHistoryOptions {
585
+ /** Only runs of this project; every project otherwise. */
586
+ readonly project?: string;
587
+ /** How many of the most recent runs to consider. Default 10. */
588
+ readonly limit?: number;
589
+ }
590
+ /**
591
+ * How often recent runs wrote stray output (issue 578). A "run" is one
592
+ * reporter invocation: stray output is a run-level signal that every project
593
+ * row of an invocation repeats, so rows are counted once per `invocation_id`.
594
+ * @public
595
+ */
596
+ interface StrayOutputHistory {
597
+ /** Runs considered: the most recent ones, up to the requested limit. */
598
+ readonly runs: number;
599
+ /** How many of them recorded any stray output. */
600
+ readonly runsWithStrayOutput: number;
601
+ /** The most recent run's stray output, when that run recorded any. */
602
+ readonly latest: StrayOutput | null;
569
603
  }
570
604
  /** @public */
571
605
  interface FlakyTest {
@@ -924,6 +958,8 @@ interface TestArtifactRow {
924
958
  declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/DataReader", {
925
959
  readonly getLatestRun: (project: string) => Effect.Effect<Option.Option<AgentReport$1>, DataStoreError$1>;
926
960
  readonly getRunsByProject: () => Effect.Effect<ReadonlyArray<ProjectRunSummary>, DataStoreError$1>;
961
+ /** How often the most recent runs wrote stray output; see {@link StrayOutputHistory}. */
962
+ readonly getStrayOutputHistory: (options?: StrayOutputHistoryOptions) => Effect.Effect<StrayOutputHistory, DataStoreError$1>;
927
963
  readonly getHistory: (project: string, options?: HistoryQueryOptions) => Effect.Effect<HistoryRecord$1, DataStoreError$1>;
928
964
  readonly getBaselines: (project: string) => Effect.Effect<Option.Option<CoverageBaselines$1>, DataStoreError$1>;
929
965
  readonly getTrends: (project: string, limit?: number) => Effect.Effect<Option.Option<TrendRecord$1>, DataStoreError$1>;
@@ -1114,6 +1150,7 @@ export declare const PROJECT_MIGRATIONS: {
1114
1150
  readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1115
1151
  readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1116
1152
  readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1153
+ readonly "0004_stray_output": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
1117
1154
  };
1118
1155
  /** @public */
1119
1156
  export declare function makeTestLayer(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, PlatformLiveError, never>;
@@ -1129,5 +1166,5 @@ export declare function withFailures(filename: string): Layer.Layer<DataReader |
1129
1166
  export declare function flaky(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, import("@vitest-agent/sdk").DataStoreError | PlatformLiveError, never>;
1130
1167
  /** @public */
1131
1168
  export declare function withTddTask(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, import("@vitest-agent/sdk").BehaviorNotFoundError | import("@vitest-agent/sdk").DataStoreError | import("@vitest-agent/sdk").GoalNotFoundError | import("@vitest-agent/sdk").IllegalStatusTransitionError | import("@vitest-agent/sdk").TddTaskAlreadyEndedError | import("@vitest-agent/sdk").TddTaskNotFoundError | PlatformLiveError, never>;
1132
- export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type ArtifactSuite, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataStoreError, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistedAttachment, type PersistentFailure, type Phase, type PlatformLiveError, type ProjectRunSummary, type PruneRunsResult, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, RunRetentionPolicy, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestAnnotationInput, type TestAnnotationRow, type TestArtifactInput, type TestArtifactQueryOptions, type TestArtifactRow, type TestAttachmentInput, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput };
1169
+ export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type ArtifactSuite, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataStoreError, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistedAttachment, type PersistentFailure, type Phase, type PlatformLiveError, type ProjectRunSummary, type PruneRunsResult, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, RunRetentionPolicy, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type StrayOutputHistory, type StrayOutputHistoryOptions, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestAnnotationInput, type TestAnnotationRow, type TestArtifactInput, type TestArtifactQueryOptions, type TestArtifactRow, type TestAttachmentInput, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput };
1133
1170
  //# sourceMappingURL=testing.d.ts.map
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.59.1"
8
+ "packageVersion": "7.59.4"
9
9
  }
10
10
  ]
11
11
  }
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
2
  /** Build-time injected package version (see architecture.md D36). @public */
3
- const CURRENT_ENGINE_VERSION = "1.2.0";
3
+ const CURRENT_ENGINE_VERSION = "2.0.1";
4
4
 
5
5
  export { CURRENT_ENGINE_VERSION };