@vitest-agent/sdk 2.4.13 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/testing.d.ts CHANGED
@@ -132,6 +132,7 @@ declare const AgentReport: Schema.Struct<{
132
132
  }>>;
133
133
  readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
134
134
  readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
135
+ readonly totalFiles: Schema.optional<Schema.Number>;
135
136
  readonly lowCoverage: Schema.$Array<Schema.Struct<{
136
137
  readonly file: Schema.String;
137
138
  readonly summary: Schema.Struct<{
@@ -170,6 +171,10 @@ declare const AgentReport: Schema.Struct<{
170
171
  readonly sample: Schema.optional<Schema.String>;
171
172
  }>>;
172
173
  readonly truncated: Schema.optional<Schema.Boolean>;
174
+ readonly fromFailingTests: Schema.optional<Schema.Struct<{
175
+ readonly total: Schema.Number;
176
+ readonly files: Schema.Number;
177
+ }>>;
173
178
  }>>;
174
179
  }>;
175
180
  /** @public */
@@ -289,6 +294,7 @@ declare const CoverageReport: Schema.Struct<{
289
294
  }>>;
290
295
  readonly scoped: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
291
296
  readonly scopedFiles: Schema.optional<Schema.$Array<Schema.String>>;
297
+ readonly totalFiles: Schema.optional<Schema.Number>;
292
298
  readonly lowCoverage: Schema.$Array<Schema.Struct<{
293
299
  readonly file: Schema.String;
294
300
  readonly summary: Schema.Struct<{
@@ -469,9 +475,28 @@ type TrendRecord = typeof TrendRecord.Type;
469
475
  type Phase = "spike" | "red" | "red.triangulate" | "green" | "green.fake-it" | "refactor" | "extended-red" | "green-without-red";
470
476
  /** @public */
471
477
  type ArtifactKind = "test_written" | "test_failed_run" | "code_written" | "test_passed_run" | "refactor" | "test_weakened";
478
+ /**
479
+ * Explicit test-runner marker on a `tdd_artifacts` row (issue #363). `vitest`
480
+ * artifacts always carry a `test_case_id` when they anchor a specific test;
481
+ * `bats` artifacts never do (there is no `test_cases` row for a bats test),
482
+ * so the D2 binding-rule validator branches on this to accept a bats
483
+ * run-level artifact without weakening the vitest guard.
484
+ * @public
485
+ */
486
+ type ArtifactSuite = "vitest" | "bats";
472
487
  /** @public */
473
488
  interface CitedArtifact {
474
489
  readonly id: number;
490
+ /**
491
+ * `tdd_phases.id` of the phase this artifact was actually recorded in — the
492
+ * artifact's own phase binding. This is the honest anchor for D2 binding
493
+ * rule 1's window check (issue #245): a test_case's first-ever creation
494
+ * turn can predate the current phase (e.g. authored during spike, then
495
+ * re-run inside red) without the evidence itself being stale, so the
496
+ * window check compares this against `PhaseTransitionContext.current_phase_id`
497
+ * rather than `test_case_created_turn_at`.
498
+ */
499
+ readonly phase_id: number;
475
500
  readonly artifact_kind: ArtifactKind;
476
501
  readonly test_case_id: number | null;
477
502
  readonly test_case_created_turn_at: string | null;
@@ -479,6 +504,12 @@ interface CitedArtifact {
479
504
  readonly test_run_id: number | null;
480
505
  readonly test_first_failure_run_id: number | null;
481
506
  readonly behavior_id: number | null;
507
+ /**
508
+ * Issue #363: explicit suite marker distinguishing vitest from bats runs.
509
+ * Optional on the pure type so hand-built contexts default to `vitest`;
510
+ * the DB row always carries the persisted value.
511
+ */
512
+ readonly suite?: ArtifactSuite;
482
513
  }
483
514
  //#endregion
484
515
  //#region src/schemas/Identity.d.ts
@@ -655,6 +686,29 @@ declare class IdempotencyHit extends IdempotencyHit_base<{
655
686
  readonly existingAgentId: typeof AgentId.Type;
656
687
  }> {}
657
688
  //#endregion
689
+ //#region src/schemas/Thresholds.d.ts
690
+ /**
691
+ * Fully resolved thresholds ready for evaluation.
692
+ * @public
693
+ */
694
+ declare const ResolvedThresholds: Schema.Struct<{
695
+ readonly global: Schema.Struct<{
696
+ readonly lines: Schema.optional<Schema.Number>;
697
+ readonly functions: Schema.optional<Schema.Number>;
698
+ readonly branches: Schema.optional<Schema.Number>;
699
+ readonly statements: Schema.optional<Schema.Number>;
700
+ }>;
701
+ readonly perFile: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
702
+ readonly patterns: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Tuple<readonly [Schema.String, Schema.Struct<{
703
+ readonly lines: Schema.optional<Schema.Number>;
704
+ readonly functions: Schema.optional<Schema.Number>;
705
+ readonly branches: Schema.optional<Schema.Number>;
706
+ readonly statements: Schema.optional<Schema.Number>;
707
+ }>]>>, never>;
708
+ }>;
709
+ /** @public */
710
+ type ResolvedThresholds = typeof ResolvedThresholds.Type;
711
+ //#endregion
658
712
  //#region src/services/DataStore.d.ts
659
713
  /** @public */
660
714
  interface CreateGoalInput {
@@ -879,6 +933,13 @@ interface SessionInput {
879
933
  readonly parentSessionId?: number;
880
934
  readonly triageWasNonEmpty?: boolean;
881
935
  readonly startedAt: string;
936
+ /**
937
+ * Canonical conversation UUID this session belongs to (issue #144).
938
+ * Set once at INSERT time — `sessions.conversation_id` is immutable
939
+ * thereafter (see the `trg_sessions_conv_id_immutable` trigger), so
940
+ * omit it here rather than trying to backfill via an update.
941
+ */
942
+ readonly conversationId?: string;
882
943
  }
883
944
  /** @public */
884
945
  interface TurnInput {
@@ -956,6 +1017,8 @@ interface WriteTddArtifactInput {
956
1017
  readonly testFirstFailureRunId?: number;
957
1018
  readonly diffExcerpt?: string;
958
1019
  readonly recordedAt: string;
1020
+ /** Issue #363: explicit suite marker. Defaults to `"vitest"` when omitted. */
1021
+ readonly suite?: ArtifactSuite;
959
1022
  }
960
1023
  /** @public */
961
1024
  interface WriteCommitInput {
@@ -1030,6 +1093,18 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
1030
1093
  readonly writeCoverage: (runId: number, coverage: ReadonlyArray<FileCoverageInput>) => Effect.Effect<void, DataStoreError>;
1031
1094
  readonly writeHistory: (project: string, fullName: string, modulePath: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
1032
1095
  readonly writeBaselines: (baselines: CoverageBaselines) => Effect.Effect<void, DataStoreError>;
1096
+ /**
1097
+ * Persist the resolved (enforced) Vitest `coverage.thresholds` for the
1098
+ * current run, distinct from the ratcheted `coverage_baselines` rows.
1099
+ * Stored under `kind='threshold'`. See issue #237.
1100
+ */
1101
+ readonly writeThresholds: (thresholds: ResolvedThresholds) => Effect.Effect<void, DataStoreError>;
1102
+ /**
1103
+ * Persist the resolved aspirational `coverageTargets` for the current
1104
+ * run, distinct from the ratcheted `coverage_baselines` rows. Stored
1105
+ * under `kind='target'`. See issue #237.
1106
+ */
1107
+ readonly writeTargets: (targets: ResolvedThresholds) => Effect.Effect<void, DataStoreError>;
1033
1108
  readonly writeTrends: (project: string, runId: number, entry: TrendEntry) => Effect.Effect<void, DataStoreError>;
1034
1109
  readonly writeSourceMap: (sourceFilePath: string, testModuleId: number, mappingType: string) => Effect.Effect<void, DataStoreError>;
1035
1110
  readonly ensureFile: (filePath: string) => Effect.Effect<number, DataStoreError>;
@@ -1048,6 +1123,20 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
1048
1123
  * `INSERT ... ON CONFLICT DO NOTHING`.
1049
1124
  */
1050
1125
  readonly upsertSession: (input: SessionInput) => Effect.Effect<number, DataStoreError>;
1126
+ /**
1127
+ * Backfill `sessions.conversation_id` for a row that was inserted
1128
+ * before the canonical conversation id was known (issue #144) —
1129
+ * `record session-start` always precedes `register-agent`, so the
1130
+ * session row exists first. Race-safe and idempotent: the UPDATE's
1131
+ * `WHERE conversation_id IS NULL` guard means a session that
1132
+ * already carries a value is left untouched (zero rows affected),
1133
+ * matching the trigger's null→value-only relaxation rather than
1134
+ * risking an immutability-violation error on a redundant call.
1135
+ */
1136
+ readonly setSessionConversationIdIfNull: (input: {
1137
+ readonly sessionId: number;
1138
+ readonly conversationId: string;
1139
+ }) => Effect.Effect<void, DataStoreError>;
1051
1140
  readonly writeTurn: (input: TurnInput) => Effect.Effect<number, DataStoreError>;
1052
1141
  readonly writeFailureSignature: (input: FailureSignatureWriteInput) => Effect.Effect<void, DataStoreError>;
1053
1142
  readonly endSession: (chatId: string, endedAt: string, endReason: string | null) => Effect.Effect<void, DataStoreError>;
@@ -1225,6 +1314,8 @@ interface SessionDetail {
1225
1314
  readonly startedAt: string;
1226
1315
  readonly endedAt: string | null;
1227
1316
  readonly endReason: string | null;
1317
+ /** Canonical conversation UUID this session belongs to (issue #144), null when unset. */
1318
+ readonly conversationId: string | null;
1228
1319
  }
1229
1320
  /** @public */
1230
1321
  interface TurnSummary {
@@ -1406,6 +1497,7 @@ interface TddArtifactRow {
1406
1497
  readonly testRunId: number | null;
1407
1498
  readonly testFirstFailureRunId: number | null;
1408
1499
  readonly recordedAt: string;
1500
+ readonly suite: ArtifactSuite;
1409
1501
  }
1410
1502
  /** @public */
1411
1503
  interface TagInventoryRow {
@@ -1519,7 +1611,32 @@ declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/Da
1519
1611
  * preserves the prior single-session contract.
1520
1612
  */
1521
1613
  readonly walkParents?: boolean;
1614
+ /**
1615
+ * When true AND `sessionId`'s session has a non-null
1616
+ * `conversation_id`, also return tdd_tasks belonging to
1617
+ * any OTHER session sharing that `conversation_id` — the
1618
+ * detached-session fallback (issue #144). A session with
1619
+ * a null `conversation_id` never triggers this fallback.
1620
+ * Rows are ordered so a task owned by an `agent_kind =
1621
+ * 'main'` session sorts first, then by `started_at DESC`.
1622
+ * Default `false` preserves the prior contract.
1623
+ */
1624
+ readonly walkConversation?: boolean;
1522
1625
  }) => Effect.Effect<ReadonlyArray<TddTaskSummary>, DataStoreError>;
1626
+ /**
1627
+ * Diagnostic for the `missing_artifact_evidence` phase-transition
1628
+ * denial (issue #144): counts `tdd_artifacts` rows recorded at or
1629
+ * after `sinceIso` under sessions OTHER than the one that opened
1630
+ * `tddTaskId`, but sharing that session's `conversation_id`. Used
1631
+ * to tell an agent "your hooks may be attributing to a detached
1632
+ * session" rather than a bare "no artifact found". Returns 0 when
1633
+ * `tddTaskId` is unknown or its session's `conversation_id` is
1634
+ * null — the diagnostic never fabricates a signal it cannot back.
1635
+ */
1636
+ readonly countRecentArtifactsInOtherSessionsOfConversation: (input: {
1637
+ readonly tddTaskId: number;
1638
+ readonly sinceIso: string;
1639
+ }) => Effect.Effect<number, DataStoreError>;
1523
1640
  /**
1524
1641
  * List artifacts recorded for a TDD task, optionally filtered
1525
1642
  * by `artifactKind`, `phaseId`, or `behaviorId`. Returns rows in
@@ -1578,5 +1695,5 @@ declare function flaky(filename: string): Layer.Layer<DataReader | DataStore | i
1578
1695
  /** @public */
1579
1696
  declare function withTddTask(filename: string): Layer.Layer<DataReader | DataStore | import("effect/unstable/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, BehaviorNotFoundError | DataStoreError | GoalNotFoundError | IllegalStatusTransitionError | import("@effect/sql-sqlite-node/SqliteMigrator").MigrationError | import("effect/unstable/sql/SqlError").SqlError | TddTaskAlreadyEndedError | TddTaskNotFoundError, never>;
1580
1697
  //#endregion
1581
- export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, 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, DataReader, DataStore, DataStoreError, DataStoreTestLayer, 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 PersistentFailure, type Phase, type ProjectRunSummary, type RegisterAgentInput, RegistrationConflictError, type RunChangedFile, type RunInvocationMethod, 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 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, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
1698
+ 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, DataReader, DataStore, DataStoreError, DataStoreTestLayer, 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 PersistentFailure, type Phase, type ProjectRunSummary, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, 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 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, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
1582
1699
  //# sourceMappingURL=testing.d.ts.map
@@ -10,14 +10,28 @@ function truncateSample(content) {
10
10
  * Aggregate raw console-leak entries into a ConsoleLeaks signal:
11
11
  * bucket by file, split stdout/stderr, collect attributable test names,
12
12
  * capture one truncated sample per file, sort by total writes descending,
13
- * and cap the file list. Returns `undefined` when there are no entries so a
14
- * clean run attaches nothing.
13
+ * and cap the file list. Entries logged inside a failing test (with
14
+ * `failed` set) are excluded from `total`/`byFile` — the actionable leak signal —
15
+ * and summarized instead in `fromFailingTests` (issue #263: a logger-backed
16
+ * assertion failure otherwise makes every red run look like a leak). Returns
17
+ * `undefined` only when there is no output at all, non-failing or failing.
15
18
  * @public
16
19
  */
17
20
  function buildConsoleLeaks(entries) {
18
21
  if (entries.length === 0) return void 0;
22
+ const nonFailing = entries.filter((e) => e.failed !== true);
23
+ const failing = entries.filter((e) => e.failed === true);
24
+ const fromFailingTests = failing.length > 0 ? {
25
+ total: failing.length,
26
+ files: new Set(failing.map((e) => e.file)).size
27
+ } : void 0;
28
+ if (nonFailing.length === 0) return {
29
+ total: 0,
30
+ byFile: [],
31
+ ...fromFailingTests !== void 0 ? { fromFailingTests } : {}
32
+ };
19
33
  const byFile = /* @__PURE__ */ new Map();
20
- for (const e of entries) {
34
+ for (const e of nonFailing) {
21
35
  let acc = byFile.get(e.file);
22
36
  if (acc === void 0) {
23
37
  acc = {
@@ -46,30 +60,43 @@ function buildConsoleLeaks(entries) {
46
60
  files.sort((a, b) => b.stdout + b.stderr - (a.stdout + a.stderr));
47
61
  const truncated = files.length > MAX_FILES;
48
62
  return {
49
- total: entries.length,
63
+ total: nonFailing.length,
50
64
  byFile: truncated ? files.slice(0, MAX_FILES) : files,
51
- ...truncated ? { truncated: true } : {}
65
+ ...truncated ? { truncated: true } : {},
66
+ ...fromFailingTests !== void 0 ? { fromFailingTests } : {}
52
67
  };
53
68
  }
54
69
  /**
55
70
  * Walk a Vitest `File[]` task tree (from `vitest.state.getFiles()`) into flat
56
71
  * {@link ConsoleLeakEntry} values. Each task `log` becomes one entry attributed
57
72
  * to its enclosing file and, when the log sits on a test task, that test's name.
73
+ * An entry is marked `failed: true` when it was logged inside a test whose own
74
+ * `result.state` is `"fail"`, or — for output with no owning test — when the
75
+ * enclosing file itself failed (e.g. a collection/load error).
58
76
  * @public
59
77
  */
60
78
  function collectConsoleLeakEntries(files) {
61
79
  const entries = [];
62
- const visit = (task, file, test) => {
63
- const currentTest = task.type === "test" ? task.fullTestName ?? task.name ?? test : test;
64
- for (const log of task.logs ?? []) entries.push({
65
- file,
66
- ...currentTest !== void 0 ? { test: currentTest } : {},
67
- type: log.type,
68
- content: log.content
69
- });
70
- for (const child of task.tasks ?? []) visit(child, file, currentTest);
80
+ const visit = (task, file, test, testFailed, fileFailed) => {
81
+ const isTest = task.type === "test";
82
+ const currentTest = isTest ? task.fullTestName ?? task.name ?? test : test;
83
+ const currentTestFailed = isTest ? task.result?.state === "fail" : testFailed;
84
+ for (const log of task.logs ?? []) {
85
+ const failed = currentTest !== void 0 ? currentTestFailed : fileFailed;
86
+ entries.push({
87
+ file,
88
+ ...currentTest !== void 0 ? { test: currentTest } : {},
89
+ type: log.type,
90
+ content: log.content,
91
+ ...failed ? { failed: true } : {}
92
+ });
93
+ }
94
+ for (const child of task.tasks ?? []) visit(child, file, currentTest, currentTestFailed, fileFailed);
71
95
  };
72
- for (const file of files) visit(file, file.name ?? "(unknown)", void 0);
96
+ for (const file of files) {
97
+ const fileFailed = file.result?.state === "fail";
98
+ visit(file, file.name ?? "(unknown)", void 0, false, fileFailed);
99
+ }
73
100
  return entries;
74
101
  }
75
102
 
@@ -0,0 +1,38 @@
1
+ //#region src/utils/detect-non-default-discover-strategy.ts
2
+ /**
3
+ * Cheaply strips `//` line comments and `/* *\/` block comments from `source`.
4
+ *
5
+ * Not a full lexer — a marker string that happens to sit inside a string
6
+ * literal containing comment-like syntax could slip through uncaught. That
7
+ * false positive is acceptable: {@link detectNonDefaultDiscoverStrategy} is
8
+ * only ever used to decide whether to fail open, and failing open is the
9
+ * safe direction.
10
+ */
11
+ function stripComments(source) {
12
+ return source.replace(/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, "").replace(/\/\/.*$/gm, "");
13
+ }
14
+ const DISCOVER_STRATEGY_OPTION_RE = /\bdiscoverStrategy\s*:/;
15
+ const ADD_PROJECT_MARKER = ".addProject(";
16
+ const EXTENDS_DEFAULT_STRATEGY_RE = /\bextends\s+DefaultDiscoverStrategy\b/;
17
+ const IMPLEMENTS_STRATEGY_RE = /\bimplements\s+DiscoverStrategy\b/;
18
+ /**
19
+ * Lexically detects whether a Vitest/Vite config source text appears to
20
+ * configure a non-default `DiscoverStrategy` — a custom `discoverStrategy`
21
+ * option (including `discoverStrategy: false`), an `AgentPlugin.discover()`
22
+ * `.addProject(...)` chain, or a class extending `DefaultDiscoverStrategy` /
23
+ * implementing `DiscoverStrategy`.
24
+ *
25
+ * Pure and comment-tolerant (best-effort): a marker mentioned only inside a
26
+ * comment may still register as a false positive if it isn't caught by the
27
+ * comment-stripping pass. That is intentional — this function only ever
28
+ * decides whether to fail open, so a false positive is harmless while a
29
+ * false negative would produce a confidently wrong deny.
30
+ * @public
31
+ */
32
+ function detectNonDefaultDiscoverStrategy(source) {
33
+ const stripped = stripComments(source);
34
+ return DISCOVER_STRATEGY_OPTION_RE.test(stripped) || stripped.includes(ADD_PROJECT_MARKER) || EXTENDS_DEFAULT_STRATEGY_RE.test(stripped) || IMPLEMENTS_STRATEGY_RE.test(stripped);
35
+ }
36
+
37
+ //#endregion
38
+ export { detectNonDefaultDiscoverStrategy };
@@ -1,4 +1,5 @@
1
1
  import { ansi } from "./ansi.js";
2
+ import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
2
3
 
3
4
  //#region src/utils/format-console.ts
4
5
  /**
@@ -161,6 +162,10 @@ function formatConsoleMarkdown(report, options) {
161
162
  if (report.coverage) {
162
163
  const cov = report.coverage;
163
164
  const globalThresholds = cov.thresholds.global;
165
+ if (cov.scoped) {
166
+ lines.push(formatScopedCoverageNote(cov.scopedFiles?.length ?? 0, cov.totalFiles));
167
+ lines.push("");
168
+ }
164
169
  const filesToShow = cov.lowCoverage.slice(0, coverageConsoleLimit);
165
170
  if (filesToShow.length > 0) {
166
171
  lines.push(`### Coverage gaps`);
@@ -0,0 +1,25 @@
1
+ //#region src/utils/format-scoped-coverage-note.ts
2
+ /**
3
+ * Format the informational note shown when coverage thresholds were
4
+ * skipped because a run only exercised a subset of the project's test
5
+ * files (issue #160). Vitest's coverage provider enforces
6
+ * `coverage.thresholds` against the whole-project denominator
7
+ * regardless of how many files actually ran, so callers that detect a
8
+ * scoped/partial run suppress the native threshold check and surface
9
+ * this note instead so the agent understands why no pass/fail verdict
10
+ * was rendered for coverage.
11
+ *
12
+ * @param testedFileCount - number of test files that actually ran
13
+ * @param totalFileCount - total number of test files in the project, when
14
+ * known. Some callers (formatters working from a persisted
15
+ * `CoverageReport`, which tracks only the tested-file list) cannot supply
16
+ * this; the note degrades to `"(N test files)"` when omitted.
17
+ * @returns the note text, e.g. `"Coverage thresholds skipped: partial run (2 of 47 test files)"`
18
+ * @public
19
+ */
20
+ function formatScopedCoverageNote(testedFileCount, totalFileCount) {
21
+ return `Coverage thresholds skipped: partial run (${totalFileCount !== void 0 ? `${testedFileCount} of ${totalFileCount}` : `${testedFileCount}`} test files)`;
22
+ }
23
+
24
+ //#endregion
25
+ export { formatScopedCoverageNote };
@@ -1,4 +1,5 @@
1
1
  import { ansi } from "./ansi.js";
2
+ import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
2
3
  import { relativePath } from "./format-console.js";
3
4
  import { compressLines } from "./compress-lines.js";
4
5
 
@@ -257,6 +258,9 @@ const aggregateCoverage = (reports) => {
257
258
  let hasCoverage = false;
258
259
  let thresholdsGlobal;
259
260
  let targetsGlobal;
261
+ let scoped = false;
262
+ let scopedFileCount = 0;
263
+ let totalFiles;
260
264
  const belowTargetByFile = /* @__PURE__ */ new Map();
261
265
  const belowThresholdByFile = /* @__PURE__ */ new Map();
262
266
  for (const r of reports) {
@@ -265,6 +269,11 @@ const aggregateCoverage = (reports) => {
265
269
  hasCoverage = true;
266
270
  thresholdsGlobal ??= cov.thresholds.global;
267
271
  targetsGlobal ??= cov.targets?.global;
272
+ if (cov.scoped) {
273
+ scoped = true;
274
+ scopedFileCount = Math.max(scopedFileCount, cov.scopedFiles?.length ?? 0);
275
+ if (cov.totalFiles !== void 0) totalFiles = cov.totalFiles;
276
+ }
268
277
  if (cov.lowCoverage) {
269
278
  for (const f of cov.lowCoverage) if (!belowThresholdByFile.has(f.file)) belowThresholdByFile.set(f.file, f);
270
279
  }
@@ -279,8 +288,11 @@ const aggregateCoverage = (reports) => {
279
288
  thresholdsMet: belowThresholdCount === 0,
280
289
  belowThresholdCount,
281
290
  belowTargetFiles,
291
+ scoped,
292
+ scopedFileCount,
282
293
  ...thresholdsGlobal !== void 0 ? { thresholdsGlobal } : {},
283
- ...targetsGlobal !== void 0 ? { targetsGlobal } : {}
294
+ ...targetsGlobal !== void 0 ? { targetsGlobal } : {},
295
+ ...totalFiles !== void 0 ? { totalFiles } : {}
284
296
  };
285
297
  };
286
298
  /**
@@ -294,7 +306,8 @@ const renderCoverageSection = (reports, options, ao) => {
294
306
  const lines = [];
295
307
  const thresholdSpec = agg.thresholdsGlobal ? formatTargetSpec(agg.thresholdsGlobal) : "";
296
308
  const targetSpec = agg.targetsGlobal ? formatTargetSpec(agg.targetsGlobal) : "";
297
- if (!agg.thresholdsMet) {
309
+ if (agg.scoped) lines.push(formatScopedCoverageNote(agg.scopedFileCount, agg.totalFiles));
310
+ else if (!agg.thresholdsMet) {
298
311
  const cross = ansi("✗", "red", ao);
299
312
  const fileWord = agg.belowThresholdCount === 1 ? "file" : "files";
300
313
  lines.push(`Coverage: ${cross} ${agg.belowThresholdCount} ${fileWord} below minimum thresholds${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
@@ -55,6 +55,19 @@ const validatePhaseTransition = (ctx) => {
55
55
  humanHint: `Cannot transition from '${ctx.current_phase}' directly to 'green'. The red phase must be entered explicitly first (${ctx.current_phase}→red), then a failing test written and run, then red→green requested with a test_failed_run artifact.`
56
56
  }
57
57
  };
58
+ if (ctx.requested_phase === "refactor" && ctx.current_phase !== "green" && ctx.current_phase !== "green.fake-it") {
59
+ const canEnterGreen = ctx.current_phase === "red" || ctx.current_phase === "red.triangulate";
60
+ return {
61
+ accepted: false,
62
+ phase: ctx.current_phase,
63
+ denialReason: "refactor_without_passing_run",
64
+ remediation: {
65
+ suggestedTool: "tdd_phase_transition_request",
66
+ suggestedArgs: { requestedPhase: canEnterGreen ? "green" : "red" },
67
+ humanHint: canEnterGreen ? `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. The green phase must be entered first (${ctx.current_phase}→green with a test_failed_run artifact), then green→refactor requested with a test_passed_run artifact.` : `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. Enter red first (${ctx.current_phase}→red), write and run a failing test, request red→green with a test_failed_run artifact, then green→refactor with a test_passed_run artifact.`
68
+ }
69
+ };
70
+ }
58
71
  const isTriangulateGreen = ctx.current_phase === "red.triangulate" && ctx.requested_phase === "green";
59
72
  const expected = requiredArtifactForTransition(ctx.current_phase, ctx.requested_phase);
60
73
  if (expected === null) return {
@@ -71,28 +84,29 @@ const validatePhaseTransition = (ctx) => {
71
84
  humanHint: expected.humanHint
72
85
  }
73
86
  };
74
- if (ctx.cited_artifact.test_case_id === null) return {
87
+ const isBatsRunLevel = ctx.cited_artifact.test_case_id === null && (ctx.cited_artifact.suite ?? "vitest") === "bats";
88
+ if (ctx.cited_artifact.test_case_id === null && !isBatsRunLevel) return {
75
89
  accepted: false,
76
90
  phase: ctx.current_phase,
77
91
  denialReason: "missing_artifact_evidence",
78
92
  remediation: {
79
93
  suggestedTool: "run_tests",
80
94
  suggestedArgs: {},
81
- humanHint: "The cited artifact has no specific test (test_case_id is null), so it cannot be bound to this phase. Run a specific failing test via run_tests so the resulting artifact carries a test_case_id, then cite that artifact."
95
+ humanHint: "The cited artifact is run-level (test_case_id is null) and its suite is 'vitest', so it cannot be bound to this phase — only bats-suite run-level artifacts are accepted without a specific test. Run a specific failing test via run_tests so the resulting artifact carries a test_case_id, then cite that artifact."
82
96
  }
83
97
  };
84
- if (expected.kind === "test_failed_run") {
85
- if (!isTriangulateGreen && ctx.cited_artifact.test_case_created_turn_at !== null && ctx.cited_artifact.test_case_created_turn_at < ctx.phase_started_at) return {
98
+ if (expected.kind === "test_failed_run" || isBatsRunLevel && expected.kind === "test_passed_run") {
99
+ if (!isTriangulateGreen && ctx.current_phase_id !== null && ctx.cited_artifact.phase_id !== ctx.current_phase_id) return {
86
100
  accepted: false,
87
101
  phase: ctx.current_phase,
88
102
  denialReason: "evidence_not_in_phase_window",
89
103
  remediation: {
90
104
  suggestedTool: "run_tests",
91
105
  suggestedArgs: {},
92
- humanHint: "The cited test was authored before this phase started. Write a new failing test inside the current phase."
106
+ humanHint: "The cited artifact was recorded in a different phase than the one currently open. Run the failing test again inside the current phase so a fresh test_failed_run artifact is bound to it."
93
107
  }
94
108
  };
95
- if (!ctx.cited_artifact.test_case_authored_in_session) return {
109
+ if (!isBatsRunLevel && !ctx.cited_artifact.test_case_authored_in_session) return {
96
110
  accepted: false,
97
111
  phase: ctx.current_phase,
98
112
  denialReason: "evidence_not_in_phase_window",