@programinglive/dev-workflow-mcp-server 1.0.9 → 1.1.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/CHANGELOG.md +43 -0
- package/README.md +2 -2
- package/commit-helpers.js +166 -0
- package/exec.js +4 -0
- package/git-helpers.js +152 -0
- package/index.js +45 -1300
- package/package.json +1 -1
- package/tests/handlers.test.js +460 -0
- package/tools/definitions.js +211 -0
- package/tools/handlers.js +739 -0
- package/utils.js +45 -0
- package/workflow-state.js +107 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
const textResponse = (text) => ({
|
|
2
|
+
content: [
|
|
3
|
+
{
|
|
4
|
+
type: "text",
|
|
5
|
+
text,
|
|
6
|
+
},
|
|
7
|
+
],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
11
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
16
|
+
const hasStagedChanges = await git.hasStagedChanges();
|
|
17
|
+
|
|
18
|
+
if (!hasWorkingChanges && !hasStagedChanges) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
23
|
+
workflowState.state.released = false;
|
|
24
|
+
workflowState.state.currentPhase = "commit";
|
|
25
|
+
workflowState.state.releaseCommand = "";
|
|
26
|
+
workflowState.state.releaseNotes = "";
|
|
27
|
+
await workflowState.save();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatChecklist(checks) {
|
|
32
|
+
return checks.map((c) => `${c.done ? "✅" : "❌"} ${c.name}`).join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function summarizeHistory(history, limit) {
|
|
36
|
+
const recent = history.slice(-limit).reverse();
|
|
37
|
+
if (recent.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const historyText = recent
|
|
42
|
+
.map(
|
|
43
|
+
(entry, index) =>
|
|
44
|
+
`${index + 1}. [${new Date(entry.timestamp).toLocaleString()}]\n Task: ${entry.taskDescription}\n Type: ${entry.taskType}\n Commit: ${entry.commitMessage}\n`
|
|
45
|
+
)
|
|
46
|
+
.join("\n");
|
|
47
|
+
|
|
48
|
+
return `📜 Workflow History (last ${recent.length} tasks)\n\n${historyText}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getContinueGuidance(status) {
|
|
52
|
+
switch (status.currentPhase) {
|
|
53
|
+
case "coding": {
|
|
54
|
+
const task = status.taskDescription ? ` "${status.taskDescription}"` : "";
|
|
55
|
+
return `Finish implementing${task} and then run 'mark_bug_fixed' with a brief summary of the change.`;
|
|
56
|
+
}
|
|
57
|
+
case "testing":
|
|
58
|
+
if (!status.testsCreated) {
|
|
59
|
+
return "Create automated coverage now using 'create_tests' before recording any results.";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (status.testsPassed) {
|
|
63
|
+
return "With tests green, move on to 'create_documentation' and capture what changed.";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (status.testsSkipped) {
|
|
67
|
+
return "Document why tests were skipped using 'create_documentation' so the workflow can proceed.";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return status.testCommand
|
|
71
|
+
? `Fix any failing tests and re-run '${status.testCommand}'. Once they pass, record the results with 'run_tests'.`
|
|
72
|
+
: "Run your automated tests and record the outcome with 'run_tests'. Fix any failures before moving forward.";
|
|
73
|
+
case "documentation":
|
|
74
|
+
return status.testsSkipped
|
|
75
|
+
? `Document the change (including manual QA for skipped tests) with 'create_documentation'. Reason recorded: ${
|
|
76
|
+
status.testsSkippedReason || "(none provided)"
|
|
77
|
+
}.`
|
|
78
|
+
: "Capture your updates with 'create_documentation' (provide the documentation type and a short summary).";
|
|
79
|
+
case "ready":
|
|
80
|
+
return "Run 'check_ready_to_commit' to verify all prerequisite steps before committing.";
|
|
81
|
+
case "commit":
|
|
82
|
+
return "All checks passed—run 'commit_and_push' with your commit message (and optional branch).";
|
|
83
|
+
case "release":
|
|
84
|
+
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes.";
|
|
85
|
+
case "ready_to_complete":
|
|
86
|
+
return "Release recorded. Finish up by calling 'complete_task' with the commit message you used.";
|
|
87
|
+
default:
|
|
88
|
+
return "Review your workflow progress and complete the next required step.";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleContinueWorkflow(workflowState, context) {
|
|
93
|
+
const status = workflowState.state;
|
|
94
|
+
|
|
95
|
+
if (!status || status.currentPhase === "idle") {
|
|
96
|
+
return textResponse("⚠️ No active workflow. Use 'start_task' to kick off a new task before continuing.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (await resetToCommitIfWorkingChanges(workflowState, context.git)) {
|
|
100
|
+
return textResponse(
|
|
101
|
+
"⚠️ Detected new changes after the last commit. Workflow moved back to the commit phase. Run 'commit_and_push' to capture the latest updates before continuing."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (status.currentPhase === "ready") {
|
|
106
|
+
return handleReadyCheck(workflowState);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (status.currentPhase === "commit") {
|
|
110
|
+
return handleCommitAndPush({}, context);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (status.currentPhase === "release") {
|
|
114
|
+
return textResponse(
|
|
115
|
+
"⚠️ Release phase requires explicit command. Run 'perform_release' with your release command (e.g., 'npm run release')."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sections = [
|
|
120
|
+
`📊 Current Phase: ${status.currentPhase}`,
|
|
121
|
+
`📝 Task: ${status.taskDescription || "(none)"}`,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
if (status.testsSkipped) {
|
|
125
|
+
sections.push(`⚠️ Tests skipped: ${status.testsSkippedReason || "(no reason provided)"}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
status.currentPhase === "testing" &&
|
|
130
|
+
status.testsCreated &&
|
|
131
|
+
!status.testsPassed &&
|
|
132
|
+
typeof status.testCommand === "string" &&
|
|
133
|
+
status.testCommand.trim() !== ""
|
|
134
|
+
) {
|
|
135
|
+
sections.push(`🧪 Last test command: ${status.testCommand}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sections.push(`➡️ ${getContinueGuidance(status)}`);
|
|
139
|
+
|
|
140
|
+
return textResponse(sections.join("\n\n"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function handleStartTask(args, workflowState) {
|
|
144
|
+
workflowState.reset();
|
|
145
|
+
workflowState.state.currentPhase = "coding";
|
|
146
|
+
workflowState.state.taskDescription = args.description;
|
|
147
|
+
workflowState.state.taskType = args.type;
|
|
148
|
+
workflowState.state.testsSkipped = false;
|
|
149
|
+
workflowState.state.testsSkippedReason = "";
|
|
150
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
151
|
+
workflowState.state.lastCommitMessage = "";
|
|
152
|
+
workflowState.state.lastPushBranch = "";
|
|
153
|
+
await workflowState.save();
|
|
154
|
+
|
|
155
|
+
return textResponse(
|
|
156
|
+
`✅ Task Started: ${args.description}\n\n🎯 Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Run 'commit_and_push' (commits and pushes)\n8. ⏳ Run 'perform_release' (handles versioning and tags)\n9. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleMarkBugFixed(args, workflowState) {
|
|
161
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
162
|
+
return textResponse("⚠️ Please start a task first using 'start_task'!");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
workflowState.state.bugFixed = true;
|
|
166
|
+
workflowState.state.testsCreated = false;
|
|
167
|
+
workflowState.state.currentPhase = "testing";
|
|
168
|
+
workflowState.state.testsSkipped = false;
|
|
169
|
+
workflowState.state.testsSkippedReason = "";
|
|
170
|
+
workflowState.state.fixSummary = args.summary;
|
|
171
|
+
workflowState.state.readyCheckCompleted = false;
|
|
172
|
+
workflowState.state.released = false;
|
|
173
|
+
workflowState.state.releaseCommand = "";
|
|
174
|
+
workflowState.state.releaseNotes = "";
|
|
175
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
176
|
+
workflowState.state.lastCommitMessage = "";
|
|
177
|
+
workflowState.state.lastPushBranch = "";
|
|
178
|
+
await workflowState.save();
|
|
179
|
+
|
|
180
|
+
return textResponse(
|
|
181
|
+
`✅ Feature/Bug marked as fixed!\n\n⚠️ CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ⏳ Create tests for: ${args.summary}\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function handleCreateTests(workflowState) {
|
|
186
|
+
if (!workflowState.state.bugFixed) {
|
|
187
|
+
return textResponse("⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
workflowState.state.testsCreated = true;
|
|
191
|
+
workflowState.state.testsSkipped = false;
|
|
192
|
+
workflowState.state.testsSkippedReason = "";
|
|
193
|
+
workflowState.state.readyCheckCompleted = false;
|
|
194
|
+
workflowState.state.released = false;
|
|
195
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
196
|
+
workflowState.state.lastCommitMessage = "";
|
|
197
|
+
workflowState.state.lastPushBranch = "";
|
|
198
|
+
await workflowState.save();
|
|
199
|
+
|
|
200
|
+
return textResponse(
|
|
201
|
+
"✅ Tests recorded!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function handleSkipTests(args, workflowState) {
|
|
206
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
207
|
+
return textResponse("⚠️ No active task. Use 'start_task' before skipping tests.");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!workflowState.state.bugFixed) {
|
|
211
|
+
return textResponse("⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
215
|
+
|
|
216
|
+
if (!reason) {
|
|
217
|
+
return textResponse("⚠️ Provide a non-empty justification when skipping tests.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
workflowState.state.testsCreated = true;
|
|
221
|
+
workflowState.state.testsPassed = true;
|
|
222
|
+
workflowState.state.testsSkipped = true;
|
|
223
|
+
workflowState.state.testsSkippedReason = reason;
|
|
224
|
+
workflowState.state.currentPhase = "documentation";
|
|
225
|
+
workflowState.state.readyToCommit = false;
|
|
226
|
+
workflowState.state.readyCheckCompleted = false;
|
|
227
|
+
workflowState.state.released = false;
|
|
228
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
229
|
+
workflowState.state.lastCommitMessage = "";
|
|
230
|
+
workflowState.state.lastPushBranch = "";
|
|
231
|
+
await workflowState.save();
|
|
232
|
+
|
|
233
|
+
return textResponse(
|
|
234
|
+
`⚠️ Tests skipped for this task.
|
|
235
|
+
Reason: ${reason}
|
|
236
|
+
|
|
237
|
+
Manual verification is now required.
|
|
238
|
+
|
|
239
|
+
Next Steps:
|
|
240
|
+
1. ✓ Fix/implement feature
|
|
241
|
+
2. ⚠️ Tests skipped (ensure manual QA)
|
|
242
|
+
3. ⏳ Create/update documentation
|
|
243
|
+
4. ⏳ Run 'check_ready_to_commit'
|
|
244
|
+
5. ⏳ Run 'commit_and_push'
|
|
245
|
+
6. ⏳ Run 'perform_release'
|
|
246
|
+
7. ⏳ Complete task`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function handleRunTests(args, workflowState) {
|
|
251
|
+
if (!workflowState.state.bugFixed) {
|
|
252
|
+
return textResponse("⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!workflowState.state.testsCreated) {
|
|
256
|
+
return textResponse("⚠️ Please create tests first using 'create_tests' before recording test results!");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
typeof args.passed !== "boolean" ||
|
|
261
|
+
typeof args.testCommand !== "string" ||
|
|
262
|
+
args.testCommand.trim() === ""
|
|
263
|
+
) {
|
|
264
|
+
return textResponse(
|
|
265
|
+
"⚠️ Please provide both 'passed' (boolean) and 'testCommand' (non-empty string) when recording test results."
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
workflowState.state.testsPassed = args.passed;
|
|
270
|
+
workflowState.state.testCommand = args.testCommand;
|
|
271
|
+
workflowState.state.testDetails = typeof args.details === "string" ? args.details : "";
|
|
272
|
+
workflowState.state.testsSkipped = false;
|
|
273
|
+
workflowState.state.testsSkippedReason = "";
|
|
274
|
+
workflowState.state.readyCheckCompleted = false;
|
|
275
|
+
workflowState.state.released = false;
|
|
276
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
277
|
+
workflowState.state.lastCommitMessage = "";
|
|
278
|
+
workflowState.state.lastPushBranch = "";
|
|
279
|
+
|
|
280
|
+
if (!args.passed) {
|
|
281
|
+
workflowState.state.currentPhase = "testing";
|
|
282
|
+
await workflowState.save();
|
|
283
|
+
|
|
284
|
+
return textResponse(
|
|
285
|
+
`❌ TESTS FAILED!\n\n🚫 STOP! DO NOT COMMIT OR PUSH!\n\nYou must:\n1. Fix the failing tests\n2. Run tests again until they pass\n\nTest command: ${args.testCommand}\n\nNever skip or ignore failing tests!`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
workflowState.state.currentPhase = "documentation";
|
|
290
|
+
await workflowState.save();
|
|
291
|
+
|
|
292
|
+
return textResponse(
|
|
293
|
+
`✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ⏳ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task\n\nReminder: Document what you did before committing!`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function handleCreateDocumentation(args, workflowState) {
|
|
298
|
+
if (!workflowState.state.testsPassed) {
|
|
299
|
+
return textResponse("⚠️ Please ensure tests are passing first! Run 'run_tests' with passed=true.");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
workflowState.state.documentationCreated = true;
|
|
303
|
+
workflowState.state.documentationType = args.documentationType;
|
|
304
|
+
workflowState.state.documentationSummary = args.summary;
|
|
305
|
+
workflowState.state.currentPhase = "ready";
|
|
306
|
+
workflowState.state.readyToCommit = true;
|
|
307
|
+
workflowState.state.readyCheckCompleted = false;
|
|
308
|
+
workflowState.state.released = false;
|
|
309
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
310
|
+
workflowState.state.lastCommitMessage = "";
|
|
311
|
+
workflowState.state.lastPushBranch = "";
|
|
312
|
+
await workflowState.save();
|
|
313
|
+
|
|
314
|
+
return textResponse(
|
|
315
|
+
`✅ Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n\n🎉 You're ready to verify your work!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ✓ Create documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Mark as complete with 'complete_task'`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function handleReadyCheck(workflowState) {
|
|
320
|
+
const status = workflowState.state;
|
|
321
|
+
const testsCreatedDone = status.testsSkipped || status.testsCreated;
|
|
322
|
+
const testsPassedDone = status.testsSkipped || status.testsPassed;
|
|
323
|
+
const checks = [
|
|
324
|
+
{ name: "Task started", done: status.currentPhase !== "idle" },
|
|
325
|
+
{ name: "Feature/bug fixed", done: status.bugFixed },
|
|
326
|
+
{
|
|
327
|
+
name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
|
|
328
|
+
done: testsCreatedDone,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
|
|
332
|
+
done: testsPassedDone,
|
|
333
|
+
},
|
|
334
|
+
{ name: "Documentation created", done: status.documentationCreated },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const allDone = checks.every((c) => c.done);
|
|
338
|
+
const checkList = formatChecklist(checks);
|
|
339
|
+
|
|
340
|
+
workflowState.state.readyCheckCompleted = allDone;
|
|
341
|
+
if (allDone) {
|
|
342
|
+
workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
|
|
343
|
+
? workflowState.state.released
|
|
344
|
+
? "ready_to_complete"
|
|
345
|
+
: "release"
|
|
346
|
+
: "commit";
|
|
347
|
+
}
|
|
348
|
+
await workflowState.save();
|
|
349
|
+
|
|
350
|
+
if (allDone) {
|
|
351
|
+
return textResponse(
|
|
352
|
+
`🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' (commits and pushes your changes)\n2. Run 'perform_release' (handles versioning, tags, and final push)\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to 'commit_and_push' to push to a non-default branch.\n\nTask: ${status.taskDescription}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleCommitAndPush(args, context) {
|
|
360
|
+
const { workflowState, exec, git, utils } = context;
|
|
361
|
+
if (!workflowState.state.readyToCommit) {
|
|
362
|
+
return textResponse("⚠️ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
366
|
+
return textResponse(
|
|
367
|
+
"⚠️ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const providedCommitMessage =
|
|
372
|
+
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
373
|
+
|
|
374
|
+
const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
|
|
375
|
+
const currentBranch = await git.getCurrentBranch();
|
|
376
|
+
const branchForPush = requestedBranch || currentBranch || "";
|
|
377
|
+
|
|
378
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
379
|
+
|
|
380
|
+
if (!hasWorkingChanges) {
|
|
381
|
+
const lastCommitMessage = await git.getLastCommitMessage();
|
|
382
|
+
const effectiveCommitMessage = lastCommitMessage || providedCommitMessage;
|
|
383
|
+
|
|
384
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
385
|
+
workflowState.state.lastCommitMessage = effectiveCommitMessage || "";
|
|
386
|
+
workflowState.state.lastPushBranch = branchForPush;
|
|
387
|
+
workflowState.state.currentPhase = workflowState.state.released ? "ready_to_complete" : "release";
|
|
388
|
+
await workflowState.save();
|
|
389
|
+
|
|
390
|
+
return textResponse(
|
|
391
|
+
`ℹ️ No changes detected. Assuming commit and push already completed.\nLast commit: ${
|
|
392
|
+
effectiveCommitMessage || "(unavailable)"
|
|
393
|
+
}\n\nNext: Run 'perform_release' to handle the release and push tags.`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!(await git.hasTestChanges()) && !workflowState.state.testsSkipped) {
|
|
398
|
+
return textResponse(
|
|
399
|
+
"⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await exec("git add .");
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return textResponse(
|
|
407
|
+
`❌ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const stagedChanges = await git.getStagedChanges();
|
|
412
|
+
const { summary: generatedSummary, body: commitBody } = utils.createCommitMessageParts(
|
|
413
|
+
stagedChanges,
|
|
414
|
+
providedCommitMessage
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (!generatedSummary) {
|
|
418
|
+
return textResponse("⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
if (commitBody) {
|
|
423
|
+
await exec(
|
|
424
|
+
`git commit -m ${utils.shellEscape(generatedSummary)} -m ${utils.shellEscape(commitBody)}`
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
await exec(`git commit -m ${utils.shellEscape(generatedSummary)}`);
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const output = error.stderr || error.stdout || error.message || "Unknown error";
|
|
431
|
+
if (output.includes("nothing to commit")) {
|
|
432
|
+
return textResponse(
|
|
433
|
+
"⚠️ Nothing to commit. Make additional changes before running 'commit_and_push'."
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return textResponse(`❌ git commit failed:\n\n${output}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const pushCommand = branchForPush
|
|
442
|
+
? `git push origin ${utils.shellEscape(branchForPush)}`
|
|
443
|
+
: "git push";
|
|
444
|
+
await exec(pushCommand);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return textResponse(
|
|
447
|
+
`❌ git push failed:\n\n${error.stderr || error.stdout || error.message}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
452
|
+
workflowState.state.lastCommitMessage = generatedSummary;
|
|
453
|
+
workflowState.state.lastPushBranch = branchForPush;
|
|
454
|
+
workflowState.state.currentPhase = workflowState.state.released ? "ready_to_complete" : "release";
|
|
455
|
+
await workflowState.save();
|
|
456
|
+
|
|
457
|
+
return textResponse(
|
|
458
|
+
`✅ Commit and push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${
|
|
459
|
+
branchForPush || "(default upstream)"
|
|
460
|
+
}\n\nNext: Run 'perform_release' to handle the release and push tags.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function handlePerformRelease(args, context) {
|
|
465
|
+
const { workflowState, exec, git, utils } = context;
|
|
466
|
+
|
|
467
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
468
|
+
return textResponse("⚠️ Please start a task first using 'start_task'!");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (await resetToCommitIfWorkingChanges(workflowState, git)) {
|
|
472
|
+
return textResponse(
|
|
473
|
+
"⚠️ Detected new changes after the last commit. Please run 'commit_and_push' again before recording a release."
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
478
|
+
return textResponse(
|
|
479
|
+
"⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!"
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let autoDetectedCommit = false;
|
|
484
|
+
|
|
485
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
486
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
487
|
+
|
|
488
|
+
if (hasWorkingChanges) {
|
|
489
|
+
return textResponse(
|
|
490
|
+
"⚠️ Please run 'commit_and_push' after the ready check before recording a release!"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const lastCommitMessage = await git.getLastCommitMessage();
|
|
495
|
+
const currentBranch = await git.getCurrentBranch();
|
|
496
|
+
|
|
497
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
498
|
+
workflowState.state.lastCommitMessage =
|
|
499
|
+
lastCommitMessage || workflowState.state.lastCommitMessage || "";
|
|
500
|
+
workflowState.state.lastPushBranch = currentBranch || workflowState.state.lastPushBranch || "";
|
|
501
|
+
workflowState.state.currentPhase = "release";
|
|
502
|
+
autoDetectedCommit = true;
|
|
503
|
+
await workflowState.save();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const rawReleaseCommand = typeof args.command === "string" ? args.command.trim() : "";
|
|
507
|
+
const releaseTypeArg =
|
|
508
|
+
typeof args.releaseType === "string" ? args.releaseType.trim().toLowerCase() : "";
|
|
509
|
+
const presetArg = typeof args.preset === "string" ? args.preset.trim().toLowerCase() : "";
|
|
510
|
+
const recognizedTypes = new Set(["major", "minor", "patch"]);
|
|
511
|
+
|
|
512
|
+
const tokens = rawReleaseCommand
|
|
513
|
+
.toLowerCase()
|
|
514
|
+
.split(/\s+/)
|
|
515
|
+
.map((token) => token.trim())
|
|
516
|
+
.filter(Boolean);
|
|
517
|
+
|
|
518
|
+
let releaseType = "";
|
|
519
|
+
if (recognizedTypes.has(releaseTypeArg)) {
|
|
520
|
+
releaseType = releaseTypeArg;
|
|
521
|
+
} else if (recognizedTypes.has(presetArg)) {
|
|
522
|
+
releaseType = presetArg;
|
|
523
|
+
} else if (tokens.length > 0) {
|
|
524
|
+
const possibleType = tokens[tokens.length - 1];
|
|
525
|
+
if (recognizedTypes.has(possibleType)) {
|
|
526
|
+
releaseType = possibleType;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let releaseCommand = rawReleaseCommand;
|
|
531
|
+
if (releaseCommand && recognizedTypes.has(releaseCommand.toLowerCase())) {
|
|
532
|
+
releaseType = releaseCommand.toLowerCase();
|
|
533
|
+
releaseCommand = "";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const scriptTypeMatch = /release:(major|minor|patch)/i.exec(releaseCommand);
|
|
537
|
+
if (scriptTypeMatch) {
|
|
538
|
+
releaseType = scriptTypeMatch[1].toLowerCase();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!releaseCommand && releaseType) {
|
|
542
|
+
releaseCommand = `npm run release:${releaseType}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!releaseCommand) {
|
|
546
|
+
return textResponse("⚠️ Please provide a non-empty 'command' when using 'perform_release'.");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let statusOutput;
|
|
550
|
+
try {
|
|
551
|
+
const { stdout } = await exec("git status --porcelain");
|
|
552
|
+
statusOutput = stdout;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
const details = error.stderr || error.stdout || error.message || "Unknown error";
|
|
555
|
+
return textResponse(
|
|
556
|
+
`⚠️ Unable to verify Git status. Refusing to run the release command until the repository state can be checked.\n\nDetails:\n${details}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const statusSummary = git.workingTreeSummary(statusOutput);
|
|
561
|
+
if (statusSummary.hasChanges) {
|
|
562
|
+
const changeDetails =
|
|
563
|
+
statusSummary.lines.length > 0
|
|
564
|
+
? statusSummary.lines.map((line) => `• ${line}`).join("\n")
|
|
565
|
+
: "• (unable to list changes)";
|
|
566
|
+
|
|
567
|
+
return textResponse(
|
|
568
|
+
`⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.\n\nDetected changes:\n${changeDetails}`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const lastCommitMessage = await git.getLastCommitMessage();
|
|
573
|
+
if (!releaseType) {
|
|
574
|
+
releaseType = utils.determineReleaseTypeFromCommit(
|
|
575
|
+
lastCommitMessage || workflowState.state.lastCommitMessage
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const finalCommand =
|
|
580
|
+
releaseCommand.includes("--release-as") || /release:(major|minor|patch)/i.test(releaseCommand)
|
|
581
|
+
? releaseCommand
|
|
582
|
+
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
await exec(finalCommand);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return textResponse(
|
|
588
|
+
`❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const branchForPush = workflowState.state.lastPushBranch || (await git.getCurrentBranch()) || "";
|
|
593
|
+
const pushCommand = branchForPush
|
|
594
|
+
? `git push --follow-tags origin ${utils.shellEscape(branchForPush)}`
|
|
595
|
+
: "git push --follow-tags";
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
await exec(pushCommand);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
return textResponse(
|
|
601
|
+
`❌ git push (with tags) failed:\n\n${error.stderr || error.stdout || error.message}`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
workflowState.state.released = true;
|
|
606
|
+
workflowState.state.releaseCommand = finalCommand;
|
|
607
|
+
workflowState.state.releaseNotes = typeof args.notes === "string" ? args.notes : "";
|
|
608
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
609
|
+
await workflowState.save();
|
|
610
|
+
|
|
611
|
+
const message = summarizeHistory(workflowState.state.history, args.limit || 10);
|
|
612
|
+
if (!message) {
|
|
613
|
+
const autoNote = autoDetectedCommit
|
|
614
|
+
? "\n\nℹ️ Detected clean working tree and treated commit/push as already completed."
|
|
615
|
+
: "";
|
|
616
|
+
return textResponse(`📜 No workflow history yet.${autoNote}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const autoNote = autoDetectedCommit
|
|
620
|
+
? "\n\nℹ️ Detected clean working tree and treated commit/push as already completed."
|
|
621
|
+
: "";
|
|
622
|
+
return textResponse(`${message}${autoNote}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function handleCompleteTask(args, workflowState) {
|
|
626
|
+
const entry = {
|
|
627
|
+
taskDescription: workflowState.state.taskDescription,
|
|
628
|
+
taskType: workflowState.state.taskType,
|
|
629
|
+
commitMessage: args.commitMessage,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
workflowState.addToHistory(entry);
|
|
633
|
+
workflowState.reset();
|
|
634
|
+
await workflowState.save();
|
|
635
|
+
|
|
636
|
+
return textResponse(
|
|
637
|
+
`✅ Task completed!\n\nCommit message: ${args.commitMessage}\nHistory recorded.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function handleDropTask(args, workflowState) {
|
|
642
|
+
workflowState.addToHistory({
|
|
643
|
+
taskDescription: workflowState.state.taskDescription,
|
|
644
|
+
taskType: workflowState.state.taskType,
|
|
645
|
+
commitMessage: workflowState.state.lastCommitMessage,
|
|
646
|
+
dropped: true,
|
|
647
|
+
dropReason: typeof args.reason === "string" ? args.reason : "",
|
|
648
|
+
});
|
|
649
|
+
workflowState.reset();
|
|
650
|
+
await workflowState.save();
|
|
651
|
+
|
|
652
|
+
return textResponse("⚠️ Task dropped. Workflow state reset.");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function handleGetWorkflowStatus(workflowState) {
|
|
656
|
+
const status = workflowState.state;
|
|
657
|
+
const nextStep = status ? status.currentPhase : "idle";
|
|
658
|
+
|
|
659
|
+
return textResponse(
|
|
660
|
+
`📊 Current Workflow Status:
|
|
661
|
+
|
|
662
|
+
Phase: ${status.currentPhase}
|
|
663
|
+
Task: ${status.taskDescription || "(none)"}
|
|
664
|
+
Bug fixed: ${status.bugFixed ? "✅" : "❌"}
|
|
665
|
+
Tests created: ${status.testsCreated ? "✅" : "❌"}
|
|
666
|
+
Tests passed: ${status.testsPassed ? "✅" : "❌"}
|
|
667
|
+
Documentation: ${status.documentationCreated ? "✅" : "❌"}
|
|
668
|
+
Ready check: ${status.readyCheckCompleted ? "✅" : "❌"}
|
|
669
|
+
Commit & push: ${status.commitAndPushCompleted ? "✅" : "❌"}
|
|
670
|
+
Released: ${status.released ? "✅" : "❌"}
|
|
671
|
+
|
|
672
|
+
Next step: ${nextStep}`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function handleViewHistory(args, workflowState) {
|
|
677
|
+
const message = summarizeHistory(workflowState.state.history, args.limit || 10);
|
|
678
|
+
if (!message) {
|
|
679
|
+
return textResponse("📜 No workflow history yet.");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return textResponse(message);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export async function handleToolCall({
|
|
686
|
+
request,
|
|
687
|
+
normalizeRequestArgs,
|
|
688
|
+
workflowState,
|
|
689
|
+
exec,
|
|
690
|
+
git,
|
|
691
|
+
utils,
|
|
692
|
+
}) {
|
|
693
|
+
try {
|
|
694
|
+
const { name } = request.params;
|
|
695
|
+
const { args, error: argumentsError } = normalizeRequestArgs(request.params.arguments);
|
|
696
|
+
|
|
697
|
+
if (argumentsError) {
|
|
698
|
+
return textResponse(argumentsError);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
switch (name) {
|
|
702
|
+
case "start_task":
|
|
703
|
+
return handleStartTask(args, workflowState);
|
|
704
|
+
case "mark_bug_fixed":
|
|
705
|
+
return handleMarkBugFixed(args, workflowState);
|
|
706
|
+
case "create_tests":
|
|
707
|
+
return handleCreateTests(workflowState);
|
|
708
|
+
case "skip_tests":
|
|
709
|
+
return handleSkipTests(args, workflowState);
|
|
710
|
+
case "run_tests":
|
|
711
|
+
return handleRunTests(args, workflowState);
|
|
712
|
+
case "create_documentation":
|
|
713
|
+
return handleCreateDocumentation(args, workflowState);
|
|
714
|
+
case "check_ready_to_commit":
|
|
715
|
+
return handleReadyCheck(workflowState);
|
|
716
|
+
case "commit_and_push":
|
|
717
|
+
return handleCommitAndPush(args, { workflowState, exec, git, utils });
|
|
718
|
+
case "perform_release":
|
|
719
|
+
return handlePerformRelease(args, { workflowState, exec, git, utils });
|
|
720
|
+
case "complete_task":
|
|
721
|
+
return handleCompleteTask(args, workflowState);
|
|
722
|
+
case "drop_task":
|
|
723
|
+
return handleDropTask(args, workflowState);
|
|
724
|
+
case "get_workflow_status":
|
|
725
|
+
return handleGetWorkflowStatus(workflowState);
|
|
726
|
+
case "view_history":
|
|
727
|
+
return handleViewHistory(args, workflowState);
|
|
728
|
+
case "continue_workflow":
|
|
729
|
+
return handleContinueWorkflow(workflowState, { workflowState, exec, git, utils });
|
|
730
|
+
default:
|
|
731
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
console.error("Tool handler error:", error);
|
|
735
|
+
return textResponse(
|
|
736
|
+
"⚠️ Internal server error while processing the tool request. Please try again."
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|