@programinglive/dev-workflow-mcp-server 1.0.11 ā 1.1.2
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 +28 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/tests/handlers.test.js +301 -0
- package/tools/definitions.js +18 -0
- package/tools/handlers.js +166 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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.2](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.1...v1.1.2) (2025-10-25)
|
|
6
|
+
|
|
7
|
+
### [1.1.1](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.0...v1.1.1) (2025-10-25)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⨠Features
|
|
11
|
+
|
|
12
|
+
* add force_complete_task tool ([f731a7d](https://github.com/programinglive/dev-workflow-mcp-server/commit/f731a7d2e48295963b8573d4a2abd3ebd42ba5ab))
|
|
13
|
+
|
|
14
|
+
## [1.1.0](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.11...v1.1.0) (2025-10-25)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### š Documentation
|
|
18
|
+
|
|
19
|
+
* clarify commit and push step ([c741f61](https://github.com/programinglive/dev-workflow-mcp-server/commit/c741f618813479c5df2bcca94c3dffc8881e97c8))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### š Bug Fixes
|
|
23
|
+
|
|
24
|
+
* allow commit_and_push on clean tree ([a831974](https://github.com/programinglive/dev-workflow-mcp-server/commit/a831974f66313ebe8752d416752076df63902bea))
|
|
25
|
+
* auto-detect commit in perform_release when tree is clean ([0db7038](https://github.com/programinglive/dev-workflow-mcp-server/commit/0db7038148f68963f53e52a5f73623dea7ad3462))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### ⨠Features
|
|
29
|
+
|
|
30
|
+
* check staged changes when resetting workflow to commit phase ([05d02fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/05d02fd7ec6177af3fb1fd86ec093e1e37b56cbd))
|
|
31
|
+
* continue_workflow auto-executes next step in commit phase ([a5b49ed](https://github.com/programinglive/dev-workflow-mcp-server/commit/a5b49edaae7cb51ea3aa49d9b1b64587843441ce))
|
|
32
|
+
|
|
5
33
|
### [1.0.11](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.10...v1.0.11) (2025-10-25)
|
|
6
34
|
|
|
7
35
|
|
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,85 @@ test("commit_and_push commits changes and pushes", async () => {
|
|
|
95
96
|
});
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
test("force_complete_task records entry and resets state", async () => {
|
|
100
|
+
await withWorkflowState(async (workflowState) => {
|
|
101
|
+
workflowState.state.taskDescription = "Improve caching";
|
|
102
|
+
workflowState.state.taskType = "feature";
|
|
103
|
+
workflowState.state.lastCommitMessage = "chore: wip";
|
|
104
|
+
workflowState.state.currentPhase = "release";
|
|
105
|
+
await workflowState.save();
|
|
106
|
+
|
|
107
|
+
const response = await handleToolCall({
|
|
108
|
+
request: createRequest("force_complete_task", {
|
|
109
|
+
commitMessage: "chore: force finish",
|
|
110
|
+
reason: "Deadline hit",
|
|
111
|
+
}),
|
|
112
|
+
normalizeRequestArgs,
|
|
113
|
+
workflowState,
|
|
114
|
+
exec: async () => ({ stdout: "" }),
|
|
115
|
+
git: {},
|
|
116
|
+
utils,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
assert.ok(response.content[0].text.includes("Task force-completed."));
|
|
120
|
+
assert.equal(workflowState.state.currentPhase, "idle");
|
|
121
|
+
assert.equal(workflowState.state.history.length, 1);
|
|
122
|
+
assert.equal(workflowState.state.history[0].commitMessage, "chore: force finish");
|
|
123
|
+
assert.equal(workflowState.state.history[0].forced, true);
|
|
124
|
+
assert.equal(workflowState.state.history[0].forceReason, "Deadline hit");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("perform_release blocks when new changes detected", async () => {
|
|
129
|
+
await withWorkflowState(async (workflowState) => {
|
|
130
|
+
workflowState.state.currentPhase = "release";
|
|
131
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
132
|
+
workflowState.state.readyCheckCompleted = true;
|
|
133
|
+
await workflowState.save();
|
|
134
|
+
|
|
135
|
+
const commands = [];
|
|
136
|
+
const execStub = async (command) => {
|
|
137
|
+
commands.push(command);
|
|
138
|
+
if (command === "git status --porcelain") {
|
|
139
|
+
return { stdout: "?? new-file.js" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { stdout: "" };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const git = {
|
|
146
|
+
hasWorkingChanges: async () => true,
|
|
147
|
+
hasStagedChanges: async () => false,
|
|
148
|
+
hasTestChanges: async () => true,
|
|
149
|
+
getStagedChanges: async () => [],
|
|
150
|
+
getCurrentBranch: async () => "main",
|
|
151
|
+
getLastCommitMessage: async () => "fix: adjust layout",
|
|
152
|
+
workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const response = await handleToolCall({
|
|
156
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
157
|
+
normalizeRequestArgs,
|
|
158
|
+
workflowState,
|
|
159
|
+
exec: execStub,
|
|
160
|
+
git,
|
|
161
|
+
utils,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.ok(
|
|
165
|
+
response.content[0].text.includes("run 'commit_and_push' again"),
|
|
166
|
+
"response should instruct to recommit"
|
|
167
|
+
);
|
|
168
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
169
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
98
173
|
test("continue_workflow warns when workflow is idle", async () => {
|
|
99
174
|
await withWorkflowState(async (workflowState) => {
|
|
100
175
|
const git = {
|
|
101
176
|
hasWorkingChanges: async () => false,
|
|
177
|
+
hasStagedChanges: async () => false,
|
|
102
178
|
hasTestChanges: async () => false,
|
|
103
179
|
getStagedChanges: async () => [],
|
|
104
180
|
getCurrentBranch: async () => "main",
|
|
@@ -122,6 +198,87 @@ test("continue_workflow warns when workflow is idle", async () => {
|
|
|
122
198
|
});
|
|
123
199
|
});
|
|
124
200
|
|
|
201
|
+
test("continue_workflow resets to commit when new changes detected", async () => {
|
|
202
|
+
await withWorkflowState(async (workflowState) => {
|
|
203
|
+
workflowState.state.currentPhase = "release";
|
|
204
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
205
|
+
workflowState.state.readyCheckCompleted = true;
|
|
206
|
+
await workflowState.save();
|
|
207
|
+
|
|
208
|
+
const git = {
|
|
209
|
+
hasWorkingChanges: async () => true,
|
|
210
|
+
hasStagedChanges: async () => false,
|
|
211
|
+
hasTestChanges: async () => true,
|
|
212
|
+
getStagedChanges: async () => [],
|
|
213
|
+
getCurrentBranch: async () => "main",
|
|
214
|
+
getLastCommitMessage: async () => "fix: adjust layout",
|
|
215
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const response = await handleToolCall({
|
|
219
|
+
request: createRequest("continue_workflow", {}),
|
|
220
|
+
normalizeRequestArgs,
|
|
221
|
+
workflowState,
|
|
222
|
+
exec: async () => ({ stdout: "" }),
|
|
223
|
+
git,
|
|
224
|
+
utils,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert.ok(
|
|
228
|
+
response.content[0].text.includes("Workflow moved back to the commit phase"),
|
|
229
|
+
"response should warn that workflow reset to commit"
|
|
230
|
+
);
|
|
231
|
+
assert.equal(workflowState.state.currentPhase, "commit");
|
|
232
|
+
assert.equal(workflowState.state.commitAndPushCompleted, false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("commit_and_push recognizes already committed work", async () => {
|
|
237
|
+
await withWorkflowState(async (workflowState) => {
|
|
238
|
+
workflowState.state.readyToCommit = true;
|
|
239
|
+
workflowState.state.readyCheckCompleted = true;
|
|
240
|
+
workflowState.state.testsSkipped = false;
|
|
241
|
+
workflowState.state.testsCreated = true;
|
|
242
|
+
workflowState.state.testsPassed = true;
|
|
243
|
+
workflowState.state.currentPhase = "commit";
|
|
244
|
+
await workflowState.save();
|
|
245
|
+
|
|
246
|
+
const commands = [];
|
|
247
|
+
const execStub = async (command) => {
|
|
248
|
+
commands.push(command);
|
|
249
|
+
return { stdout: "" };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const git = {
|
|
253
|
+
hasWorkingChanges: async () => false,
|
|
254
|
+
hasStagedChanges: async () => false,
|
|
255
|
+
hasTestChanges: async () => true,
|
|
256
|
+
getStagedChanges: async () => [],
|
|
257
|
+
getCurrentBranch: async () => "main",
|
|
258
|
+
getLastCommitMessage: async () => "fix: previous work",
|
|
259
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const response = await handleToolCall({
|
|
263
|
+
request: createRequest("commit_and_push", { commitMessage: "" }),
|
|
264
|
+
normalizeRequestArgs,
|
|
265
|
+
workflowState,
|
|
266
|
+
exec: execStub,
|
|
267
|
+
git,
|
|
268
|
+
utils,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.equal(commands.length, 0, "no git commands should run when work is already committed");
|
|
272
|
+
assert.ok(
|
|
273
|
+
response.content[0].text.includes("No changes detected"),
|
|
274
|
+
"response should note that no changes were detected"
|
|
275
|
+
);
|
|
276
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
277
|
+
assert.equal(workflowState.state.lastCommitMessage, "fix: previous work");
|
|
278
|
+
assert.equal(workflowState.state.currentPhase, "release");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
125
282
|
test("perform_release runs release command before pushing with tags", async () => {
|
|
126
283
|
await withWorkflowState(async (workflowState) => {
|
|
127
284
|
workflowState.state.currentPhase = "release";
|
|
@@ -143,6 +300,7 @@ test("perform_release runs release command before pushing with tags", async () =
|
|
|
143
300
|
|
|
144
301
|
const git = {
|
|
145
302
|
hasWorkingChanges: async () => false,
|
|
303
|
+
hasStagedChanges: async () => false,
|
|
146
304
|
hasTestChanges: async () => true,
|
|
147
305
|
getStagedChanges: async () => [],
|
|
148
306
|
getCurrentBranch: async () => "main",
|
|
@@ -186,3 +344,146 @@ test("perform_release runs release command before pushing with tags", async () =
|
|
|
186
344
|
);
|
|
187
345
|
});
|
|
188
346
|
});
|
|
347
|
+
|
|
348
|
+
test("perform_release accepts shorthand patch command", async () => {
|
|
349
|
+
await withWorkflowState(async (workflowState) => {
|
|
350
|
+
workflowState.state.currentPhase = "release";
|
|
351
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
352
|
+
workflowState.state.readyCheckCompleted = true;
|
|
353
|
+
workflowState.state.lastCommitMessage = "fix: adjust labels";
|
|
354
|
+
await workflowState.save();
|
|
355
|
+
|
|
356
|
+
const commands = [];
|
|
357
|
+
const execStub = async (command) => {
|
|
358
|
+
commands.push(command);
|
|
359
|
+
if (command === "git status --porcelain") {
|
|
360
|
+
return { stdout: "" };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { stdout: "" };
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const git = {
|
|
367
|
+
hasWorkingChanges: async () => false,
|
|
368
|
+
hasStagedChanges: async () => false,
|
|
369
|
+
hasTestChanges: async () => true,
|
|
370
|
+
getStagedChanges: async () => [],
|
|
371
|
+
getCurrentBranch: async () => "main",
|
|
372
|
+
getLastCommitMessage: async () => "fix: adjust labels",
|
|
373
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
await handleToolCall({
|
|
377
|
+
request: createRequest("perform_release", { command: "patch" }),
|
|
378
|
+
normalizeRequestArgs,
|
|
379
|
+
workflowState,
|
|
380
|
+
exec: execStub,
|
|
381
|
+
git,
|
|
382
|
+
utils,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release:patch"));
|
|
386
|
+
assert.ok(releaseCommand, "shorthand patch command should map to release:patch script");
|
|
387
|
+
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
388
|
+
assert.ok(pushCommand, "git push with tags should run after shorthand release");
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("perform_release respects explicit releaseType option", async () => {
|
|
393
|
+
await withWorkflowState(async (workflowState) => {
|
|
394
|
+
workflowState.state.currentPhase = "release";
|
|
395
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
396
|
+
workflowState.state.readyCheckCompleted = true;
|
|
397
|
+
await workflowState.save();
|
|
398
|
+
|
|
399
|
+
const commands = [];
|
|
400
|
+
const execStub = async (command) => {
|
|
401
|
+
commands.push(command);
|
|
402
|
+
if (command === "git status --porcelain") {
|
|
403
|
+
return { stdout: "" };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { stdout: "" };
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const git = {
|
|
410
|
+
hasWorkingChanges: async () => false,
|
|
411
|
+
hasStagedChanges: async () => false,
|
|
412
|
+
hasTestChanges: async () => true,
|
|
413
|
+
getStagedChanges: async () => [],
|
|
414
|
+
getCurrentBranch: async () => "main",
|
|
415
|
+
getLastCommitMessage: async () => "feat: import improvements",
|
|
416
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
await handleToolCall({
|
|
420
|
+
request: createRequest("perform_release", {
|
|
421
|
+
command: "npm run release",
|
|
422
|
+
releaseType: "minor",
|
|
423
|
+
}),
|
|
424
|
+
normalizeRequestArgs,
|
|
425
|
+
workflowState,
|
|
426
|
+
exec: execStub,
|
|
427
|
+
git,
|
|
428
|
+
utils,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const releaseCommand = commands.find((command) => command.startsWith("npm run release"));
|
|
432
|
+
assert.ok(releaseCommand, "release command should run");
|
|
433
|
+
assert.ok(
|
|
434
|
+
releaseCommand.includes("-- --release-as minor"),
|
|
435
|
+
"release command should append provided releaseType"
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("perform_release auto-detects commit when tree is clean", async () => {
|
|
441
|
+
await withWorkflowState(async (workflowState) => {
|
|
442
|
+
workflowState.state.currentPhase = "release";
|
|
443
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
444
|
+
workflowState.state.readyCheckCompleted = true;
|
|
445
|
+
workflowState.state.lastPushBranch = "";
|
|
446
|
+
workflowState.state.lastCommitMessage = "";
|
|
447
|
+
await workflowState.save();
|
|
448
|
+
|
|
449
|
+
const commands = [];
|
|
450
|
+
const execStub = async (command) => {
|
|
451
|
+
commands.push(command);
|
|
452
|
+
if (command === "git status --porcelain") {
|
|
453
|
+
return { stdout: "" };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { stdout: "" };
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const git = {
|
|
460
|
+
hasWorkingChanges: async () => false,
|
|
461
|
+
hasStagedChanges: async () => false,
|
|
462
|
+
hasTestChanges: async () => true,
|
|
463
|
+
getStagedChanges: async () => [],
|
|
464
|
+
getCurrentBranch: async () => "main",
|
|
465
|
+
getLastCommitMessage: async () => "chore: already pushed",
|
|
466
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const response = await handleToolCall({
|
|
470
|
+
request: createRequest("perform_release", { command: "npm run release" }),
|
|
471
|
+
normalizeRequestArgs,
|
|
472
|
+
workflowState,
|
|
473
|
+
exec: execStub,
|
|
474
|
+
git,
|
|
475
|
+
utils,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const pushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
|
|
479
|
+
assert.ok(pushCommand, "git push with tags should still run");
|
|
480
|
+
assert.equal(workflowState.state.commitAndPushCompleted, true);
|
|
481
|
+
assert.equal(workflowState.state.lastCommitMessage, "chore: already pushed");
|
|
482
|
+
assert.equal(workflowState.state.lastPushBranch, "main");
|
|
483
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
484
|
+
assert.ok(
|
|
485
|
+
response.content[0].text.includes("Detected clean working tree"),
|
|
486
|
+
"response should mention auto-detected commit"
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
package/tools/definitions.js
CHANGED
|
@@ -163,6 +163,24 @@ export function getToolList() {
|
|
|
163
163
|
required: ["commitMessage"],
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
|
+
{
|
|
167
|
+
name: "force_complete_task",
|
|
168
|
+
description:
|
|
169
|
+
"Force completion of the current task even if workflow steps are incomplete. Records history entry and resets the workflow.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
commitMessage: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Commit message or summary to record (optional)",
|
|
176
|
+
},
|
|
177
|
+
reason: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Why the task was force completed",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
166
184
|
{
|
|
167
185
|
name: "drop_task",
|
|
168
186
|
description:
|
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) {
|
|
@@ -519,6 +638,32 @@ async function handleCompleteTask(args, workflowState) {
|
|
|
519
638
|
);
|
|
520
639
|
}
|
|
521
640
|
|
|
641
|
+
async function handleForceCompleteTask(args, workflowState) {
|
|
642
|
+
const commitMessageArg =
|
|
643
|
+
typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
|
|
644
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
645
|
+
|
|
646
|
+
const commitMessage =
|
|
647
|
+
commitMessageArg || workflowState.state.lastCommitMessage || "(no commit recorded)";
|
|
648
|
+
|
|
649
|
+
workflowState.addToHistory({
|
|
650
|
+
taskDescription: workflowState.state.taskDescription,
|
|
651
|
+
taskType: workflowState.state.taskType,
|
|
652
|
+
commitMessage,
|
|
653
|
+
forced: true,
|
|
654
|
+
forceReason: reason,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
workflowState.reset();
|
|
658
|
+
await workflowState.save();
|
|
659
|
+
|
|
660
|
+
const reasonText = reason ? reason : "(none provided)";
|
|
661
|
+
|
|
662
|
+
return textResponse(
|
|
663
|
+
`ā ļø Task force-completed.\n\nRecorded commit message: ${commitMessage}\nReason: ${reasonText}\nWorkflow state reset. Start a new task when ready.`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
522
667
|
async function handleDropTask(args, workflowState) {
|
|
523
668
|
workflowState.addToHistory({
|
|
524
669
|
taskDescription: workflowState.state.taskDescription,
|
|
@@ -600,6 +745,8 @@ export async function handleToolCall({
|
|
|
600
745
|
return handlePerformRelease(args, { workflowState, exec, git, utils });
|
|
601
746
|
case "complete_task":
|
|
602
747
|
return handleCompleteTask(args, workflowState);
|
|
748
|
+
case "force_complete_task":
|
|
749
|
+
return handleForceCompleteTask(args, workflowState);
|
|
603
750
|
case "drop_task":
|
|
604
751
|
return handleDropTask(args, workflowState);
|
|
605
752
|
case "get_workflow_status":
|
|
@@ -607,7 +754,7 @@ export async function handleToolCall({
|
|
|
607
754
|
case "view_history":
|
|
608
755
|
return handleViewHistory(args, workflowState);
|
|
609
756
|
case "continue_workflow":
|
|
610
|
-
return handleContinueWorkflow(workflowState);
|
|
757
|
+
return handleContinueWorkflow(workflowState, { workflowState, exec, git, utils });
|
|
611
758
|
default:
|
|
612
759
|
throw new Error(`Unknown tool: ${name}`);
|
|
613
760
|
}
|