@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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/vitest-agent-mcp.js +93 -0
- package/context.js +72 -0
- package/index.d.ts +1577 -0
- package/index.js +19 -0
- package/layers/McpLive.js +30 -0
- package/middleware/idempotency.js +128 -0
- package/package.json +58 -0
- package/prompts/explain-failure.js +27 -0
- package/prompts/index.js +89 -0
- package/prompts/regression-since-pass.js +28 -0
- package/prompts/tdd-resume.js +28 -0
- package/prompts/triage.js +24 -0
- package/prompts/why-flaky.js +30 -0
- package/prompts/wrapup.js +19 -0
- package/resources/index.js +155 -0
- package/resources/indexes.js +77 -0
- package/resources/manifest-schema.js +46 -0
- package/resources/paths.js +20 -0
- package/resources/patterns.js +22 -0
- package/resources/upstream-docs.js +22 -0
- package/router.js +74 -0
- package/server.js +838 -0
- package/tools/_tdd-error-envelope.js +98 -0
- package/tools/acceptance-metrics.js +75 -0
- package/tools/cache-health.js +83 -0
- package/tools/commit-changes.js +64 -0
- package/tools/configure.js +107 -0
- package/tools/coverage.js +76 -0
- package/tools/errors.js +151 -0
- package/tools/failure-signature-get.js +73 -0
- package/tools/file-coverage.js +106 -0
- package/tools/help.js +146 -0
- package/tools/history.js +121 -0
- package/tools/hypothesis.js +127 -0
- package/tools/inventory.js +377 -0
- package/tools/note.js +208 -0
- package/tools/overview.js +92 -0
- package/tools/ping.js +22 -0
- package/tools/register-agent.js +135 -0
- package/tools/run-tests.js +359 -0
- package/tools/settings-list.js +48 -0
- package/tools/status.js +74 -0
- package/tools/tdd-artifact.js +101 -0
- package/tools/tdd-behavior.js +177 -0
- package/tools/tdd-goal.js +147 -0
- package/tools/tdd-phase-transition-request.js +212 -0
- package/tools/tdd-task.js +278 -0
- package/tools/test.js +281 -0
- package/tools/trends.js +112 -0
- package/tools/triage-brief.js +42 -0
- package/tools/turn-search.js +60 -0
- package/tools/wrapup-prompt.js +49 -0
- package/tsdoc-metadata.json +11 -0
- 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 };
|