@programinglive/dev-workflow-mcp-server 1.0.2 → 1.0.7
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 +40 -0
- package/README.md +94 -11
- package/index.js +766 -11
- package/package.json +1 -1
- package/tests/workflowState.test.js +127 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.0.7](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.6...v1.0.7) (2025-10-20)
|
|
6
|
+
|
|
7
|
+
### [1.0.6](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.5...v1.0.6) (2025-10-20)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ✅ Tests
|
|
11
|
+
|
|
12
|
+
* enforce clean release workflow ([c261a7b](https://github.com/programinglive/dev-workflow-mcp-server/commit/c261a7b3d141ba4472567bd47e4f6b6e4d2ee3fd))
|
|
13
|
+
|
|
14
|
+
### [1.0.5](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.4...v1.0.5) (2025-10-20)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### ✅ Tests
|
|
18
|
+
|
|
19
|
+
* update index.js and tests/workflowState.test.js ([b9df3c8](https://github.com/programinglive/dev-workflow-mcp-server/commit/b9df3c8643303769dbe4c5a0992995b6328cdfcf))
|
|
20
|
+
|
|
21
|
+
### [1.0.4](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.3...v1.0.4) (2025-10-20)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### ✨ Features
|
|
25
|
+
|
|
26
|
+
* enforce release step after ready check ([0725496](https://github.com/programinglive/dev-workflow-mcp-server/commit/07254967becd515cb2a4261d62af01dfeb842175))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### 🐛 Bug Fixes
|
|
30
|
+
|
|
31
|
+
* support windows-friendly commit quoting ([47881fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/47881fd0db54da3fabe368b1a135902e47ef873e))
|
|
32
|
+
|
|
33
|
+
### [1.0.3](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.2...v1.0.3) (2025-10-20)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### 🧹 Chores
|
|
37
|
+
|
|
38
|
+
* add gitignore ([5f695fb](https://github.com/programinglive/dev-workflow-mcp-server/commit/5f695fbc71515c7f1ffbc44fd74b52314c966ee6))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### 📝 Documentation
|
|
42
|
+
|
|
43
|
+
* clarify cross-platform installation ([52b5ee7](https://github.com/programinglive/dev-workflow-mcp-server/commit/52b5ee7061e8f968ff94e6c226586fc1b7e2a057))
|
|
44
|
+
|
|
5
45
|
### [1.0.2](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.1...v1.0.2) (2025-10-19)
|
|
6
46
|
|
|
7
47
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ This MCP server guides you through a disciplined development workflow:
|
|
|
11
11
|
3. **Create Tests** - Always test your changes
|
|
12
12
|
4. **Run Tests** - Tests must pass (GREEN)
|
|
13
13
|
5. **Document** - Update documentation
|
|
14
|
-
6. **Commit & Push** -
|
|
14
|
+
6. **Commit & Push** - Let the server drive the git commands once verification passes
|
|
15
|
+
7. **Release** - Record the release details before closing out the task
|
|
15
16
|
|
|
16
17
|
## 🚀 Installation
|
|
17
18
|
|
|
@@ -23,34 +24,94 @@ npm install
|
|
|
23
24
|
|
|
24
25
|
### 2. Configure in Windsurf/Claude Desktop
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Point your MCP client to the server entry point. Replace `<PROJECT_ROOT>` with the absolute path to this repository on your machine.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
#### macOS
|
|
30
|
+
|
|
31
|
+
- **Windsurf** (`~/Library/Application Support/Windsurf/config.json`):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"dev-workflow": {
|
|
37
|
+
"command": "node",
|
|
38
|
+
"args": ["<PROJECT_ROOT>/index.js"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"dev-workflow": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["<PROJECT_ROOT>/index.js"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Windows
|
|
58
|
+
|
|
59
|
+
- **Windsurf** (`%APPDATA%\Windsurf\config.json`):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"dev-workflow": {
|
|
65
|
+
"command": "node",
|
|
66
|
+
"args": ["<PROJECT_ROOT>\\index.js"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Claude Desktop** (`%APPDATA%\Claude\claude_desktop_config.json`):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"dev-workflow": {
|
|
78
|
+
"command": "node",
|
|
79
|
+
"args": ["<PROJECT_ROOT>\\index.js"]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Linux
|
|
86
|
+
|
|
87
|
+
- **Windsurf** (`~/.config/windsurf/config.json`):
|
|
29
88
|
|
|
30
89
|
```json
|
|
31
90
|
{
|
|
32
91
|
"mcpServers": {
|
|
33
92
|
"dev-workflow": {
|
|
34
93
|
"command": "node",
|
|
35
|
-
"args": ["
|
|
94
|
+
"args": ["<PROJECT_ROOT>/index.js"]
|
|
36
95
|
}
|
|
37
96
|
}
|
|
38
97
|
}
|
|
39
98
|
```
|
|
40
99
|
|
|
41
|
-
**
|
|
100
|
+
- **Claude Desktop** (`~/.config/claude/claude_desktop_config.json`):
|
|
42
101
|
|
|
43
102
|
```json
|
|
44
103
|
{
|
|
45
104
|
"mcpServers": {
|
|
46
105
|
"dev-workflow": {
|
|
47
106
|
"command": "node",
|
|
48
|
-
"args": ["
|
|
107
|
+
"args": ["<PROJECT_ROOT>/index.js"]
|
|
49
108
|
}
|
|
50
109
|
}
|
|
51
110
|
}
|
|
52
111
|
```
|
|
53
112
|
|
|
113
|
+
> **Note:** On Windows paths in JSON require escaped backslashes (e.g., `"C:\\path\\to\\project"`).
|
|
114
|
+
|
|
54
115
|
### 3. Restart Windsurf/Claude Desktop
|
|
55
116
|
|
|
56
117
|
After adding the configuration, restart the application to load the MCP server.
|
|
@@ -92,6 +153,11 @@ Mark that the bug/feature is fixed. **Reminder: Now you MUST create tests!**
|
|
|
92
153
|
**Parameters:**
|
|
93
154
|
- `summary` (string, required): Brief summary of what was fixed/implemented
|
|
94
155
|
|
|
156
|
+
### `create_tests`
|
|
157
|
+
Confirm that you've created the necessary tests covering your change. Required before recording test results.
|
|
158
|
+
|
|
159
|
+
**Parameters:** _none_
|
|
160
|
+
|
|
95
161
|
### `run_tests`
|
|
96
162
|
Record test results. **NEVER commit if tests fail!** Only proceed if all tests are green.
|
|
97
163
|
|
|
@@ -118,6 +184,20 @@ Mark that documentation has been created/updated. This is required before commit
|
|
|
118
184
|
### `check_ready_to_commit`
|
|
119
185
|
Check if all workflow steps are completed and you're ready to commit & push.
|
|
120
186
|
|
|
187
|
+
### `commit_and_push`
|
|
188
|
+
Automatically run `git add`, `git commit`, and `git push` after the ready check passes.
|
|
189
|
+
|
|
190
|
+
**Parameters:**
|
|
191
|
+
- `commitMessage` (string, required): Conventional commit message to use
|
|
192
|
+
- `branch` (string, optional): Target branch to push (defaults to current branch)
|
|
193
|
+
|
|
194
|
+
### `perform_release`
|
|
195
|
+
Record the release after you've committed and pushed. Required before you can complete the task.
|
|
196
|
+
|
|
197
|
+
**Parameters:**
|
|
198
|
+
- `command` (string, required): Release command that was executed (e.g., `npm run release`)
|
|
199
|
+
- `notes` (string, optional): Additional release notes
|
|
200
|
+
|
|
121
201
|
### `complete_task`
|
|
122
202
|
Mark the task as complete after successful commit & push. Resets workflow for next task.
|
|
123
203
|
|
|
@@ -180,13 +260,16 @@ Here's how you'd use this MCP server in a typical coding session:
|
|
|
180
260
|
```
|
|
181
261
|
|
|
182
262
|
8. **Commit & Push:**
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
263
|
+
```
|
|
264
|
+
"Commit and push: commitMessage='feat: add user profile page with tests and docs'"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
9. **Record release:**
|
|
268
|
+
```
|
|
269
|
+
"Record release: command='npm run release', notes='v1.2.3'"
|
|
187
270
|
```
|
|
188
271
|
|
|
189
|
-
|
|
272
|
+
10. **Complete:**
|
|
190
273
|
```
|
|
191
274
|
"Complete the task with commit message: 'feat: add user profile page'"
|
|
192
275
|
```
|
package/index.js
CHANGED
|
@@ -25,9 +25,17 @@ export class WorkflowState {
|
|
|
25
25
|
currentPhase: "idle",
|
|
26
26
|
taskDescription: "",
|
|
27
27
|
bugFixed: false,
|
|
28
|
+
testsCreated: false,
|
|
28
29
|
testsPassed: false,
|
|
29
30
|
documentationCreated: false,
|
|
30
31
|
readyToCommit: false,
|
|
32
|
+
readyCheckCompleted: false,
|
|
33
|
+
released: false,
|
|
34
|
+
releaseCommand: "",
|
|
35
|
+
releaseNotes: "",
|
|
36
|
+
commitAndPushCompleted: false,
|
|
37
|
+
lastCommitMessage: "",
|
|
38
|
+
lastPushBranch: "",
|
|
31
39
|
history: [],
|
|
32
40
|
};
|
|
33
41
|
}
|
|
@@ -35,7 +43,35 @@ export class WorkflowState {
|
|
|
35
43
|
async load() {
|
|
36
44
|
try {
|
|
37
45
|
const data = await fs.readFile(this.stateFile, "utf-8");
|
|
38
|
-
|
|
46
|
+
const parsed = JSON.parse(data);
|
|
47
|
+
this.state = {
|
|
48
|
+
...this.state,
|
|
49
|
+
...parsed,
|
|
50
|
+
};
|
|
51
|
+
if (typeof this.state.testsCreated !== "boolean") {
|
|
52
|
+
this.state.testsCreated = false;
|
|
53
|
+
}
|
|
54
|
+
if (typeof this.state.readyCheckCompleted !== "boolean") {
|
|
55
|
+
this.state.readyCheckCompleted = false;
|
|
56
|
+
}
|
|
57
|
+
if (typeof this.state.released !== "boolean") {
|
|
58
|
+
this.state.released = false;
|
|
59
|
+
}
|
|
60
|
+
if (typeof this.state.commitAndPushCompleted !== "boolean") {
|
|
61
|
+
this.state.commitAndPushCompleted = false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof this.state.releaseCommand !== "string") {
|
|
64
|
+
this.state.releaseCommand = "";
|
|
65
|
+
}
|
|
66
|
+
if (typeof this.state.releaseNotes !== "string") {
|
|
67
|
+
this.state.releaseNotes = "";
|
|
68
|
+
}
|
|
69
|
+
if (typeof this.state.lastCommitMessage !== "string") {
|
|
70
|
+
this.state.lastCommitMessage = "";
|
|
71
|
+
}
|
|
72
|
+
if (typeof this.state.lastPushBranch !== "string") {
|
|
73
|
+
this.state.lastPushBranch = "";
|
|
74
|
+
}
|
|
39
75
|
} catch (error) {
|
|
40
76
|
// File doesn't exist yet, use default state
|
|
41
77
|
}
|
|
@@ -50,9 +86,17 @@ export class WorkflowState {
|
|
|
50
86
|
currentPhase: "idle",
|
|
51
87
|
taskDescription: "",
|
|
52
88
|
bugFixed: false,
|
|
89
|
+
testsCreated: false,
|
|
53
90
|
testsPassed: false,
|
|
54
91
|
documentationCreated: false,
|
|
55
92
|
readyToCommit: false,
|
|
93
|
+
readyCheckCompleted: false,
|
|
94
|
+
released: false,
|
|
95
|
+
releaseCommand: "",
|
|
96
|
+
releaseNotes: "",
|
|
97
|
+
commitAndPushCompleted: false,
|
|
98
|
+
lastCommitMessage: "",
|
|
99
|
+
lastPushBranch: "",
|
|
56
100
|
history: this.state.history,
|
|
57
101
|
};
|
|
58
102
|
}
|
|
@@ -70,6 +114,18 @@ await workflowState.load();
|
|
|
70
114
|
|
|
71
115
|
const exec = promisify(execCallback);
|
|
72
116
|
|
|
117
|
+
const isWindows = process.platform === "win32";
|
|
118
|
+
|
|
119
|
+
function shellEscape(value) {
|
|
120
|
+
const stringValue = String(value);
|
|
121
|
+
if (isWindows) {
|
|
122
|
+
// Use PowerShell-friendly single quotes, escape embedded single quotes by doubling
|
|
123
|
+
return `'${stringValue.replace(/'/g, "''")}'`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `'${stringValue.replace(/'/g, "'\\''")}'`;
|
|
127
|
+
}
|
|
128
|
+
|
|
73
129
|
function normalizeRequestArgs(rawArgs) {
|
|
74
130
|
if (rawArgs === undefined || rawArgs === null) {
|
|
75
131
|
return { args: {}, error: null };
|
|
@@ -121,6 +177,297 @@ async function hasStagedChanges() {
|
|
|
121
177
|
}
|
|
122
178
|
}
|
|
123
179
|
|
|
180
|
+
async function hasWorkingChanges() {
|
|
181
|
+
try {
|
|
182
|
+
const { stdout } = await exec("git status --porcelain");
|
|
183
|
+
return stdout.split("\n").some((line) => line && line.trim().length > 0);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function workingTreeSummary(statusOutput) {
|
|
190
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
191
|
+
return {
|
|
192
|
+
hasChanges: false,
|
|
193
|
+
lines: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const lines = statusOutput
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
hasChanges: lines.length > 0,
|
|
204
|
+
lines,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isTestFilePath(filePath) {
|
|
209
|
+
if (!filePath) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractPathFromStatusLine(line) {
|
|
221
|
+
if (!line || line.length < 4) {
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rawPath = line.slice(3).trim();
|
|
226
|
+
if (!rawPath) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (rawPath.includes(" -> ")) {
|
|
231
|
+
const parts = rawPath.split(" -> ");
|
|
232
|
+
return parts[parts.length - 1].trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return rawPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function containsTestFilesInStatus(statusOutput) {
|
|
239
|
+
if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return statusOutput
|
|
244
|
+
.split("\n")
|
|
245
|
+
.map((line) => extractPathFromStatusLine(line))
|
|
246
|
+
.some((path) => isTestFilePath(path));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isDocumentationFilePath(filePath) {
|
|
250
|
+
if (!filePath) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const normalized = filePath.toLowerCase();
|
|
255
|
+
return (
|
|
256
|
+
normalized.startsWith("docs/") ||
|
|
257
|
+
normalized.startsWith("documentation/") ||
|
|
258
|
+
normalized.endsWith(".md") ||
|
|
259
|
+
normalized.endsWith(".rst") ||
|
|
260
|
+
normalized.endsWith(".adoc") ||
|
|
261
|
+
normalized.includes("readme")
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseNameStatusLine(line) {
|
|
266
|
+
if (!line) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const parts = line.split("\t").map((part) => part.trim());
|
|
271
|
+
const status = parts[0];
|
|
272
|
+
if (!status) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
277
|
+
const originalPath = parts[1] || "";
|
|
278
|
+
const newPath = parts[2] || originalPath;
|
|
279
|
+
return {
|
|
280
|
+
status,
|
|
281
|
+
path: newPath,
|
|
282
|
+
originalPath,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
status,
|
|
288
|
+
path: parts[1] || "",
|
|
289
|
+
originalPath: "",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatFileList(paths) {
|
|
294
|
+
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
|
295
|
+
|
|
296
|
+
if (uniquePaths.length === 0) {
|
|
297
|
+
return "updates";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (uniquePaths.length === 1) {
|
|
301
|
+
return uniquePaths[0];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (uniquePaths.length === 2) {
|
|
305
|
+
return `${uniquePaths[0]} and ${uniquePaths[1]}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (uniquePaths.length === 3) {
|
|
309
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, and ${uniquePaths[2]}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const remaining = uniquePaths.length - 3;
|
|
313
|
+
return `${uniquePaths[0]}, ${uniquePaths[1]}, ${uniquePaths[2]} +${remaining} more`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function determineCommitType(changes) {
|
|
317
|
+
const hasTestChange = changes.some((change) =>
|
|
318
|
+
[change.path, change.originalPath].some((p) => isTestFilePath(p))
|
|
319
|
+
);
|
|
320
|
+
const hasDocsChange = changes.some((change) =>
|
|
321
|
+
[change.path, change.originalPath].some((p) => isDocumentationFilePath(p))
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (hasTestChange) {
|
|
325
|
+
return "test";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (hasDocsChange) {
|
|
329
|
+
return "docs";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return "chore";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function describeChange(change) {
|
|
336
|
+
const statusCode = change.status || "";
|
|
337
|
+
const path = change.path || change.originalPath || "";
|
|
338
|
+
|
|
339
|
+
if (statusCode.startsWith("R")) {
|
|
340
|
+
return `renamed ${change.originalPath} -> ${change.path}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (statusCode.startsWith("C")) {
|
|
344
|
+
return `copied ${change.originalPath} -> ${change.path}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
switch (statusCode[0]) {
|
|
348
|
+
case "A":
|
|
349
|
+
return `added ${path}`;
|
|
350
|
+
case "M":
|
|
351
|
+
return `modified ${path}`;
|
|
352
|
+
case "D":
|
|
353
|
+
return `removed ${path}`;
|
|
354
|
+
case "U":
|
|
355
|
+
return `updated ${path}`;
|
|
356
|
+
default:
|
|
357
|
+
return `${statusCode} ${path}`.trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function createCommitMessageParts(changes, providedSummary = "") {
|
|
362
|
+
const summaryInput = typeof providedSummary === "string" ? providedSummary.trim() : "";
|
|
363
|
+
|
|
364
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
365
|
+
return {
|
|
366
|
+
summary: summaryInput,
|
|
367
|
+
body: "",
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const commitType = determineCommitType(changes);
|
|
372
|
+
const summaryPaths = changes
|
|
373
|
+
.map((change) => change.path || change.originalPath)
|
|
374
|
+
.filter(Boolean);
|
|
375
|
+
const generatedSummary = `${commitType}: update ${formatFileList(summaryPaths)}`;
|
|
376
|
+
const summary = summaryInput || generatedSummary;
|
|
377
|
+
|
|
378
|
+
const detailLines = changes
|
|
379
|
+
.map((change) => describeChange(change))
|
|
380
|
+
.filter((line) => line && line.trim().length > 0)
|
|
381
|
+
.map((line) => `- ${line}`);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
summary,
|
|
385
|
+
body: detailLines.join("\n"),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function getStagedChanges() {
|
|
390
|
+
try {
|
|
391
|
+
const { stdout } = await exec("git diff --cached --name-status");
|
|
392
|
+
return stdout
|
|
393
|
+
.split("\n")
|
|
394
|
+
.map((line) => parseNameStatusLine(line))
|
|
395
|
+
.filter(Boolean);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function hasTestChanges() {
|
|
402
|
+
try {
|
|
403
|
+
const { stdout } = await exec("git status --porcelain");
|
|
404
|
+
return containsTestFilesInStatus(stdout);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function normalizeCommitHeader(message) {
|
|
411
|
+
if (typeof message !== "string") {
|
|
412
|
+
return "";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const header = message.split("\n")[0] || "";
|
|
416
|
+
return header.trim();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function containsBreakingChange(message) {
|
|
420
|
+
if (typeof message !== "string") {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return /breaking change/i.test(message);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function determineReleaseTypeFromCommit(message) {
|
|
428
|
+
const header = normalizeCommitHeader(message).toLowerCase();
|
|
429
|
+
const hasBreaking = containsBreakingChange(message) || /!/.test(header.split(":")[0] || "");
|
|
430
|
+
|
|
431
|
+
if (hasBreaking) {
|
|
432
|
+
return "major";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const typeMatch = header.match(/^([a-z]+)(\(|:)/i);
|
|
436
|
+
const commitType = typeMatch ? typeMatch[1] : "";
|
|
437
|
+
|
|
438
|
+
if (commitType === "feat") {
|
|
439
|
+
return "minor";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (commitType === "fix" || commitType === "perf") {
|
|
443
|
+
return "patch";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!commitType && containsBreakingChange(message)) {
|
|
447
|
+
return "major";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return "patch";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function getLastCommitMessage() {
|
|
454
|
+
try {
|
|
455
|
+
const { stdout } = await exec("git log -1 --pretty=%B");
|
|
456
|
+
return stdout.trim();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function getCurrentBranch() {
|
|
463
|
+
try {
|
|
464
|
+
const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
|
|
465
|
+
return stdout.trim();
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return "";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
124
471
|
// Create MCP server
|
|
125
472
|
export const server = new Server(
|
|
126
473
|
{
|
|
@@ -174,6 +521,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
174
521
|
required: ["summary"],
|
|
175
522
|
},
|
|
176
523
|
},
|
|
524
|
+
{
|
|
525
|
+
name: "create_tests",
|
|
526
|
+
description: "Mark that tests covering the change have been created.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
177
532
|
{
|
|
178
533
|
name: "run_tests",
|
|
179
534
|
description:
|
|
@@ -226,6 +581,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
226
581
|
properties: {},
|
|
227
582
|
},
|
|
228
583
|
},
|
|
584
|
+
{
|
|
585
|
+
name: "commit_and_push",
|
|
586
|
+
description:
|
|
587
|
+
"Automatically run git add, commit, and push once the ready check passes.",
|
|
588
|
+
inputSchema: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
commitMessage: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "Commit message to use",
|
|
594
|
+
},
|
|
595
|
+
branch: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description:
|
|
598
|
+
"Optional branch name to push to (defaults to current branch)",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
required: [],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "perform_release",
|
|
606
|
+
description:
|
|
607
|
+
"Record release details after committing and pushing your changes.",
|
|
608
|
+
inputSchema: {
|
|
609
|
+
type: "object",
|
|
610
|
+
properties: {
|
|
611
|
+
command: {
|
|
612
|
+
type: "string",
|
|
613
|
+
description: "Release command that was executed",
|
|
614
|
+
},
|
|
615
|
+
notes: {
|
|
616
|
+
type: "string",
|
|
617
|
+
description: "Optional release notes",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
required: ["command"],
|
|
621
|
+
},
|
|
622
|
+
},
|
|
229
623
|
{
|
|
230
624
|
name: "complete_task",
|
|
231
625
|
description:
|
|
@@ -291,13 +685,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
291
685
|
workflowState.state.currentPhase = "coding";
|
|
292
686
|
workflowState.state.taskDescription = args.description;
|
|
293
687
|
workflowState.state.taskType = args.type;
|
|
688
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
689
|
+
workflowState.state.lastCommitMessage = "";
|
|
690
|
+
workflowState.state.lastPushBranch = "";
|
|
294
691
|
await workflowState.save();
|
|
295
692
|
|
|
296
693
|
return {
|
|
297
694
|
content: [
|
|
298
695
|
{
|
|
299
696
|
type: "text",
|
|
300
|
-
text: `✅ 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. ⏳ Commit & push\n\nReminder: Focus on writing clean, maintainable code!`,
|
|
697
|
+
text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n8. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`,
|
|
301
698
|
},
|
|
302
699
|
],
|
|
303
700
|
};
|
|
@@ -316,15 +713,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
316
713
|
}
|
|
317
714
|
|
|
318
715
|
workflowState.state.bugFixed = true;
|
|
716
|
+
workflowState.state.testsCreated = false;
|
|
319
717
|
workflowState.state.currentPhase = "testing";
|
|
320
718
|
workflowState.state.fixSummary = args.summary;
|
|
719
|
+
workflowState.state.readyCheckCompleted = false;
|
|
720
|
+
workflowState.state.released = false;
|
|
721
|
+
workflowState.state.releaseCommand = "";
|
|
722
|
+
workflowState.state.releaseNotes = "";
|
|
723
|
+
workflowState.state.readyToCommit = false;
|
|
724
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
725
|
+
workflowState.state.lastCommitMessage = "";
|
|
726
|
+
workflowState.state.lastPushBranch = "";
|
|
321
727
|
await workflowState.save();
|
|
322
728
|
|
|
323
729
|
return {
|
|
324
730
|
content: [
|
|
325
731
|
{
|
|
326
732
|
type: "text",
|
|
327
|
-
text: `✅ 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. ⏳ Commit & push\n\n🚫 DO NOT SKIP TESTING!`,
|
|
733
|
+
text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`,
|
|
734
|
+
},
|
|
735
|
+
],
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
case "create_tests": {
|
|
740
|
+
if (!workflowState.state.bugFixed) {
|
|
741
|
+
return {
|
|
742
|
+
content: [
|
|
743
|
+
{
|
|
744
|
+
type: "text",
|
|
745
|
+
text: "⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
workflowState.state.testsCreated = true;
|
|
752
|
+
workflowState.state.readyCheckCompleted = false;
|
|
753
|
+
workflowState.state.released = false;
|
|
754
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
755
|
+
workflowState.state.lastCommitMessage = "";
|
|
756
|
+
workflowState.state.lastPushBranch = "";
|
|
757
|
+
await workflowState.save();
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
content: [
|
|
761
|
+
{
|
|
762
|
+
type: "text",
|
|
763
|
+
text: "✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'.",
|
|
328
764
|
},
|
|
329
765
|
],
|
|
330
766
|
};
|
|
@@ -342,6 +778,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
342
778
|
};
|
|
343
779
|
}
|
|
344
780
|
|
|
781
|
+
if (!workflowState.state.testsCreated) {
|
|
782
|
+
return {
|
|
783
|
+
content: [
|
|
784
|
+
{
|
|
785
|
+
type: "text",
|
|
786
|
+
text: "⚠️ Please create tests first using 'create_tests' before recording test results!",
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
345
792
|
if (
|
|
346
793
|
typeof args.passed !== "boolean" ||
|
|
347
794
|
typeof args.testCommand !== "string" ||
|
|
@@ -361,6 +808,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
361
808
|
workflowState.state.testCommand = args.testCommand;
|
|
362
809
|
workflowState.state.testDetails =
|
|
363
810
|
typeof args.details === "string" ? args.details : "";
|
|
811
|
+
workflowState.state.readyCheckCompleted = false;
|
|
812
|
+
workflowState.state.released = false;
|
|
813
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
814
|
+
workflowState.state.lastCommitMessage = "";
|
|
815
|
+
workflowState.state.lastPushBranch = "";
|
|
364
816
|
|
|
365
817
|
if (!args.passed) {
|
|
366
818
|
workflowState.state.currentPhase = "testing";
|
|
@@ -383,7 +835,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
383
835
|
content: [
|
|
384
836
|
{
|
|
385
837
|
type: "text",
|
|
386
|
-
text: `✅ 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. ⏳ Commit & push\n\nReminder: Document what you did before committing!`,
|
|
838
|
+
text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\nReminder: Document what you did before committing!`,
|
|
387
839
|
},
|
|
388
840
|
],
|
|
389
841
|
};
|
|
@@ -406,13 +858,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
406
858
|
workflowState.state.documentationSummary = args.summary;
|
|
407
859
|
workflowState.state.currentPhase = "ready";
|
|
408
860
|
workflowState.state.readyToCommit = true;
|
|
861
|
+
workflowState.state.readyCheckCompleted = false;
|
|
862
|
+
workflowState.state.released = false;
|
|
863
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
864
|
+
workflowState.state.lastCommitMessage = "";
|
|
865
|
+
workflowState.state.lastPushBranch = "";
|
|
409
866
|
await workflowState.save();
|
|
410
867
|
|
|
411
868
|
return {
|
|
412
869
|
content: [
|
|
413
870
|
{
|
|
414
871
|
type: "text",
|
|
415
|
-
text: `✅ Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n\n🎉 You're ready to
|
|
872
|
+
text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Mark as complete with 'complete_task'\n\nRemember: git add . && git commit && git push`,
|
|
416
873
|
},
|
|
417
874
|
],
|
|
418
875
|
};
|
|
@@ -423,6 +880,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
423
880
|
const checks = [
|
|
424
881
|
{ name: "Task started", done: status.currentPhase !== "idle" },
|
|
425
882
|
{ name: "Feature/bug fixed", done: status.bugFixed },
|
|
883
|
+
{ name: "Tests created", done: status.testsCreated },
|
|
426
884
|
{ name: "Tests created and passed", done: status.testsPassed },
|
|
427
885
|
{ name: "Documentation created", done: status.documentationCreated },
|
|
428
886
|
];
|
|
@@ -432,12 +890,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
890
|
.map((c) => `${c.done ? "✅" : "❌"} ${c.name}`)
|
|
433
891
|
.join("\n");
|
|
434
892
|
|
|
893
|
+
workflowState.state.readyCheckCompleted = allDone;
|
|
894
|
+
if (allDone) {
|
|
895
|
+
workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
|
|
896
|
+
? workflowState.state.released
|
|
897
|
+
? "ready_to_complete"
|
|
898
|
+
: "release"
|
|
899
|
+
: "commit";
|
|
900
|
+
}
|
|
901
|
+
await workflowState.save();
|
|
902
|
+
|
|
435
903
|
if (allDone) {
|
|
436
904
|
return {
|
|
437
905
|
content: [
|
|
438
906
|
{
|
|
439
907
|
type: "text",
|
|
440
|
-
text: `🎉 ALL CHECKS PASSED
|
|
908
|
+
text: `🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' with your commit message\n2. Run 'perform_release' to record the release\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to push to a non-default branch.\n\nTask: ${status.taskDescription}`,
|
|
441
909
|
},
|
|
442
910
|
],
|
|
443
911
|
};
|
|
@@ -453,6 +921,252 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
453
921
|
}
|
|
454
922
|
}
|
|
455
923
|
|
|
924
|
+
case "commit_and_push": {
|
|
925
|
+
if (!workflowState.state.readyToCommit) {
|
|
926
|
+
return {
|
|
927
|
+
content: [
|
|
928
|
+
{
|
|
929
|
+
type: "text",
|
|
930
|
+
text: "⚠️ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.",
|
|
931
|
+
},
|
|
932
|
+
],
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
937
|
+
return {
|
|
938
|
+
content: [
|
|
939
|
+
{
|
|
940
|
+
type: "text",
|
|
941
|
+
text: "⚠️ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!",
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const providedCommitMessage =
|
|
948
|
+
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
949
|
+
|
|
950
|
+
if (!(await hasWorkingChanges())) {
|
|
951
|
+
return {
|
|
952
|
+
content: [
|
|
953
|
+
{
|
|
954
|
+
type: "text",
|
|
955
|
+
text: "⚠️ No changes detected. Make sure you have modifications to commit before running 'commit_and_push'.",
|
|
956
|
+
},
|
|
957
|
+
],
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (!(await hasTestChanges())) {
|
|
962
|
+
return {
|
|
963
|
+
content: [
|
|
964
|
+
{
|
|
965
|
+
type: "text",
|
|
966
|
+
text: "⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.",
|
|
967
|
+
},
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
await exec("git add .");
|
|
974
|
+
} catch (error) {
|
|
975
|
+
return {
|
|
976
|
+
content: [
|
|
977
|
+
{
|
|
978
|
+
type: "text",
|
|
979
|
+
text: `❌ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`,
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const stagedChanges = await getStagedChanges();
|
|
986
|
+
const { summary: generatedSummary, body: commitBody } =
|
|
987
|
+
createCommitMessageParts(stagedChanges, providedCommitMessage);
|
|
988
|
+
|
|
989
|
+
if (!generatedSummary) {
|
|
990
|
+
return {
|
|
991
|
+
content: [
|
|
992
|
+
{
|
|
993
|
+
type: "text",
|
|
994
|
+
text: "⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.",
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
if (commitBody) {
|
|
1002
|
+
await exec(
|
|
1003
|
+
`git commit -m ${shellEscape(generatedSummary)} -m ${shellEscape(commitBody)}`
|
|
1004
|
+
);
|
|
1005
|
+
} else {
|
|
1006
|
+
await exec(`git commit -m ${shellEscape(generatedSummary)}`);
|
|
1007
|
+
}
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
const output = error.stderr || error.stdout || error.message || "Unknown error";
|
|
1010
|
+
if (output.includes("nothing to commit")) {
|
|
1011
|
+
return {
|
|
1012
|
+
content: [
|
|
1013
|
+
{
|
|
1014
|
+
type: "text",
|
|
1015
|
+
text: "⚠️ Nothing to commit. Make additional changes before running 'commit_and_push'.",
|
|
1016
|
+
},
|
|
1017
|
+
],
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return {
|
|
1022
|
+
content: [
|
|
1023
|
+
{
|
|
1024
|
+
type: "text",
|
|
1025
|
+
text: `❌ git commit failed:\n\n${output}`,
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const requestedBranch =
|
|
1032
|
+
typeof args.branch === "string" ? args.branch.trim() : "";
|
|
1033
|
+
const currentBranch = await getCurrentBranch();
|
|
1034
|
+
const pushCommand = requestedBranch
|
|
1035
|
+
? `git push origin ${shellEscape(requestedBranch)}`
|
|
1036
|
+
: "git push";
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
await exec(pushCommand);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
return {
|
|
1042
|
+
content: [
|
|
1043
|
+
{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: `❌ git push failed:\n\n${error.stderr || error.stdout || error.message}`,
|
|
1046
|
+
},
|
|
1047
|
+
],
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
1052
|
+
workflowState.state.lastCommitMessage = generatedSummary;
|
|
1053
|
+
workflowState.state.lastPushBranch =
|
|
1054
|
+
requestedBranch || currentBranch || "";
|
|
1055
|
+
workflowState.state.currentPhase = workflowState.state.released
|
|
1056
|
+
? "ready_to_complete"
|
|
1057
|
+
: "release";
|
|
1058
|
+
await workflowState.save();
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
content: [
|
|
1062
|
+
{
|
|
1063
|
+
type: "text",
|
|
1064
|
+
text: `✅ Commit & push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${
|
|
1065
|
+
requestedBranch || currentBranch || "(default upstream)"
|
|
1066
|
+
}\n\nNext: Run 'perform_release' to record the release.`,
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
case "perform_release": {
|
|
1073
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
1074
|
+
return {
|
|
1075
|
+
content: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: "⚠️ Please start a task first using 'start_task'!",
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
1085
|
+
return {
|
|
1086
|
+
content: [
|
|
1087
|
+
{
|
|
1088
|
+
type: "text",
|
|
1089
|
+
text: "⚠️ Please run 'commit_and_push' after the ready check before recording a release!",
|
|
1090
|
+
},
|
|
1091
|
+
],
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
1096
|
+
return {
|
|
1097
|
+
content: [
|
|
1098
|
+
{
|
|
1099
|
+
type: "text",
|
|
1100
|
+
text: "⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!",
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const releaseCommand =
|
|
1107
|
+
typeof args.command === "string" ? args.command.trim() : "";
|
|
1108
|
+
|
|
1109
|
+
if (!releaseCommand) {
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: "text",
|
|
1114
|
+
text: "⚠️ Please provide a non-empty 'command' when using 'perform_release'.",
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (await hasWorkingChanges()) {
|
|
1121
|
+
return {
|
|
1122
|
+
content: [
|
|
1123
|
+
{
|
|
1124
|
+
type: "text",
|
|
1125
|
+
text: "⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.",
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const lastCommitMessage = await getLastCommitMessage();
|
|
1132
|
+
const releaseType = determineReleaseTypeFromCommit(
|
|
1133
|
+
lastCommitMessage || workflowState.state.lastCommitMessage
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
const finalCommand = releaseCommand.includes("--release-as")
|
|
1137
|
+
? releaseCommand
|
|
1138
|
+
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
await exec(finalCommand);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
return {
|
|
1144
|
+
content: [
|
|
1145
|
+
{
|
|
1146
|
+
type: "text",
|
|
1147
|
+
text: `❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`,
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
workflowState.state.released = true;
|
|
1154
|
+
workflowState.state.releaseCommand = finalCommand;
|
|
1155
|
+
workflowState.state.releaseNotes =
|
|
1156
|
+
typeof args.notes === "string" ? args.notes : "";
|
|
1157
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
1158
|
+
await workflowState.save();
|
|
1159
|
+
|
|
1160
|
+
return {
|
|
1161
|
+
content: [
|
|
1162
|
+
{
|
|
1163
|
+
type: "text",
|
|
1164
|
+
text: `🚀 Release recorded!\n\nCommand: ${finalCommand}\nRelease type: ${releaseType}\n${workflowState.state.releaseNotes ? `Notes: ${workflowState.state.releaseNotes}\n` : ""}\n✅ Next: Run 'complete_task' to wrap up.`,
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
456
1170
|
case "complete_task": {
|
|
457
1171
|
if (!workflowState.state.readyToCommit) {
|
|
458
1172
|
return {
|
|
@@ -465,6 +1179,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
465
1179
|
};
|
|
466
1180
|
}
|
|
467
1181
|
|
|
1182
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
1183
|
+
return {
|
|
1184
|
+
content: [
|
|
1185
|
+
{
|
|
1186
|
+
type: "text",
|
|
1187
|
+
text: "⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before completing the task!",
|
|
1188
|
+
},
|
|
1189
|
+
],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
1194
|
+
return {
|
|
1195
|
+
content: [
|
|
1196
|
+
{
|
|
1197
|
+
type: "text",
|
|
1198
|
+
text: "⚠️ Commit and push not completed! Run 'commit_and_push' first.",
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (!workflowState.state.released) {
|
|
1205
|
+
return {
|
|
1206
|
+
content: [
|
|
1207
|
+
{
|
|
1208
|
+
type: "text",
|
|
1209
|
+
text: "⚠️ Release not recorded! Run 'perform_release' after committing and pushing.",
|
|
1210
|
+
},
|
|
1211
|
+
],
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
468
1215
|
if (await hasStagedChanges()) {
|
|
469
1216
|
return {
|
|
470
1217
|
content: [
|
|
@@ -483,6 +1230,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
483
1230
|
testCommand: workflowState.state.testCommand,
|
|
484
1231
|
documentationType: workflowState.state.documentationType,
|
|
485
1232
|
commitMessage: args.commitMessage,
|
|
1233
|
+
releaseCommand: workflowState.state.releaseCommand,
|
|
1234
|
+
releaseNotes: workflowState.state.releaseNotes,
|
|
486
1235
|
});
|
|
487
1236
|
|
|
488
1237
|
const completedTask = workflowState.state.taskDescription;
|
|
@@ -518,14 +1267,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
518
1267
|
coding: "🔨 Currently coding",
|
|
519
1268
|
testing: "🧪 Ready for testing",
|
|
520
1269
|
documentation: "📝 Ready for documentation",
|
|
521
|
-
ready: "✅ Ready
|
|
1270
|
+
ready: "✅ Ready for verification",
|
|
1271
|
+
commit: "📦 Ready to commit & push",
|
|
1272
|
+
release: "🚀 Ready to release",
|
|
1273
|
+
ready_to_complete: "✅ Release completed",
|
|
522
1274
|
};
|
|
523
1275
|
|
|
524
1276
|
return {
|
|
525
1277
|
content: [
|
|
526
1278
|
{
|
|
527
1279
|
type: "text",
|
|
528
|
-
text: `📋 Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "✅" : "⏳"} Feature/bug fixed\n${status.testsPassed ? "✅" : "⏳"} Tests passed\n${status.documentationCreated ? "✅" : "⏳"} Documentation created\n${status.
|
|
1280
|
+
text: `📋 Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "✅" : "⏳"} Feature/bug fixed\n${status.testsCreated ? "✅" : "⏳"} Tests created\n${status.testsPassed ? "✅" : "⏳"} Tests passed\n${status.documentationCreated ? "✅" : "⏳"} Documentation created\n${status.readyCheckCompleted ? "✅" : "⏳"} Ready check completed\n${status.commitAndPushCompleted ? "✅" : "⏳"} Commit & push completed\n${status.released ? "✅" : "⏳"} Release recorded\n\nNext: ${getNextStep(status)}`,
|
|
529
1281
|
},
|
|
530
1282
|
],
|
|
531
1283
|
};
|
|
@@ -582,10 +1334,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
582
1334
|
// Helper function
|
|
583
1335
|
export function getNextStep(status) {
|
|
584
1336
|
if (!status.bugFixed) return "Mark feature/bug as fixed";
|
|
585
|
-
if (!status.
|
|
1337
|
+
if (!status.testsCreated) return "Create tests";
|
|
1338
|
+
if (!status.testsPassed) return "Run tests";
|
|
586
1339
|
if (!status.documentationCreated) return "Create documentation";
|
|
587
|
-
if (!status.
|
|
588
|
-
return "
|
|
1340
|
+
if (!status.readyCheckCompleted) return "Run 'check_ready_to_commit'";
|
|
1341
|
+
if (!status.commitAndPushCompleted) return "Run 'commit_and_push'";
|
|
1342
|
+
if (!status.released) return "Run 'perform_release'";
|
|
1343
|
+
return "Complete the task";
|
|
589
1344
|
}
|
|
590
1345
|
|
|
591
1346
|
// Define prompts
|
package/package.json
CHANGED
|
@@ -3,7 +3,14 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
WorkflowState,
|
|
8
|
+
getNextStep,
|
|
9
|
+
containsTestFilesInStatus,
|
|
10
|
+
createCommitMessageParts,
|
|
11
|
+
determineReleaseTypeFromCommit,
|
|
12
|
+
workingTreeSummary,
|
|
13
|
+
} from "../index.js";
|
|
7
14
|
|
|
8
15
|
async function withWorkflowState(callback) {
|
|
9
16
|
const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
|
|
@@ -23,6 +30,14 @@ test("WorkflowState persists data across save/load", async () => {
|
|
|
23
30
|
state.state.currentPhase = "testing";
|
|
24
31
|
state.state.taskDescription = "Test persistence";
|
|
25
32
|
state.state.bugFixed = true;
|
|
33
|
+
state.state.testsCreated = true;
|
|
34
|
+
state.state.readyCheckCompleted = true;
|
|
35
|
+
state.state.released = true;
|
|
36
|
+
state.state.releaseCommand = "npm run release";
|
|
37
|
+
state.state.releaseNotes = "Automated release";
|
|
38
|
+
state.state.commitAndPushCompleted = true;
|
|
39
|
+
state.state.lastCommitMessage = "test: persistence";
|
|
40
|
+
state.state.lastPushBranch = "main";
|
|
26
41
|
await state.save();
|
|
27
42
|
|
|
28
43
|
const reloaded = new WorkflowState(state.stateFile);
|
|
@@ -31,21 +46,37 @@ test("WorkflowState persists data across save/load", async () => {
|
|
|
31
46
|
assert.equal(reloaded.state.currentPhase, "testing");
|
|
32
47
|
assert.equal(reloaded.state.taskDescription, "Test persistence");
|
|
33
48
|
assert.equal(reloaded.state.bugFixed, true);
|
|
49
|
+
assert.equal(reloaded.state.testsCreated, true);
|
|
50
|
+
assert.equal(reloaded.state.readyCheckCompleted, true);
|
|
51
|
+
assert.equal(reloaded.state.released, true);
|
|
52
|
+
assert.equal(reloaded.state.releaseCommand, "npm run release");
|
|
53
|
+
assert.equal(reloaded.state.releaseNotes, "Automated release");
|
|
54
|
+
assert.equal(reloaded.state.commitAndPushCompleted, true);
|
|
55
|
+
assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
|
|
56
|
+
assert.equal(reloaded.state.lastPushBranch, "main");
|
|
34
57
|
});
|
|
35
58
|
});
|
|
36
59
|
|
|
37
60
|
test("WorkflowState reset keeps history but clears progress", async () => {
|
|
38
61
|
await withWorkflowState(async (state) => {
|
|
39
62
|
state.state.currentPhase = "ready";
|
|
63
|
+
state.state.testsCreated = true;
|
|
40
64
|
state.state.testsPassed = true;
|
|
41
65
|
state.state.documentationCreated = true;
|
|
66
|
+
state.state.readyCheckCompleted = true;
|
|
67
|
+
state.state.released = true;
|
|
68
|
+
state.state.commitAndPushCompleted = true;
|
|
42
69
|
state.addToHistory({ taskDescription: "Sample task" });
|
|
43
70
|
|
|
44
71
|
state.reset();
|
|
45
72
|
|
|
46
73
|
assert.equal(state.state.currentPhase, "idle");
|
|
74
|
+
assert.equal(state.state.testsCreated, false);
|
|
47
75
|
assert.equal(state.state.testsPassed, false);
|
|
48
76
|
assert.equal(state.state.documentationCreated, false);
|
|
77
|
+
assert.equal(state.state.readyCheckCompleted, false);
|
|
78
|
+
assert.equal(state.state.released, false);
|
|
79
|
+
assert.equal(state.state.commitAndPushCompleted, false);
|
|
49
80
|
assert.equal(state.state.history.length, 1);
|
|
50
81
|
assert.equal(state.state.history[0].taskDescription, "Sample task");
|
|
51
82
|
});
|
|
@@ -54,9 +85,12 @@ test("WorkflowState reset keeps history but clears progress", async () => {
|
|
|
54
85
|
test("getNextStep guides workflow progression", () => {
|
|
55
86
|
const baseState = {
|
|
56
87
|
bugFixed: false,
|
|
88
|
+
testsCreated: false,
|
|
57
89
|
testsPassed: false,
|
|
58
90
|
documentationCreated: false,
|
|
59
|
-
|
|
91
|
+
readyCheckCompleted: false,
|
|
92
|
+
commitAndPushCompleted: false,
|
|
93
|
+
released: false,
|
|
60
94
|
};
|
|
61
95
|
|
|
62
96
|
assert.equal(
|
|
@@ -67,21 +101,104 @@ test("getNextStep guides workflow progression", () => {
|
|
|
67
101
|
const afterFix = { ...baseState, bugFixed: true };
|
|
68
102
|
assert.equal(
|
|
69
103
|
getNextStep(afterFix),
|
|
70
|
-
"Create
|
|
104
|
+
"Create tests"
|
|
71
105
|
);
|
|
72
106
|
|
|
73
|
-
const
|
|
107
|
+
const afterTestsCreated = { ...afterFix, testsCreated: true };
|
|
108
|
+
assert.equal(getNextStep(afterTestsCreated), "Run tests");
|
|
109
|
+
|
|
110
|
+
const afterTests = { ...afterTestsCreated, testsPassed: true };
|
|
74
111
|
assert.equal(
|
|
75
112
|
getNextStep(afterTests),
|
|
76
113
|
"Create documentation"
|
|
77
114
|
);
|
|
78
115
|
|
|
79
116
|
const afterDocs = { ...afterTests, documentationCreated: true };
|
|
80
|
-
assert.equal(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
117
|
+
assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
|
|
118
|
+
|
|
119
|
+
const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
|
|
120
|
+
assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
|
|
121
|
+
|
|
122
|
+
const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
|
|
123
|
+
assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
|
|
124
|
+
|
|
125
|
+
const afterRelease = { ...afterCommit, released: true };
|
|
126
|
+
assert.equal(getNextStep(afterRelease), "Complete the task");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("containsTestFilesInStatus detects modified test files", () => {
|
|
130
|
+
const statusOutput = [
|
|
131
|
+
" M src/app.test.js",
|
|
132
|
+
"A tests/helpers/util.js",
|
|
133
|
+
"R src/old.js -> src/new.spec.ts",
|
|
134
|
+
].join("\n");
|
|
135
|
+
|
|
136
|
+
assert.equal(containsTestFilesInStatus(statusOutput), true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("containsTestFilesInStatus returns false when no test files present", () => {
|
|
140
|
+
const statusOutput = [
|
|
141
|
+
" M src/app.js",
|
|
142
|
+
"A docs/README.md",
|
|
143
|
+
"?? assets/logo.png",
|
|
144
|
+
].join("\n");
|
|
145
|
+
|
|
146
|
+
assert.equal(containsTestFilesInStatus(statusOutput), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("createCommitMessageParts generates test prefixed summary with details", () => {
|
|
150
|
+
const changes = [
|
|
151
|
+
{ status: "M", path: "src/app.test.js" },
|
|
152
|
+
{ status: "A", path: "tests/helpers/util.js" },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const { summary, body } = createCommitMessageParts(changes);
|
|
156
|
+
|
|
157
|
+
assert.equal(summary.startsWith("test:"), true);
|
|
158
|
+
assert.equal(body.includes("- modified src/app.test.js"), true);
|
|
159
|
+
assert.equal(body.includes("- added tests/helpers/util.js"), true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("createCommitMessageParts respects provided summary when present", () => {
|
|
163
|
+
const changes = [{ status: "M", path: "docs/guide.md" }];
|
|
164
|
+
const provided = "docs: update guide";
|
|
165
|
+
|
|
166
|
+
const { summary, body } = createCommitMessageParts(changes, provided);
|
|
167
|
+
|
|
168
|
+
assert.equal(summary, provided);
|
|
169
|
+
assert.equal(body.includes("- modified docs/guide.md"), true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("determineReleaseTypeFromCommit returns major for breaking change", () => {
|
|
173
|
+
const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
|
|
174
|
+
assert.equal(determineReleaseTypeFromCommit(message), "major");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("determineReleaseTypeFromCommit returns minor for feat", () => {
|
|
178
|
+
const message = "feat: add dashboard";
|
|
179
|
+
assert.equal(determineReleaseTypeFromCommit(message), "minor");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("determineReleaseTypeFromCommit returns patch for fix", () => {
|
|
183
|
+
const message = "fix: resolve login issue";
|
|
184
|
+
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
|
|
188
|
+
const message = "chore: update dependencies";
|
|
189
|
+
assert.equal(determineReleaseTypeFromCommit(message), "patch");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("workingTreeSummary reports no changes for empty output", () => {
|
|
193
|
+
const summary = workingTreeSummary("\n \n");
|
|
194
|
+
assert.equal(summary.hasChanges, false);
|
|
195
|
+
assert.deepEqual(summary.lines, []);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("workingTreeSummary lists trimmed status lines", () => {
|
|
199
|
+
const statusOutput = " M src/app.js\nA tests/new.test.js\n";
|
|
200
|
+
const summary = workingTreeSummary(statusOutput);
|
|
84
201
|
|
|
85
|
-
|
|
86
|
-
assert.
|
|
202
|
+
assert.equal(summary.hasChanges, true);
|
|
203
|
+
assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
|
|
87
204
|
});
|