@vitest-agent/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/bin/vitest-agent-mcp.js +93 -0
  4. package/context.js +72 -0
  5. package/index.d.ts +1577 -0
  6. package/index.js +19 -0
  7. package/layers/McpLive.js +30 -0
  8. package/middleware/idempotency.js +128 -0
  9. package/package.json +58 -0
  10. package/prompts/explain-failure.js +27 -0
  11. package/prompts/index.js +89 -0
  12. package/prompts/regression-since-pass.js +28 -0
  13. package/prompts/tdd-resume.js +28 -0
  14. package/prompts/triage.js +24 -0
  15. package/prompts/why-flaky.js +30 -0
  16. package/prompts/wrapup.js +19 -0
  17. package/resources/index.js +155 -0
  18. package/resources/indexes.js +77 -0
  19. package/resources/manifest-schema.js +46 -0
  20. package/resources/paths.js +20 -0
  21. package/resources/patterns.js +22 -0
  22. package/resources/upstream-docs.js +22 -0
  23. package/router.js +74 -0
  24. package/server.js +838 -0
  25. package/tools/_tdd-error-envelope.js +98 -0
  26. package/tools/acceptance-metrics.js +75 -0
  27. package/tools/cache-health.js +83 -0
  28. package/tools/commit-changes.js +64 -0
  29. package/tools/configure.js +107 -0
  30. package/tools/coverage.js +76 -0
  31. package/tools/errors.js +151 -0
  32. package/tools/failure-signature-get.js +73 -0
  33. package/tools/file-coverage.js +106 -0
  34. package/tools/help.js +146 -0
  35. package/tools/history.js +121 -0
  36. package/tools/hypothesis.js +127 -0
  37. package/tools/inventory.js +377 -0
  38. package/tools/note.js +208 -0
  39. package/tools/overview.js +92 -0
  40. package/tools/ping.js +22 -0
  41. package/tools/register-agent.js +135 -0
  42. package/tools/run-tests.js +359 -0
  43. package/tools/settings-list.js +48 -0
  44. package/tools/status.js +74 -0
  45. package/tools/tdd-artifact.js +101 -0
  46. package/tools/tdd-behavior.js +177 -0
  47. package/tools/tdd-goal.js +147 -0
  48. package/tools/tdd-phase-transition-request.js +212 -0
  49. package/tools/tdd-task.js +278 -0
  50. package/tools/test.js +281 -0
  51. package/tools/trends.js +112 -0
  52. package/tools/triage-brief.js +42 -0
  53. package/tools/turn-search.js +60 -0
  54. package/tools/wrapup-prompt.js +49 -0
  55. package/tsdoc-metadata.json +11 -0
  56. package/utils/effect-to-zod.js +81 -0
@@ -0,0 +1,101 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { DataReader } from "@vitest-agent/sdk";
3
+ import { Effect, ParseResult, Schema } from "effect";
4
+
5
+ //#region src/tools/tdd-artifact.ts
6
+ /**
7
+ * `tdd_artifact_list` MCP tool — Schema-driven implementation.
8
+ *
9
+ * Returns the artifacts recorded for a TDD task, ordered with the
10
+ * most recent first. The structuredContent payload carries
11
+ * tddTaskId, the applied filters, the count, and the artifact
12
+ * rows so the orchestrator can extract artifact ids without parsing
13
+ * markdown. The legacy `format` input was dropped because
14
+ * structuredContent supersedes it.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+ const ArtifactKindSchema = Schema.Literal("test_written", "test_failed_run", "code_written", "test_passed_run", "refactor", "test_weakened");
19
+ const TddArtifactRow = Schema.Struct({
20
+ id: Schema.Number.annotations({
21
+ title: "tdd_artifacts.id",
22
+ description: "Pass as `citedArtifactId` to `tdd_phase_transition_request`."
23
+ }),
24
+ tddTaskId: Schema.Number,
25
+ phaseId: Schema.Number,
26
+ phaseName: Schema.Literal("spike", "red", "red.triangulate", "green", "green.fake-it", "refactor", "extended-red", "green-without-red"),
27
+ artifactKind: ArtifactKindSchema,
28
+ behaviorId: Schema.NullOr(Schema.Number),
29
+ testCaseId: Schema.NullOr(Schema.Number),
30
+ testRunId: Schema.NullOr(Schema.Number),
31
+ testFirstFailureRunId: Schema.NullOr(Schema.Number),
32
+ recordedAt: Schema.String
33
+ }).annotations({ identifier: "TddArtifactListRow" });
34
+ const ArtifactFilters = Schema.Struct({
35
+ artifactKind: Schema.optional(ArtifactKindSchema),
36
+ phaseId: Schema.optional(Schema.Number),
37
+ behaviorId: Schema.optional(Schema.Number)
38
+ }).annotations({ identifier: "TddArtifactFilters" });
39
+ const TddArtifactListResult = Schema.Struct({
40
+ tddTaskId: Schema.Number,
41
+ filters: ArtifactFilters,
42
+ count: Schema.Number,
43
+ artifacts: Schema.Array(TddArtifactRow)
44
+ }).annotations({
45
+ identifier: "TddArtifactListResult",
46
+ title: "tdd_artifact_list result",
47
+ description: "Newest-first artifact rows for a TDD task. Echoes the filters that were applied so callers can reason about what is/isn't included."
48
+ });
49
+ const describeFilters = (filters) => {
50
+ const parts = [];
51
+ if (filters.artifactKind !== void 0) parts.push(`artifactKind=${filters.artifactKind}`);
52
+ if (filters.phaseId !== void 0) parts.push(`phaseId=${filters.phaseId}`);
53
+ if (filters.behaviorId !== void 0) parts.push(`behaviorId=${filters.behaviorId}`);
54
+ return parts.length > 0 ? ` matching ${parts.join(", ")}` : "";
55
+ };
56
+ const formatTddArtifactListMarkdown = (data) => {
57
+ if (data.count === 0) return `No artifacts recorded for tdd_task ${data.tddTaskId}${describeFilters(data.filters)}.`;
58
+ const lines = [`# Artifacts for tdd_task ${data.tddTaskId} (newest first, ${data.count} shown)`, ""];
59
+ for (const r of data.artifacts) {
60
+ const extras = [`phase=${r.phaseName} [phaseId=${r.phaseId}]`];
61
+ if (r.behaviorId !== null) extras.push(`behaviorId=${r.behaviorId}`);
62
+ if (r.testCaseId !== null) extras.push(`testCaseId=${r.testCaseId}`);
63
+ if (r.testRunId !== null) extras.push(`testRunId=${r.testRunId}`);
64
+ lines.push(`- **${r.artifactKind}** [id=${r.id}] at=${r.recordedAt} ${extras.join(" ")}`);
65
+ }
66
+ return lines.join("\n");
67
+ };
68
+ const TddArtifactListAsMarkdown = Schema.transformOrFail(TddArtifactListResult, Schema.String, {
69
+ strict: true,
70
+ decode: (data) => ParseResult.succeed(formatTddArtifactListMarkdown(data)),
71
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TddArtifactListAsMarkdown is one-way."))
72
+ });
73
+ const TddArtifactListInput = Schema.Struct({
74
+ tddTaskId: Schema.Number,
75
+ artifactKind: Schema.optional(ArtifactKindSchema),
76
+ phaseId: Schema.optional(Schema.Number),
77
+ behaviorId: Schema.optional(Schema.Number),
78
+ limit: Schema.optional(Schema.Number)
79
+ });
80
+ const tddArtifactList = publicProcedure.input(Schema.standardSchemaV1(TddArtifactListInput)).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
81
+ const rows = yield* (yield* DataReader).listTddArtifactsForTask({
82
+ tddTaskId: input.tddTaskId,
83
+ ...input.artifactKind !== void 0 && { artifactKind: input.artifactKind },
84
+ ...input.phaseId !== void 0 && { phaseId: input.phaseId },
85
+ ...input.behaviorId !== void 0 && { behaviorId: input.behaviorId },
86
+ ...input.limit !== void 0 && { limit: input.limit }
87
+ });
88
+ return {
89
+ tddTaskId: input.tddTaskId,
90
+ filters: {
91
+ ...input.artifactKind !== void 0 && { artifactKind: input.artifactKind },
92
+ ...input.phaseId !== void 0 && { phaseId: input.phaseId },
93
+ ...input.behaviorId !== void 0 && { behaviorId: input.behaviorId }
94
+ },
95
+ count: rows.length,
96
+ artifacts: rows
97
+ };
98
+ })));
99
+
100
+ //#endregion
101
+ export { TddArtifactListAsMarkdown, TddArtifactListResult, tddArtifactList };
@@ -0,0 +1,177 @@
1
+ import { idempotentProcedure } from "../middleware/idempotency.js";
2
+ import { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
3
+ import { BehaviorDetail, BehaviorRow, DataReader, DataStore } from "@vitest-agent/sdk";
4
+ import { Effect, Match, Option, Schema } from "effect";
5
+
6
+ //#region src/tools/tdd-behavior.ts
7
+ /**
8
+ * Consolidated `tdd_behavior` MCP tool.
9
+ *
10
+ * Replaces `tdd_behavior_create`, `tdd_behavior_update`,
11
+ * `tdd_behavior_delete`, `tdd_behavior_get`, and `tdd_behavior_list`
12
+ * (the latter already had a goal/tdd_task scope discriminator that
13
+ * folds into the action discriminator here as `list_by_goal` /
14
+ * `list_by_tdd_task`).
15
+ */
16
+ const BehaviorStatus = Schema.Literal("pending", "in_progress", "done", "abandoned");
17
+ const TddBehaviorErrorEnvelope = Schema.Struct({
18
+ ok: Schema.Literal(false).annotations({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
19
+ error: Schema.Struct({
20
+ _tag: Schema.String.annotations({ description: "Tagged error name (e.g. BehaviorNotFoundError, GoalNotFoundError)." }),
21
+ message: Schema.String,
22
+ remediation: Schema.optional(Schema.String)
23
+ }).pipe(Schema.extend(Schema.Record({
24
+ key: Schema.String,
25
+ value: Schema.Unknown
26
+ })))
27
+ }).annotations({ identifier: "TddBehaviorErrorEnvelope" });
28
+ const TddBehaviorCreateOk = Schema.Struct({
29
+ ok: Schema.Literal(true),
30
+ action: Schema.Literal("create"),
31
+ behavior: BehaviorRow.annotations({ description: "Newly inserted behavior row." })
32
+ });
33
+ const TddBehaviorUpdateOk = Schema.Struct({
34
+ ok: Schema.Literal(true),
35
+ action: Schema.Literal("update"),
36
+ behavior: BehaviorRow.annotations({ description: "Updated behavior row." })
37
+ });
38
+ const TddBehaviorDeleteOk = Schema.Struct({
39
+ ok: Schema.Literal(true),
40
+ action: Schema.Literal("delete"),
41
+ id: Schema.Number
42
+ });
43
+ const TddBehaviorGetFound = Schema.Struct({
44
+ action: Schema.Literal("get"),
45
+ found: Schema.Literal(true),
46
+ behavior: BehaviorDetail.annotations({ description: "Behavior with parentGoal + dependencies[]." })
47
+ });
48
+ const TddBehaviorGetMissing = Schema.Struct({
49
+ action: Schema.Literal("get"),
50
+ found: Schema.Literal(false),
51
+ id: Schema.Number
52
+ });
53
+ const TddBehaviorListByGoalOk = Schema.Struct({
54
+ ok: Schema.Literal(true),
55
+ action: Schema.Literal("list_by_goal"),
56
+ goalId: Schema.Number,
57
+ behaviors: Schema.Array(BehaviorRow)
58
+ });
59
+ const TddBehaviorListByTddTaskOk = Schema.Struct({
60
+ ok: Schema.Literal(true),
61
+ action: Schema.Literal("list_by_tdd_task"),
62
+ tddTaskId: Schema.Number,
63
+ behaviors: Schema.Array(BehaviorRow)
64
+ });
65
+ const TddBehaviorResult = Schema.Union(TddBehaviorCreateOk, TddBehaviorUpdateOk, TddBehaviorDeleteOk, TddBehaviorGetFound, TddBehaviorGetMissing, TddBehaviorListByGoalOk, TddBehaviorListByTddTaskOk, TddBehaviorErrorEnvelope).annotations({
66
+ identifier: "TddBehaviorResult",
67
+ title: "tdd_behavior result",
68
+ description: "Discriminate on `action` (or `ok=false` for the tagged-error envelope)."
69
+ });
70
+ const CreateVariant = Schema.Struct({
71
+ action: Schema.Literal("create"),
72
+ goalId: Schema.Number,
73
+ behavior: Schema.String,
74
+ suggestedTestName: Schema.optional(Schema.String),
75
+ dependsOnBehaviorIds: Schema.optional(Schema.Array(Schema.Number))
76
+ });
77
+ const UpdateVariant = Schema.Struct({
78
+ action: Schema.Literal("update"),
79
+ id: Schema.Number,
80
+ behavior: Schema.optional(Schema.String),
81
+ suggestedTestName: Schema.optional(Schema.NullOr(Schema.String)),
82
+ status: Schema.optional(BehaviorStatus),
83
+ dependsOnBehaviorIds: Schema.optional(Schema.Array(Schema.Number))
84
+ });
85
+ const DeleteVariant = Schema.Struct({
86
+ action: Schema.Literal("delete"),
87
+ id: Schema.Number
88
+ });
89
+ const GetVariant = Schema.Struct({
90
+ action: Schema.Literal("get"),
91
+ id: Schema.Number
92
+ });
93
+ const ListByGoalVariant = Schema.Struct({
94
+ action: Schema.Literal("list_by_goal"),
95
+ goalId: Schema.Number
96
+ });
97
+ const ListByTddTaskVariant = Schema.Struct({
98
+ action: Schema.Literal("list_by_tdd_task"),
99
+ tddTaskId: Schema.Number
100
+ });
101
+ const TddBehaviorInput = Schema.Union(CreateVariant, UpdateVariant, DeleteVariant, GetVariant, ListByGoalVariant, ListByTddTaskVariant);
102
+ const tddBehavior = idempotentProcedure.input(Schema.standardSchemaV1(TddBehaviorInput)).mutation(async ({ ctx, input }) => {
103
+ return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
104
+ create: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
105
+ return {
106
+ ok: true,
107
+ action: "create",
108
+ behavior: yield* (yield* DataStore).createBehavior({
109
+ goalId: variant.goalId,
110
+ behavior: variant.behavior,
111
+ ...variant.suggestedTestName !== void 0 && { suggestedTestName: variant.suggestedTestName },
112
+ ...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
113
+ })
114
+ };
115
+ })),
116
+ update: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
117
+ return {
118
+ ok: true,
119
+ action: "update",
120
+ behavior: yield* (yield* DataStore).updateBehavior({
121
+ id: variant.id,
122
+ ...variant.behavior !== void 0 && { behavior: variant.behavior },
123
+ ...variant.suggestedTestName !== void 0 && { suggestedTestName: variant.suggestedTestName },
124
+ ...variant.status !== void 0 && { status: variant.status },
125
+ ...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
126
+ })
127
+ };
128
+ })),
129
+ delete: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
130
+ yield* (yield* DataStore).deleteBehavior(variant.id);
131
+ return {
132
+ ok: true,
133
+ action: "delete",
134
+ id: variant.id
135
+ };
136
+ })),
137
+ get: (variant) => Effect.gen(function* () {
138
+ const opt = yield* (yield* DataReader).getBehaviorById(variant.id);
139
+ return Option.isNone(opt) ? {
140
+ action: "get",
141
+ found: false,
142
+ id: variant.id
143
+ } : {
144
+ action: "get",
145
+ found: true,
146
+ behavior: opt.value
147
+ };
148
+ }),
149
+ list_by_goal: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
150
+ const store = yield* DataStore;
151
+ const reader = yield* DataReader;
152
+ yield* store.listBehaviorsByGoal(variant.goalId);
153
+ const behaviors = yield* reader.getBehaviorsByGoal(variant.goalId);
154
+ return {
155
+ ok: true,
156
+ action: "list_by_goal",
157
+ goalId: variant.goalId,
158
+ behaviors
159
+ };
160
+ })),
161
+ list_by_tdd_task: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
162
+ const store = yield* DataStore;
163
+ const reader = yield* DataReader;
164
+ yield* store.listBehaviorsByTddTask(variant.tddTaskId);
165
+ const behaviors = yield* reader.getBehaviorsByTddTask(variant.tddTaskId);
166
+ return {
167
+ ok: true,
168
+ action: "list_by_tdd_task",
169
+ tddTaskId: variant.tddTaskId,
170
+ behaviors
171
+ };
172
+ }))
173
+ })));
174
+ });
175
+
176
+ //#endregion
177
+ export { TddBehaviorResult, tddBehavior };
@@ -0,0 +1,147 @@
1
+ import { idempotentProcedure } from "../middleware/idempotency.js";
2
+ import { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
3
+ import { DataReader, DataStore, GoalDetail, GoalRow } from "@vitest-agent/sdk";
4
+ import { Effect, Match, Option, Schema } from "effect";
5
+
6
+ //#region src/tools/tdd-goal.ts
7
+ /**
8
+ * Consolidated `tdd_goal` MCP tool.
9
+ *
10
+ * Replaces `tdd_goal_create`, `tdd_goal_update`, `tdd_goal_delete`,
11
+ * `tdd_goal_get`, and `tdd_goal_list` with a single tool keyed on
12
+ * `action`. All five tagged TDD errors are caught and surfaced as
13
+ * `{ ok: false, error: { _tag, ..., remediation } }` envelopes via
14
+ * the shared helper.
15
+ */
16
+ const GoalStatus = Schema.Literal("pending", "in_progress", "done", "abandoned");
17
+ const TddErrorEnvelope = Schema.Struct({
18
+ ok: Schema.Literal(false).annotations({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
19
+ error: Schema.Struct({
20
+ _tag: Schema.String.annotations({ description: "Tagged error name (e.g. GoalNotFoundError, TddTaskNotFoundError)." }),
21
+ message: Schema.String,
22
+ remediation: Schema.optional(Schema.String).annotations({ description: "Suggested next action when known." })
23
+ }).pipe(Schema.extend(Schema.Record({
24
+ key: Schema.String,
25
+ value: Schema.Unknown
26
+ })))
27
+ }).annotations({ identifier: "TddErrorEnvelope" });
28
+ const TddGoalCreateOk = Schema.Struct({
29
+ ok: Schema.Literal(true),
30
+ action: Schema.Literal("create"),
31
+ goal: GoalRow.annotations({ description: "Newly inserted goal row." })
32
+ }).annotations({ identifier: "TddGoalCreateOk" });
33
+ const TddGoalUpdateOk = Schema.Struct({
34
+ ok: Schema.Literal(true),
35
+ action: Schema.Literal("update"),
36
+ goal: GoalRow.annotations({ description: "Updated goal row." })
37
+ }).annotations({ identifier: "TddGoalUpdateOk" });
38
+ const TddGoalDeleteOk = Schema.Struct({
39
+ ok: Schema.Literal(true),
40
+ action: Schema.Literal("delete"),
41
+ id: Schema.Number
42
+ }).annotations({ identifier: "TddGoalDeleteOk" });
43
+ const TddGoalGetFound = Schema.Struct({
44
+ action: Schema.Literal("get"),
45
+ found: Schema.Literal(true),
46
+ goal: GoalDetail.annotations({ description: "Goal with nested behaviors[]." })
47
+ }).annotations({ identifier: "TddGoalGetFound" });
48
+ const TddGoalGetMissing = Schema.Struct({
49
+ action: Schema.Literal("get"),
50
+ found: Schema.Literal(false),
51
+ id: Schema.Number
52
+ }).annotations({ identifier: "TddGoalGetMissing" });
53
+ const TddGoalListOk = Schema.Struct({
54
+ ok: Schema.Literal(true),
55
+ action: Schema.Literal("list"),
56
+ tddTaskId: Schema.Number,
57
+ goals: Schema.Array(GoalDetail).annotations({ description: "All goals for the TDD task, with their behaviors." })
58
+ }).annotations({ identifier: "TddGoalListOk" });
59
+ const TddGoalResult = Schema.Union(TddGoalCreateOk, TddGoalUpdateOk, TddGoalDeleteOk, TddGoalGetFound, TddGoalGetMissing, TddGoalListOk, TddErrorEnvelope).annotations({
60
+ identifier: "TddGoalResult",
61
+ title: "tdd_goal result",
62
+ description: "Discriminate first on `action` (or on `ok=false` for the tagged-error envelope). Get returns `found:true|false`; create/update/delete/list return `ok:true` on success."
63
+ });
64
+ const CreateVariant = Schema.Struct({
65
+ action: Schema.Literal("create"),
66
+ tddTaskId: Schema.Number,
67
+ goal: Schema.String
68
+ });
69
+ const UpdateVariant = Schema.Struct({
70
+ action: Schema.Literal("update"),
71
+ id: Schema.Number,
72
+ goal: Schema.optional(Schema.String),
73
+ status: Schema.optional(GoalStatus)
74
+ });
75
+ const DeleteVariant = Schema.Struct({
76
+ action: Schema.Literal("delete"),
77
+ id: Schema.Number
78
+ });
79
+ const GetVariant = Schema.Struct({
80
+ action: Schema.Literal("get"),
81
+ id: Schema.Number
82
+ });
83
+ const ListVariant = Schema.Struct({
84
+ action: Schema.Literal("list"),
85
+ tddTaskId: Schema.Number
86
+ });
87
+ const TddGoalInput = Schema.Union(CreateVariant, UpdateVariant, DeleteVariant, GetVariant, ListVariant);
88
+ const tddGoal = idempotentProcedure.input(Schema.standardSchemaV1(TddGoalInput)).mutation(async ({ ctx, input }) => {
89
+ return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
90
+ create: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
91
+ return {
92
+ ok: true,
93
+ action: "create",
94
+ goal: yield* (yield* DataStore).createGoal({
95
+ tddTaskId: variant.tddTaskId,
96
+ goal: variant.goal
97
+ })
98
+ };
99
+ })),
100
+ update: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
101
+ return {
102
+ ok: true,
103
+ action: "update",
104
+ goal: yield* (yield* DataStore).updateGoal({
105
+ id: variant.id,
106
+ ...variant.goal !== void 0 && { goal: variant.goal },
107
+ ...variant.status !== void 0 && { status: variant.status }
108
+ })
109
+ };
110
+ })),
111
+ delete: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
112
+ yield* (yield* DataStore).deleteGoal(variant.id);
113
+ return {
114
+ ok: true,
115
+ action: "delete",
116
+ id: variant.id
117
+ };
118
+ })),
119
+ get: (variant) => Effect.gen(function* () {
120
+ const opt = yield* (yield* DataReader).getGoalById(variant.id);
121
+ return Option.isNone(opt) ? {
122
+ action: "get",
123
+ found: false,
124
+ id: variant.id
125
+ } : {
126
+ action: "get",
127
+ found: true,
128
+ goal: opt.value
129
+ };
130
+ }),
131
+ list: (variant) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
132
+ const store = yield* DataStore;
133
+ const reader = yield* DataReader;
134
+ yield* store.listGoalsByTddTask(variant.tddTaskId);
135
+ const goals = yield* reader.getGoalsByTddTask(variant.tddTaskId);
136
+ return {
137
+ ok: true,
138
+ action: "list",
139
+ tddTaskId: variant.tddTaskId,
140
+ goals
141
+ };
142
+ }))
143
+ })));
144
+ });
145
+
146
+ //#endregion
147
+ export { TddGoalResult, tddGoal };
@@ -0,0 +1,212 @@
1
+ import { publicProcedure } from "../context.js";
2
+ import { DataReader, DataStore, requiredArtifactForTransition, validatePhaseTransition } from "@vitest-agent/sdk";
3
+ import { Effect, Option, Schema } from "effect";
4
+
5
+ //#region src/tools/tdd-phase-transition-request.ts
6
+ const phaseLiteral = Schema.Literal("spike", "red", "red.triangulate", "green", "green.fake-it", "refactor", "extended-red", "green-without-red");
7
+ const artifactKindLiteral = Schema.Literal("test_written", "test_failed_run", "code_written", "test_passed_run", "refactor", "test_weakened");
8
+ const denialReasonLiteral = Schema.Literal("missing_artifact_evidence", "wrong_artifact_kind", "wrong_source_phase", "unknown_tdd_task", "tdd_task_already_ended", "goal_not_started", "goal_not_found", "goal_not_in_progress", "goal_not_in_tdd_task", "behavior_not_found", "behavior_not_in_goal", "refactor_without_passing_run", "evidence_not_in_phase_window", "evidence_not_for_behavior", "evidence_test_was_already_failing");
9
+ const RemediationSchema = Schema.Struct({
10
+ suggestedTool: Schema.String.annotations({ description: "Next tool the agent should call to make progress." }),
11
+ suggestedArgs: Schema.Record({
12
+ key: Schema.String,
13
+ value: Schema.Unknown
14
+ }).annotations({ description: "Concrete arguments for `suggestedTool` that fix the underlying issue." }),
15
+ humanHint: Schema.String.annotations({ description: "Plain-language explanation of what to do next." })
16
+ }).annotations({ identifier: "PhaseTransitionRemediation" });
17
+ const PhaseTransitionAccepted = Schema.Struct({
18
+ accepted: Schema.Literal(true).annotations({ description: "Discriminant — `true` when the transition was granted." }),
19
+ phase: phaseLiteral.annotations({ description: "Phase the session is now in (echo of `requestedPhase`)." }),
20
+ newPhaseId: Schema.Number.annotations({ description: "`tdd_phases.id` of the freshly opened row." }),
21
+ previousPhaseId: Schema.NullOr(Schema.Number).annotations({ description: "`tdd_phases.id` of the phase that was closed (`null` for the first transition)." }),
22
+ citedArtifactId: Schema.optional(Schema.Number).annotations({ description: "Resolved `tdd_artifacts.id` actually used. Absent only for transitions that need no artifact (e.g. spike→red)." }),
23
+ citedArtifactSource: Schema.optional(Schema.Literal("explicit-id", "explicit-kind", "transition-derived", "none")).annotations({ description: "Where `citedArtifactId` came from: `explicit-id` (caller passed it), `explicit-kind` (resolved from caller's `citedArtifactKind`), `transition-derived` (resolved from the transition's required-evidence rule), or `none` (no artifact needed)." })
24
+ }).annotations({ identifier: "PhaseTransitionAccepted" });
25
+ const PhaseTransitionDenied = Schema.Struct({
26
+ accepted: Schema.Literal(false).annotations({ description: "Discriminant — `false` when the transition was refused." }),
27
+ phase: phaseLiteral.annotations({ description: "Phase the session remains in (the current phase, unchanged)." }),
28
+ denialReason: denialReasonLiteral.annotations({ description: "Categorical refusal reason; see `remediation` for the suggested fix." }),
29
+ remediation: RemediationSchema
30
+ }).annotations({ identifier: "PhaseTransitionDenied" });
31
+ const PhaseTransitionResult = Schema.Union(PhaseTransitionAccepted, PhaseTransitionDenied).annotations({
32
+ identifier: "PhaseTransitionResult",
33
+ title: "tdd_phase_transition_request result",
34
+ description: "Discriminate on `accepted`. Acceptance carries the new phaseId; denial carries a typed reason and a remediation pointer."
35
+ });
36
+ const tddPhaseTransitionRequest = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({
37
+ tddTaskId: Schema.Number,
38
+ goalId: Schema.Number,
39
+ requestedPhase: phaseLiteral,
40
+ citedArtifactId: Schema.optional(Schema.Number),
41
+ citedArtifactKind: Schema.optional(artifactKindLiteral),
42
+ behaviorId: Schema.optional(Schema.Number),
43
+ reason: Schema.optional(Schema.String)
44
+ }))).mutation(async ({ ctx, input }) => {
45
+ return ctx.runtime.runPromise(Effect.gen(function* () {
46
+ const reader = yield* DataReader;
47
+ const store = yield* DataStore;
48
+ const currentOpt = yield* reader.getCurrentTddPhase(input.tddTaskId);
49
+ const currentPhase = Option.isSome(currentOpt) ? currentOpt.value.phase : "spike";
50
+ const phaseStartedAt = Option.isSome(currentOpt) ? currentOpt.value.startedAt : (/* @__PURE__ */ new Date()).toISOString();
51
+ const goalOpt = yield* reader.getGoalById(input.goalId);
52
+ if (Option.isNone(goalOpt)) return {
53
+ accepted: false,
54
+ phase: currentPhase,
55
+ denialReason: "goal_not_found",
56
+ remediation: {
57
+ suggestedTool: "tdd_goal",
58
+ suggestedArgs: {
59
+ action: "list",
60
+ tddTaskId: input.tddTaskId
61
+ },
62
+ humanHint: `No tdd_session_goals row with id=${input.goalId}. Call tdd_goal({ action: "list" }) to find the correct goal id.`
63
+ }
64
+ };
65
+ if (goalOpt.value.sessionId !== input.tddTaskId) return {
66
+ accepted: false,
67
+ phase: currentPhase,
68
+ denialReason: "goal_not_in_tdd_task",
69
+ remediation: {
70
+ suggestedTool: "tdd_goal",
71
+ suggestedArgs: {
72
+ action: "list",
73
+ tddTaskId: input.tddTaskId
74
+ },
75
+ humanHint: `Goal id=${input.goalId} belongs to TDD task ${goalOpt.value.sessionId}, not the requested tddTaskId=${input.tddTaskId}. Pass the tddTaskId of the goal's parent task, or pick a goal that belongs to the active task.`
76
+ }
77
+ };
78
+ if (goalOpt.value.status !== "in_progress") return {
79
+ accepted: false,
80
+ phase: currentPhase,
81
+ denialReason: "goal_not_in_progress",
82
+ remediation: {
83
+ suggestedTool: "tdd_goal_update",
84
+ suggestedArgs: {
85
+ id: input.goalId,
86
+ status: "in_progress"
87
+ },
88
+ humanHint: `Goal id=${input.goalId} has status '${goalOpt.value.status}'. Phase transitions require the goal to be in_progress. Call tdd_goal_update({status:'in_progress'}) before requesting transitions.`
89
+ }
90
+ };
91
+ if (input.behaviorId !== void 0) {
92
+ const behaviorOpt = yield* reader.getBehaviorById(input.behaviorId);
93
+ if (Option.isNone(behaviorOpt)) return {
94
+ accepted: false,
95
+ phase: currentPhase,
96
+ denialReason: "behavior_not_found",
97
+ remediation: {
98
+ suggestedTool: "tdd_behavior_list",
99
+ suggestedArgs: {
100
+ scope: "goal",
101
+ goalId: input.goalId
102
+ },
103
+ humanHint: `No tdd_session_behaviors row with id=${input.behaviorId}. Call tdd_behavior_list to find the correct behavior id.`
104
+ }
105
+ };
106
+ if (behaviorOpt.value.goalId !== input.goalId) return {
107
+ accepted: false,
108
+ phase: currentPhase,
109
+ denialReason: "behavior_not_in_goal",
110
+ remediation: {
111
+ suggestedTool: "tdd_behavior_get",
112
+ suggestedArgs: { id: input.behaviorId },
113
+ humanHint: `Behavior id=${input.behaviorId} belongs to goal ${behaviorOpt.value.goalId}, not the requested goalId=${input.goalId}. Pass the goalId of the behavior's parent goal.`
114
+ }
115
+ };
116
+ }
117
+ const requiredKindForTransition = requiredArtifactForTransition(currentPhase, input.requestedPhase);
118
+ let resolvedArtifactId;
119
+ let resolvedKindSource = "none";
120
+ let kindToLookUp;
121
+ if (input.citedArtifactId !== void 0) {
122
+ resolvedArtifactId = input.citedArtifactId;
123
+ resolvedKindSource = "explicit-id";
124
+ } else if (input.citedArtifactKind !== void 0) {
125
+ kindToLookUp = input.citedArtifactKind;
126
+ resolvedKindSource = "explicit-kind";
127
+ } else if (requiredKindForTransition !== null) {
128
+ kindToLookUp = requiredKindForTransition.kind;
129
+ resolvedKindSource = "transition-derived";
130
+ }
131
+ if (resolvedArtifactId === void 0 && kindToLookUp !== void 0) {
132
+ const recent = yield* reader.listTddArtifactsForTask({
133
+ tddTaskId: input.tddTaskId,
134
+ artifactKind: kindToLookUp,
135
+ limit: 1
136
+ });
137
+ if (recent.length === 0) return {
138
+ accepted: false,
139
+ phase: currentPhase,
140
+ denialReason: "missing_artifact_evidence",
141
+ remediation: {
142
+ suggestedTool: "run_tests",
143
+ suggestedArgs: {},
144
+ humanHint: `No '${kindToLookUp}' artifact has been recorded for tdd_task ${input.tddTaskId}. Artifacts are recorded by hooks observing your tool calls (Decision D7) — run the test (e.g. via run_tests) or make the file edit first; the post-tool-use hook will write the matching tdd_artifacts row and the next call to this tool will pick it up automatically.`
145
+ }
146
+ };
147
+ resolvedArtifactId = recent[0].id;
148
+ }
149
+ let citedArtifact;
150
+ if (resolvedArtifactId !== void 0) {
151
+ const artifactOpt = yield* reader.getTddArtifactWithContext(resolvedArtifactId);
152
+ if (Option.isNone(artifactOpt)) return {
153
+ accepted: false,
154
+ phase: currentPhase,
155
+ denialReason: "missing_artifact_evidence",
156
+ remediation: {
157
+ suggestedTool: "run_tests",
158
+ suggestedArgs: {},
159
+ humanHint: `Cited artifact id ${resolvedArtifactId} does not exist. Artifacts are recorded by hooks observing your tool calls (Decision D7), so run the test (e.g. via the run_tests MCP tool) or make the file edit first; the post-tool-use hook will write the matching tdd_artifacts row and return its id, which can then be cited here.`
160
+ }
161
+ };
162
+ citedArtifact = artifactOpt.value;
163
+ } else citedArtifact = {
164
+ id: -1,
165
+ phase_id: -1,
166
+ artifact_kind: "test_written",
167
+ test_case_id: null,
168
+ test_case_created_turn_at: null,
169
+ test_case_authored_in_session: false,
170
+ test_run_id: null,
171
+ test_first_failure_run_id: null,
172
+ behavior_id: null
173
+ };
174
+ const result = validatePhaseTransition({
175
+ tdd_task_id: input.tddTaskId,
176
+ current_phase: currentPhase,
177
+ phase_started_at: phaseStartedAt,
178
+ now: (/* @__PURE__ */ new Date()).toISOString(),
179
+ requested_phase: input.requestedPhase,
180
+ cited_artifact: citedArtifact,
181
+ requested_behavior_id: input.behaviorId ?? null
182
+ });
183
+ if (!result.accepted) return result;
184
+ const out = yield* store.writeTddPhase({
185
+ tddTaskId: input.tddTaskId,
186
+ phase: result.phase,
187
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
188
+ ...input.behaviorId !== void 0 && { behaviorId: input.behaviorId },
189
+ ...input.reason !== void 0 && { transitionReason: input.reason }
190
+ });
191
+ if (input.behaviorId !== void 0) yield* Effect.ignoreLogged(Effect.gen(function* () {
192
+ const behOpt = yield* reader.getBehaviorById(input.behaviorId);
193
+ if (Option.isSome(behOpt) && behOpt.value.status === "pending") yield* store.updateBehavior({
194
+ id: input.behaviorId,
195
+ status: "in_progress"
196
+ });
197
+ }));
198
+ return {
199
+ accepted: true,
200
+ phase: result.phase,
201
+ newPhaseId: out.id,
202
+ previousPhaseId: out.previousPhaseId,
203
+ ...resolvedArtifactId !== void 0 && {
204
+ citedArtifactId: resolvedArtifactId,
205
+ citedArtifactSource: resolvedKindSource
206
+ }
207
+ };
208
+ }));
209
+ });
210
+
211
+ //#endregion
212
+ export { PhaseTransitionResult, tddPhaseTransitionRequest };