@programinglive/dev-workflow-mcp-server 1.0.11 ā 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 +19 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/tests/handlers.test.js +272 -0
- package/tools/handlers.js +138 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.1.0](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.11...v1.1.0) (2025-10-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### š Documentation
|
|
9
|
+
|
|
10
|
+
* clarify commit and push step ([c741f61](https://github.com/programinglive/dev-workflow-mcp-server/commit/c741f618813479c5df2bcca94c3dffc8881e97c8))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### š Bug Fixes
|
|
14
|
+
|
|
15
|
+
* allow commit_and_push on clean tree ([a831974](https://github.com/programinglive/dev-workflow-mcp-server/commit/a831974f66313ebe8752d416752076df63902bea))
|
|
16
|
+
* auto-detect commit in perform_release when tree is clean ([0db7038](https://github.com/programinglive/dev-workflow-mcp-server/commit/0db7038148f68963f53e52a5f73623dea7ad3462))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### ⨠Features
|
|
20
|
+
|
|
21
|
+
* check staged changes when resetting workflow to commit phase ([05d02fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/05d02fd7ec6177af3fb1fd86ec093e1e37b56cbd))
|
|
22
|
+
* continue_workflow auto-executes next step in commit phase ([a5b49ed](https://github.com/programinglive/dev-workflow-mcp-server/commit/a5b49edaae7cb51ea3aa49d9b1b64587843441ce))
|
|
23
|
+
|
|
5
24
|
### [1.0.11](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.10...v1.0.11) (2025-10-25)
|
|
6
25
|
|
|
7
26
|
|
package/README.md
CHANGED
|
@@ -11,8 +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** - Let the server
|
|
15
|
-
7. **Release** -
|
|
14
|
+
6. **Commit & Push** - Let the server stage, commit, **and push** your changes once verification passes (if you make new edits afterward, the workflow moves back to this step automatically)
|
|
15
|
+
7. **Release** - After the push succeeds, record the release details before closing out the task
|
|
16
16
|
|
|
17
17
|
## š Installation
|
|
18
18
|
|
package/package.json
CHANGED
package/tests/handlers.test.js
CHANGED
|
@@ -58,6 +58,7 @@ test("commit_and_push commits changes and pushes", async () => {
|
|
|
58
58
|
|
|
59
59
|
const git = {
|
|
60
60
|
hasWorkingChanges: async () => true,
|
|
61
|
+
hasStagedChanges: async () => false,
|
|
61
62
|
hasTestChanges: async () => true,
|
|
62
63
|
getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
|
|
63
64
|
getCurrentBranch: async () => "main",
|
|
@@ -95,10 +96,56 @@ test("commit_and_push commits changes and pushes", async () => {
|
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
test("perform_release blocks when new changes detected", async () => {
|
|
100
|
+
await withWorkflowState(async (workflowState) => {
|
|
101
|
+
workflowState.state.currentPhase = "release";
|
|
102
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
103
|
+
workflowState.state.readyCheckCompleted = true;
|
|
104
|
+
await workflowState.save();
|
|
105
|
+
|
|
106
|
+
const commands = [];
|
|
107
|
+
const execStub = async (command) => {
|
|
108
|
+
commands.push(command);
|
|
109
|
+
if (command === "git status --porcelain") {
|
|
110
|
+
return { stdout: "?? new-file.js" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { stdout: "" };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const git = {
|
|
117
|
+
hasWorkingChanges: async () => true,
|
|
118
|
+
hasStagedChanges: async () => false,
|
|
119
|
+
hasTestChanges: async () => true,
|
|
120
|
+
getStagedChanges: async () => [],
|
|
121
|
+
getCurrentBranch: async () => "main",
|
|
122
|
+
getLastCommitMessage: async () => "fix: adjust layout",
|
|
123
|
+
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const response = await handleToolCall({
|
|
127
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
128
|
+
normalizeRequestArgs,
|
|
129
|
+
workflowState,
|
|
130
|
+
exec: execStub,
|
|
131
|
+
git,
|
|
132
|
+
utils,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.ok(
|
|
136
|
+
response.content[0].text.includes("run 'commit_and_push' again"),
|
|
137
|
+
"response should instruct to recommit"
|
|
138
|
+
);
|
|
139
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
140
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
98
144
|
test("continue_workflow warns when workflow is idle", async () => {
|
|
99
145
|
await withWorkflowState(async (workflowState) => {
|
|
100
146
|
const git = {
|
|
101
147
|
hasWorkingChanges: async () => false,
|
|
148
|
+
hasStagedChanges: async () => false,
|
|
102
149
|
hasTestChanges: async () => false,
|
|
103
150
|
getStagedChanges: async () => [],
|
|
104
151
|
getCurrentBranch: async () => "main",
|
|
@@ -122,6 +169,87 @@ test("continue_workflow warns when workflow is idle", async () => {
|
|
|
122
169
|
});
|
|
123
170
|
});
|
|
124
171
|
|
|
172
|
+
test("continue_workflow resets to commit when new changes detected", async () => {
|
|
173
|
+
await withWorkflowState(async (workflowState) => {
|
|
174
|
+
workflowState.state.currentPhase = "release";
|
|
175
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
176
|
+
workflowState.state.readyCheckCompleted = true;
|
|
177
|
+
await workflowState.save();
|
|
178
|
+
|
|
179
|
+
const git = {
|
|
180
|
+
hasWorkingChanges: async () => true,
|
|
181
|
+
hasStagedChanges: async () => false,
|
|
182
|
+
hasTestChanges: async () => true,
|
|
183
|
+
getStagedChanges: async () => [],
|
|
184
|
+
getCurrentBranch: async () => "main",
|
|
185
|
+
getLastCommitMessage: async () => "fix: adjust layout",
|
|
186
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const response = await handleToolCall({
|
|
190
|
+
request: createRequest("continue_workflow", {}),
|
|
191
|
+
normalizeRequestArgs,
|
|
192
|
+
workflowState,
|
|
193
|
+
exec: async () => ({ stdout: "" }),
|
|
194
|
+
git,
|
|
195
|
+
utils,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
assert.ok(
|
|
199
|
+
response.content[0].text.includes("Workflow moved back to the commit phase"),
|
|
200
|
+
"response should warn that workflow reset to commit"
|
|
201
|
+
);
|
|
202
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
203
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("commit_and_push recognizes already committed work", async () => {
|
|
208
|
+
await withWorkflowState(async (workflowState) => {
|
|
209
|
+
workflowState.state.readyToCommit = true;
|
|
210
|
+
workflowState.state.readyCheckCompleted = true;
|
|
211
|
+
workflowState.state.testsSkipped = false;
|
|
212
|
+
workflowState.state.testsCreated = true;
|
|
213
|
+
workflowState.state.testsPassed = true;
|
|
214
|
+
workflowState.state.currentPhase = "commit";
|
|
215
|
+
await workflowState.save();
|
|
216
|
+
|
|
217
|
+
const commands = [];
|
|
218
|
+
const execStub = async (command) => {
|
|
219
|
+
commands.push(command);
|
|
220
|
+
return { stdout: "" };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const git = {
|
|
224
|
+
hasWorkingChanges: async () => false,
|
|
225
|
+
hasStagedChanges: async () => false,
|
|
226
|
+
hasTestChanges: async () => true,
|
|
227
|
+
getStagedChanges: async () => [],
|
|
228
|
+
getCurrentBranch: async () => "main",
|
|
229
|
+
getLastCommitMessage: async () => "fix: previous work",
|
|
230
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const response = await handleToolCall({
|
|
234
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
235
|
+
normalizeRequestArgs,
|
|
236
|
+
workflowState,
|
|
237
|
+
exec: execStub,
|
|
238
|
+
git,
|
|
239
|
+
utils,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.equal(commands.length, 0, "no git commands should run when work is already committed");
|
|
243
|
+
assert.ok(
|
|
244
|
+
response.content[0].text.includes("No changes detected"),
|
|
245
|
+
"response should note that no changes were detected"
|
|
246
|
+
);
|
|
247
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
248
|
+
assert.equal(workflowState.state.lastCommitMessage, "fix: previous work");
|
|
249
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
125
253
|
test("perform_release runs release command before pushing with tags", async () => {
|
|
126
254
|
await withWorkflowState(async (workflowState) => {
|
|
127
255
|
workflowState.state.currentPhase = "release";
|
|
@@ -143,6 +271,7 @@ test("perform_release runs release command before pushing with tags", async () =
|
|
|
143
271
|
|
|
144
272
|
const git = {
|
|
145
273
|
hasWorkingChanges: async () => false,
|
|
274
|
+
hasStagedChanges: async () => false,
|
|
146
275
|
hasTestChanges: async () => true,
|
|
147
276
|
getStagedChanges: async () => [],
|
|
148
277
|
getCurrentBranch: async () => "main",
|
|
@@ -186,3 +315,146 @@ test("perform_release runs release command before pushing with tags", async () =
|
|
|
186
315
|
);
|
|
187
316
|
});
|
|
188
317
|
});
|
|
318
|
+
|
|
319
|
+
test("perform_release accepts shorthand patch command", async () => {
|
|
320
|
+
await withWorkflowState(async (workflowState) => {
|
|
321
|
+
workflowState.state.currentPhase = "release";
|
|
322
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
323
|
+
workflowState.state.readyCheckCompleted = true;
|
|
324
|
+
workflowState.state.lastCommitMessage = "fix: adjust labels";
|
|
325
|
+
await workflowState.save();
|
|
326
|
+
|
|
327
|
+
const commands = [];
|
|
328
|
+
const execStub = async (command) => {
|
|
329
|
+
commands.push(command);
|
|
330
|
+
if (command === "git status --porcelain") {
|
|
331
|
+
return { stdout: "" };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { stdout: "" };
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const git = {
|
|
338
|
+
hasWorkingChanges: async () => false,
|
|
339
|
+
hasStagedChanges: async () => false,
|
|
340
|
+
hasTestChanges: async () => true,
|
|
341
|
+
getStagedChanges: async () => [],
|
|
342
|
+
getCurrentBranch: async () => "main",
|
|
343
|
+
getLastCommitMessage: async () => "fix: adjust labels",
|
|
344
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
await handleToolCall({
|
|
348
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
349
|
+
normalizeRequestArgs,
|
|
350
|
+
workflowState,
|
|
351
|
+
exec: execStub,
|
|
352
|
+
git,
|
|
353
|
+
utils,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release:patch"));
|
|
357
|
+
assert.ok(releaseCommand, "shorthand patch command should map to release:patch script");
|
|
358
|
+
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
359
|
+
assert.ok(pushCommand, "git push with tags should run after shorthand release");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("perform_release respects explicit releaseType option", async () => {
|
|
364
|
+
await withWorkflowState(async (workflowState) => {
|
|
365
|
+
workflowState.state.currentPhase = "release";
|
|
366
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
367
|
+
workflowState.state.readyCheckCompleted = true;
|
|
368
|
+
await workflowState.save();
|
|
369
|
+
|
|
370
|
+
const commands = [];
|
|
371
|
+
const execStub = async (command) => {
|
|
372
|
+
commands.push(command);
|
|
373
|
+
if (command === "git status --porcelain") {
|
|
374
|
+
return { stdout: "" };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return { stdout: "" };
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const git = {
|
|
381
|
+
hasWorkingChanges: async () => false,
|
|
382
|
+
hasStagedChanges: async () => false,
|
|
383
|
+
hasTestChanges: async () => true,
|
|
384
|
+
getStagedChanges: async () => [],
|
|
385
|
+
getCurrentBranch: async () => "main",
|
|
386
|
+
getLastCommitMessage: async () => "feat: import improvements",
|
|
387
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await handleToolCall({
|
|
391
|
+
request: createRequest("perform_release", {
|
|
392
|
+
command: "npm run release",
|
|
393
|
+
releaseType: "minor",
|
|
394
|
+
}),
|
|
395
|
+
normalizeRequestArgs,
|
|
396
|
+
workflowState,
|
|
397
|
+
exec: execStub,
|
|
398
|
+
git,
|
|
399
|
+
utils,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
403
|
+
assert.ok(releaseCommand, "release command should run");
|
|
404
|
+
assert.ok(
|
|
405
|
+
releaseCommand.includes("-- --release-as minor"),
|
|
406
|
+
"release command should append provided releaseType"
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("perform_release auto-detects commit when tree is clean", async () => {
|
|
412
|
+
await withWorkflowState(async (workflowState) => {
|
|
413
|
+
workflowState.state.currentPhase = "release";
|
|
414
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
415
|
+
workflowState.state.readyCheckCompleted = true;
|
|
416
|
+
workflowState.state.lastPushBranch = "";
|
|
417
|
+
workflowState.state.lastCommitMessage = "";
|
|
418
|
+
await workflowState.save();
|
|
419
|
+
|
|
420
|
+
const commands = [];
|
|
421
|
+
const execStub = async (command) => {
|
|
422
|
+
commands.push(command);
|
|
423
|
+
if (command === "git status --porcelain") {
|
|
424
|
+
return { stdout: "" };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { stdout: "" };
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const git = {
|
|
431
|
+
hasWorkingChanges: async () => false,
|
|
432
|
+
hasStagedChanges: async () => false,
|
|
433
|
+
hasTestChanges: async () => true,
|
|
434
|
+
getStagedChanges: async () => [],
|
|
435
|
+
getCurrentBranch: async () => "main",
|
|
436
|
+
getLastCommitMessage: async () => "chore: already pushed",
|
|
437
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const response = await handleToolCall({
|
|
441
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
442
|
+
normalizeRequestArgs,
|
|
443
|
+
workflowState,
|
|
444
|
+
exec: execStub,
|
|
445
|
+
git,
|
|
446
|
+
utils,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
450
|
+
assert.ok(pushCommand, "git push with tags should still run");
|
|
451
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
452
|
+
assert.equal(workflowState.state.lastCommitMessage, "chore: already pushed");
|
|
453
|
+
assert.equal(workflowState.state.lastPushBranch, "main");
|
|
454
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
455
|
+
assert.ok(
|
|
456
|
+
response.content[0].text.includes("Detected clean working tree"),
|
|
457
|
+
"response should mention auto-detected commit"
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
});
|
package/tools/handlers.js
CHANGED
|
@@ -7,6 +7,27 @@ const textResponse = (text) => ({
|
|
|
7
7
|
],
|
|
8
8
|
});
|
|
9
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
|
+
|
|
10
31
|
function formatChecklist(checks) {
|
|
11
32
|
return checks.map((c) => `${c.done ? "ā
" : "ā"} ${c.name}`).join("\n");
|
|
12
33
|
}
|
|
@@ -68,17 +89,33 @@ function getContinueGuidance(status) {
|
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
async function handleContinueWorkflow(workflowState) {
|
|
92
|
+
async function handleContinueWorkflow(workflowState, context) {
|
|
72
93
|
const status = workflowState.state;
|
|
73
94
|
|
|
74
95
|
if (!status || status.currentPhase === "idle") {
|
|
75
96
|
return textResponse("ā ļø No active workflow. Use 'start_task' to kick off a new task before continuing.");
|
|
76
97
|
}
|
|
77
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
|
+
|
|
78
105
|
if (status.currentPhase === "ready") {
|
|
79
106
|
return handleReadyCheck(workflowState);
|
|
80
107
|
}
|
|
81
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
|
+
|
|
82
119
|
const sections = [
|
|
83
120
|
`š Current Phase: ${status.currentPhase}`,
|
|
84
121
|
`š Task: ${status.taskDescription || "(none)"}`,
|
|
@@ -334,9 +371,26 @@ async function handleCommitAndPush(args, context) {
|
|
|
334
371
|
const providedCommitMessage =
|
|
335
372
|
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
336
373
|
|
|
337
|
-
|
|
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
|
+
|
|
338
390
|
return textResponse(
|
|
339
|
-
|
|
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.`
|
|
340
394
|
);
|
|
341
395
|
}
|
|
342
396
|
|
|
@@ -383,10 +437,6 @@ async function handleCommitAndPush(args, context) {
|
|
|
383
437
|
return textResponse(`ā git commit failed:\n\n${output}`);
|
|
384
438
|
}
|
|
385
439
|
|
|
386
|
-
const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
|
|
387
|
-
const currentBranch = await git.getCurrentBranch();
|
|
388
|
-
const branchForPush = requestedBranch || currentBranch || "";
|
|
389
|
-
|
|
390
440
|
try {
|
|
391
441
|
const pushCommand = branchForPush
|
|
392
442
|
? `git push origin ${utils.shellEscape(branchForPush)}`
|
|
@@ -418,9 +468,9 @@ async function handlePerformRelease(args, context) {
|
|
|
418
468
|
return textResponse("ā ļø Please start a task first using 'start_task'!");
|
|
419
469
|
}
|
|
420
470
|
|
|
421
|
-
if (
|
|
471
|
+
if (await resetToCommitIfWorkingChanges(workflowState, git)) {
|
|
422
472
|
return textResponse(
|
|
423
|
-
"ā ļø Please run 'commit_and_push'
|
|
473
|
+
"ā ļø Detected new changes after the last commit. Please run 'commit_and_push' again before recording a release."
|
|
424
474
|
);
|
|
425
475
|
}
|
|
426
476
|
|
|
@@ -430,7 +480,67 @@ async function handlePerformRelease(args, context) {
|
|
|
430
480
|
);
|
|
431
481
|
}
|
|
432
482
|
|
|
433
|
-
|
|
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
|
+
}
|
|
434
544
|
|
|
435
545
|
if (!releaseCommand) {
|
|
436
546
|
return textResponse("ā ļø Please provide a non-empty 'command' when using 'perform_release'.");
|
|
@@ -460,13 +570,16 @@ async function handlePerformRelease(args, context) {
|
|
|
460
570
|
}
|
|
461
571
|
|
|
462
572
|
const lastCommitMessage = await git.getLastCommitMessage();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
573
|
+
if (!releaseType) {
|
|
574
|
+
releaseType = utils.determineReleaseTypeFromCommit(
|
|
575
|
+
lastCommitMessage || workflowState.state.lastCommitMessage
|
|
576
|
+
);
|
|
577
|
+
}
|
|
466
578
|
|
|
467
|
-
const finalCommand =
|
|
468
|
-
|
|
469
|
-
|
|
579
|
+
const finalCommand =
|
|
580
|
+
releaseCommand.includes("--release-as") || /release:(major|minor|patch)/i.test(releaseCommand)
|
|
581
|
+
? releaseCommand
|
|
582
|
+
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
470
583
|
|
|
471
584
|
try {
|
|
472
585
|
await exec(finalCommand);
|
|
@@ -497,10 +610,16 @@ async function handlePerformRelease(args, context) {
|
|
|
497
610
|
|
|
498
611
|
const message = summarizeHistory(workflowState.state.history, args.limit || 10);
|
|
499
612
|
if (!message) {
|
|
500
|
-
|
|
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}`);
|
|
501
617
|
}
|
|
502
618
|
|
|
503
|
-
|
|
619
|
+
const autoNote = autoDetectedCommit
|
|
620
|
+
? "\n\nā¹ļø Detected clean working tree and treated commit/push as already completed."
|
|
621
|
+
: "";
|
|
622
|
+
return textResponse(`${message}${autoNote}`);
|
|
504
623
|
}
|
|
505
624
|
|
|
506
625
|
async function handleCompleteTask(args, workflowState) {
|
|
@@ -607,7 +726,7 @@ export async function handleToolCall({
|
|
|
607
726
|
case "view_history":
|
|
608
727
|
return handleViewHistory(args, workflowState);
|
|
609
728
|
case "continue_workflow":
|
|
610
|
-
return handleContinueWorkflow(workflowState);
|
|
729
|
+
return handleContinueWorkflow(workflowState, { workflowState, exec, git, utils });
|
|
611
730
|
default:
|
|
612
731
|
throw new Error(`Unknown tool: ${name}`);
|
|
613
732
|
}
|