@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 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 drive the git commands once verification passes
15
- 7. **Release** - Record the release details before closing out the task
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.0.11",
3
+ "version": "1.1.2",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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
+ });
@@ -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
- if (!(await git.hasWorkingChanges())) {
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
- "āš ļø No changes detected. Make sure you have modifications to commit before running 'commit_and_push'."
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 (!workflowState.state.commitAndPushCompleted) {
471
+ if (await resetToCommitIfWorkingChanges(workflowState, git)) {
422
472
  return textResponse(
423
- "āš ļø Please run 'commit_and_push' after the ready check before recording a release!"
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
- const releaseCommand = typeof args.command === "string" ? args.command.trim() : "";
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
- const releaseType = utils.determineReleaseTypeFromCommit(
464
- lastCommitMessage || workflowState.state.lastCommitMessage
465
- );
573
+ if (!releaseType) {
574
+ releaseType = utils.determineReleaseTypeFromCommit(
575
+ lastCommitMessage || workflowState.state.lastCommitMessage
576
+ );
577
+ }
466
578
 
467
- const finalCommand = releaseCommand.includes("--release-as")
468
- ? releaseCommand
469
- : `${releaseCommand} -- --release-as ${releaseType}`;
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
- return textResponse("šŸ“œ No workflow history yet.");
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
- return textResponse(message);
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
  }