@programinglive/dev-workflow-mcp-server 1.7.16 → 1.8.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.
@@ -147,6 +147,8 @@ test("commit_and_push commits changes and pushes", async () => {
147
147
  getCurrentBranch: async () => "main",
148
148
  getPrimaryBranch: async () => "main",
149
149
  getLastCommitMessage: async () => "",
150
+ getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
151
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
150
152
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
151
153
  };
152
154
 
@@ -215,6 +217,8 @@ test("run_full_workflow iterates until commit/release complete with clean tree",
215
217
  getCurrentBranch: async () => "main",
216
218
  getPrimaryBranch: async () => "main",
217
219
  getLastCommitMessage: async () => "chore: existing",
220
+ getStatusOutput: async () => "",
221
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
218
222
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
219
223
  };
220
224
 
@@ -272,6 +276,8 @@ test("commit_and_push uses primary branch when no branch specified", async () =>
272
276
  getCurrentBranch: async () => "feature/test",
273
277
  getPrimaryBranch: async () => "main",
274
278
  getLastCommitMessage: async () => "",
279
+ getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
280
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
275
281
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
276
282
  };
277
283
 
@@ -321,6 +327,8 @@ test("commit_and_push falls back to master when main not found", async () => {
321
327
  getCurrentBranch: async () => "feature/test",
322
328
  getPrimaryBranch: async () => "master",
323
329
  getLastCommitMessage: async () => "",
330
+ getStatusOutput: async () => "M tests/handlers.test.js\nM src/index.js",
331
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
324
332
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
325
333
  };
326
334
 
@@ -377,6 +385,8 @@ test("run_full_workflow uses provided branch for release push when commit is cle
377
385
  getCurrentBranch: async () => "feature/local",
378
386
  getPrimaryBranch: async () => "main",
379
387
  getLastCommitMessage: async () => "chore: existing",
388
+ getStatusOutput: async () => "",
389
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
380
390
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
381
391
  };
382
392
 
@@ -433,6 +443,8 @@ test("project_summary_data reads persisted file and falls back", async () => {
433
443
  getCurrentBranch: async () => "main",
434
444
  getPrimaryBranch: async () => "main",
435
445
  getLastCommitMessage: async () => "",
446
+ getStatusOutput: async () => "",
447
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
436
448
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
437
449
  },
438
450
  utils,
@@ -467,6 +479,8 @@ test("project_summary aggregates task types and recent activity", async () => {
467
479
  getCurrentBranch: async () => "main",
468
480
  getPrimaryBranch: async () => "main",
469
481
  getLastCommitMessage: async () => "",
482
+ getStatusOutput: async () => "",
483
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
470
484
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
471
485
  },
472
486
  utils,
@@ -510,6 +524,8 @@ test("run_full_workflow executes all steps successfully", async () => {
510
524
  getCurrentBranch: async () => "main",
511
525
  getPrimaryBranch: async () => "main",
512
526
  getLastCommitMessage: async () => "",
527
+ getStatusOutput: async () => workingChanges ? "M tests/handlers.test.js\nM src/app.js" : "",
528
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
513
529
  workingTreeSummary: () =>
514
530
  workingChanges
515
531
  ? { hasChanges: true, lines: ["M src/app.js"] }
@@ -587,6 +603,8 @@ test("run_full_workflow resumes from current phase when steps are already comple
587
603
  getCurrentBranch: async () => "main",
588
604
  getPrimaryBranch: async () => "main",
589
605
  getLastCommitMessage: async () => "",
606
+ getStatusOutput: async () => "",
607
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
590
608
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
591
609
  };
592
610
 
@@ -652,6 +670,8 @@ test("run_full_workflow validates required arguments", async () => {
652
670
  getCurrentBranch: async () => "main",
653
671
  getPrimaryBranch: async () => "main",
654
672
  getLastCommitMessage: async () => "",
673
+ getStatusOutput: async () => "",
674
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
655
675
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
656
676
  },
657
677
  utils,
@@ -688,6 +708,8 @@ test("force_complete_task records entry and resets state", async () => {
688
708
  getCurrentBranch: async () => "main",
689
709
  getPrimaryBranch: async () => "main",
690
710
  getLastCommitMessage: async () => "",
711
+ getStatusOutput: async () => "",
712
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
691
713
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
692
714
  },
693
715
  utils,
@@ -727,6 +749,8 @@ test("perform_release blocks when new changes detected", async () => {
727
749
  getCurrentBranch: async () => "main",
728
750
  getPrimaryBranch: async () => "main",
729
751
  getLastCommitMessage: async () => "",
752
+ getStatusOutput: async () => "?? new-file.js",
753
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
730
754
  workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
731
755
  };
732
756
 
@@ -758,6 +782,8 @@ test("continue_workflow warns when workflow is idle", async () => {
758
782
  getCurrentBranch: async () => "main",
759
783
  getPrimaryBranch: async () => "main",
760
784
  getLastCommitMessage: async () => "",
785
+ getStatusOutput: async () => "",
786
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
761
787
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
762
788
  };
763
789
 
@@ -792,6 +818,8 @@ test("continue_workflow resets to commit when new changes detected", async () =>
792
818
  getCurrentBranch: async () => "main",
793
819
  getPrimaryBranch: async () => "main",
794
820
  getLastCommitMessage: async () => "",
821
+ getStatusOutput: async () => "?? new-file.js",
822
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
795
823
  workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
796
824
  };
797
825
 
@@ -837,6 +865,8 @@ test("commit_and_push recognizes already committed work", async () => {
837
865
  getCurrentBranch: async () => "main",
838
866
  getPrimaryBranch: async () => "main",
839
867
  getLastCommitMessage: async () => "fix: previous work",
868
+ getStatusOutput: async () => "",
869
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
840
870
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
841
871
  };
842
872
 
@@ -886,6 +916,8 @@ test("perform_release runs release command before pushing with tags", async () =
886
916
  getStagedChanges: async () => [],
887
917
  getCurrentBranch: async () => "main",
888
918
  getLastCommitMessage: async () => "",
919
+ getStatusOutput: async () => "",
920
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
889
921
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
890
922
  };
891
923
 
@@ -951,6 +983,8 @@ test("perform_release accepts shorthand patch command", async () => {
951
983
  getStagedChanges: async () => [],
952
984
  getCurrentBranch: async () => "main",
953
985
  getLastCommitMessage: async () => "fix: adjust labels",
986
+ getStatusOutput: async () => "",
987
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
954
988
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
955
989
  };
956
990
 
@@ -970,6 +1004,47 @@ test("perform_release accepts shorthand patch command", async () => {
970
1004
  });
971
1005
  });
972
1006
 
1007
+ test("skip_release records justification and advances to completion phase", async () => {
1008
+ await withWorkflowState(async (workflowState) => {
1009
+ workflowState.state.currentPhase = "release";
1010
+ workflowState.state.commitAndPushCompleted = true;
1011
+ workflowState.state.readyCheckCompleted = true;
1012
+ workflowState.state.lastCommitMessage = "chore: docs";
1013
+ workflowState.state.releaseSkipped = false;
1014
+ workflowState.state.released = false;
1015
+ workflowState.state.releaseCommand = "";
1016
+ await workflowState.save();
1017
+
1018
+ const response = await handleToolCall({
1019
+ request: createRequest("skip_release", { reason: "Python project – no npm release" }),
1020
+ normalizeRequestArgs,
1021
+ workflowState,
1022
+ exec: async () => ({ stdout: "" }),
1023
+ git: {
1024
+ hasWorkingChanges: async () => false,
1025
+ hasStagedChanges: async () => false,
1026
+ hasTestChanges: async () => true,
1027
+ getStagedChanges: async () => [],
1028
+ getCurrentBranch: async () => "main",
1029
+ getPrimaryBranch: async () => "main",
1030
+ getLastCommitMessage: async () => "chore: docs",
1031
+ getStatusOutput: async () => "",
1032
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
1033
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
1034
+ },
1035
+ utils,
1036
+ });
1037
+
1038
+ const text = response.content[0].text;
1039
+ assert.ok(text.includes("Release step skipped"), "response should mention release was skipped");
1040
+ assert.equal(workflowState.state.releaseSkipped, true);
1041
+ assert.equal(workflowState.state.releaseSkippedReason, "Python project – no npm release");
1042
+ assert.equal(workflowState.state.releaseCommand, "(skipped)");
1043
+ assert.equal(workflowState.state.currentPhase, "ready_to_complete");
1044
+ assert.equal(workflowState.state.released, false);
1045
+ });
1046
+ });
1047
+
973
1048
  test("perform_release respects explicit releaseType option", async () => {
974
1049
  await withWorkflowState(async (workflowState) => {
975
1050
  workflowState.state.currentPhase = "release";
@@ -994,6 +1069,8 @@ test("perform_release respects explicit releaseType option", async () => {
994
1069
  getStagedChanges: async () => [],
995
1070
  getCurrentBranch: async () => "main",
996
1071
  getLastCommitMessage: async () => "feat: import improvements",
1072
+ getStatusOutput: async () => "",
1073
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
997
1074
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
998
1075
  };
999
1076
 
@@ -1040,6 +1117,8 @@ test("perform_release requires commit_and_push completion when tree is clean", a
1040
1117
  getStagedChanges: async () => [],
1041
1118
  getCurrentBranch: async () => "main",
1042
1119
  getLastCommitMessage: async () => "chore: already pushed",
1120
+ getStatusOutput: async () => "",
1121
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
1043
1122
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
1044
1123
  };
1045
1124
 
@@ -0,0 +1,50 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { WorkflowState } from "../workflow-state.js";
5
+ import { shellEscape } from "../utils.js";
6
+ import { determineReleaseTypeFromCommit, createCommitMessageParts } from "../commit-helpers.js";
7
+
8
+ export async function withWorkflowState(callback) {
9
+ const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-handlers-"));
10
+ const stateFile = path.join(tempDir, "state.json");
11
+ const state = new WorkflowState(stateFile);
12
+ await state.load();
13
+
14
+ try {
15
+ await callback(state);
16
+ } finally {
17
+ await rm(tempDir, { recursive: true, force: true });
18
+ }
19
+ }
20
+
21
+ export const testUtils = {
22
+ shellEscape,
23
+ determineReleaseTypeFromCommit,
24
+ createCommitMessageParts,
25
+ };
26
+
27
+ export function createRequest(name, args) {
28
+ return {
29
+ params: {
30
+ name,
31
+ arguments: args,
32
+ },
33
+ };
34
+ }
35
+
36
+ export function createGitMock(overrides = {}) {
37
+ return {
38
+ hasWorkingChanges: async () => false,
39
+ hasStagedChanges: async () => false,
40
+ hasTestChanges: async () => true,
41
+ getStagedChanges: async () => [],
42
+ getCurrentBranch: async () => "main",
43
+ getPrimaryBranch: async () => "main",
44
+ getLastCommitMessage: async () => "",
45
+ getStatusOutput: async () => "",
46
+ containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
47
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
48
+ ...overrides,
49
+ };
50
+ }
@@ -148,6 +148,21 @@ export function getToolList() {
148
148
  required: ["command"],
149
149
  },
150
150
  },
151
+ {
152
+ name: "skip_release",
153
+ description:
154
+ "Record that the release step was intentionally skipped (e.g., non-Node projects or documentation-only work). Requires a justification.",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ reason: {
159
+ type: "string",
160
+ description: "Why the release step is being skipped",
161
+ },
162
+ },
163
+ required: ["reason"],
164
+ },
165
+ },
151
166
  {
152
167
  name: "complete_task",
153
168
  description:
@@ -0,0 +1,159 @@
1
+ import { textResponse, isReleaseSatisfied, formatChecklist, handleReadyCheck } from "./shared.js";
2
+ import { containsTestFilesInStatus } from "../../git-helpers.js";
3
+
4
+ export async function handleCommitAndPush(args, context) {
5
+ const { workflowState, exec, git, utils } = context;
6
+ if (!workflowState.state.readyToCommit || !workflowState.state.readyCheckCompleted) {
7
+ const readyResponse = await handleReadyCheck(workflowState);
8
+ if (workflowState.state.currentPhase !== "commit") {
9
+ return readyResponse;
10
+ }
11
+ }
12
+
13
+ const providedCommitMessage =
14
+ typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
15
+
16
+ const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
17
+ const primaryBranch = await git.getPrimaryBranch();
18
+ const currentBranch = await git.getCurrentBranch();
19
+ const branchForPush = requestedBranch || primaryBranch || currentBranch || "";
20
+
21
+ const hasWorkingChanges = await git.hasWorkingChanges();
22
+
23
+ if (!hasWorkingChanges) {
24
+ const lastCommitMessage = await git.getLastCommitMessage();
25
+ const effectiveCommitMessage = lastCommitMessage || providedCommitMessage;
26
+
27
+ workflowState.state.commitAndPushCompleted = true;
28
+ workflowState.state.lastCommitMessage = effectiveCommitMessage || "";
29
+ workflowState.state.lastPushBranch = branchForPush;
30
+ workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
31
+ await workflowState.save();
32
+
33
+ return textResponse(
34
+ `ℹ️ No changes detected. Assuming commit and push already completed.\nLast commit: ${effectiveCommitMessage || "(unavailable)"
35
+ }\n\nNext: Run 'perform_release' to handle the release and push tags.`
36
+ );
37
+ }
38
+
39
+ const statusOutput = await git.getStatusOutput();
40
+
41
+ if (!workflowState.state.testsSkipped && !containsTestFilesInStatus(statusOutput)) {
42
+ return textResponse("⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.");
43
+ }
44
+
45
+ try {
46
+ await exec("git add .");
47
+ } catch (error) {
48
+ return textResponse(
49
+ `❌ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`
50
+ );
51
+ }
52
+
53
+ const stagedChanges = await git.getStagedChanges();
54
+ const { summary: generatedSummary, body: commitBody } = utils.createCommitMessageParts(
55
+ stagedChanges,
56
+ providedCommitMessage
57
+ );
58
+
59
+ if (!generatedSummary) {
60
+ return textResponse("⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.");
61
+ }
62
+
63
+ try {
64
+ if (commitBody) {
65
+ await exec(
66
+ `git commit -m ${utils.shellEscape(generatedSummary)} -m ${utils.shellEscape(commitBody)}`
67
+ );
68
+ } else {
69
+ await exec(`git commit -m ${utils.shellEscape(generatedSummary)}`);
70
+ }
71
+ } catch (error) {
72
+ const output = error.stderr || error.stdout || error.message || "Unknown error";
73
+ if (output.includes("nothing to commit")) {
74
+ return textResponse(
75
+ "⚠️ Nothing to commit. Make additional changes before running 'commit_and_push'."
76
+ );
77
+ }
78
+
79
+ return textResponse(`❌ git commit failed:\n\n${output}`);
80
+ }
81
+
82
+ try {
83
+ const pushCommand = branchForPush
84
+ ? `git push origin ${utils.shellEscape(branchForPush)}`
85
+ : "git push";
86
+ await exec(pushCommand);
87
+ } catch (error) {
88
+ return textResponse(
89
+ `❌ git push failed:\n\n${error.stderr || error.stdout || error.message}`
90
+ );
91
+ }
92
+
93
+ workflowState.state.commitAndPushCompleted = true;
94
+ workflowState.state.lastCommitMessage = generatedSummary;
95
+ workflowState.state.lastPushBranch = branchForPush;
96
+ workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
97
+ await workflowState.save();
98
+
99
+ return textResponse(
100
+ `✅ Commit and push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${branchForPush || "(default upstream)"
101
+ }\n\nNext: Run 'perform_release' to handle the release and push tags.`
102
+ );
103
+ }
104
+
105
+ export async function handleCompleteTask(args, workflowState) {
106
+ const entry = {
107
+ taskDescription: workflowState.state.taskDescription,
108
+ taskType: workflowState.state.taskType,
109
+ commitMessage: args.commitMessage,
110
+ };
111
+
112
+ workflowState.addToHistory(entry);
113
+ workflowState.reset();
114
+ await workflowState.save();
115
+
116
+ return textResponse(
117
+ `✅ Task completed!\n\nCommit message: ${args.commitMessage}\nHistory recorded.`
118
+ );
119
+ }
120
+
121
+ export async function handleForceCompleteTask(args, workflowState) {
122
+ const commitMessageArg =
123
+ typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
124
+ const reason = typeof args.reason === "string" ? args.reason.trim() : "";
125
+
126
+ const commitMessage =
127
+ commitMessageArg || workflowState.state.lastCommitMessage || "(no commit recorded)";
128
+
129
+ workflowState.addToHistory({
130
+ taskDescription: workflowState.state.taskDescription,
131
+ taskType: workflowState.state.taskType,
132
+ commitMessage,
133
+ forced: true,
134
+ forceReason: reason,
135
+ });
136
+
137
+ workflowState.reset();
138
+ await workflowState.save();
139
+
140
+ const reasonText = reason ? reason : "(none provided)";
141
+
142
+ return textResponse(
143
+ `⚠️ Task force-completed.\n\nRecorded commit message: ${commitMessage}\nReason: ${reasonText}\nWorkflow state reset. Start a new task when ready.`
144
+ );
145
+ }
146
+
147
+ export async function handleDropTask(args, workflowState) {
148
+ workflowState.addToHistory({
149
+ taskDescription: workflowState.state.taskDescription,
150
+ taskType: workflowState.state.taskType,
151
+ commitMessage: workflowState.state.lastCommitMessage,
152
+ dropped: true,
153
+ dropReason: typeof args.reason === "string" ? args.reason : "",
154
+ });
155
+ workflowState.reset();
156
+ await workflowState.save();
157
+
158
+ return textResponse("⚠️ Task dropped. Workflow state reset.");
159
+ }
@@ -0,0 +1,222 @@
1
+ import { textResponse, isReleaseSatisfied } from "./shared.js";
2
+ import { handleStartTask, handleMarkBugFixed, handleCreateTests, handleSkipTests, handleRunTests, handleCreateDocumentation } from "./workflow-handlers.js";
3
+ import { handleSkipRelease, handlePerformRelease } from "./release-handlers.js";
4
+ import { handleCommitAndPush, handleCompleteTask, handleForceCompleteTask, handleDropTask } from "./commit-handlers.js";
5
+ import { handleProjectSummary, handleProjectSummaryData, handleProjectSummaryDb, handleViewHistory } from "./summary-handlers.js";
6
+ import { handleGetWorkflowStatus, handleReadyCheck, handleContinueWorkflow, handleRerunWorkflow } from "./status-handlers.js";
7
+
8
+ function isErrorResponse(response) {
9
+ const text = response?.content?.[0]?.text || "";
10
+ return /^\s*[⚠️❌]/u.test(text);
11
+ }
12
+
13
+ function summarizeResponse(label, response) {
14
+ const text = response?.content?.[0]?.text || "";
15
+ const firstLine = text.split("\n")[0] || "";
16
+ return `${label}: ${firstLine}`;
17
+ }
18
+
19
+ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils }) {
20
+ const description = workflowState.state.taskDescription;
21
+ if (!description) {
22
+ return textResponse("⚠️ Please start a task first using 'start_task' before running the full workflow.");
23
+ }
24
+
25
+ await workflowState.ensurePrimaryFile();
26
+
27
+ const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
28
+ if (requestedBranch) {
29
+ workflowState.state.lastPushBranch = requestedBranch;
30
+ await workflowState.save();
31
+ }
32
+
33
+ const requiredStrings = [
34
+ { key: "summary", message: "Provide a non-empty 'summary' for mark_bug_fixed." },
35
+ { key: "testCommand", message: "Provide a non-empty 'testCommand' for run_tests." },
36
+ { key: "documentationType", message: "Provide 'documentationType' for create_documentation." },
37
+ { key: "documentationSummary", message: "Provide a non-empty 'documentationSummary'." },
38
+ { key: "commitMessage", message: "Provide a non-empty 'commitMessage'." },
39
+ { key: "releaseCommand", message: "Provide a non-empty 'releaseCommand'." },
40
+ ];
41
+
42
+ for (const { key, message } of requiredStrings) {
43
+ const value = args[key];
44
+ if (typeof value !== "string" || value.trim() === "") {
45
+ return textResponse(`⚠️ ${message}`);
46
+ }
47
+ }
48
+
49
+ const steps = [];
50
+ const testsPassed = typeof args.testsPassed === "boolean" ? args.testsPassed : true;
51
+ const testDetails = typeof args.testDetails === "string" ? args.testDetails : "";
52
+ const releaseArgs = {
53
+ command: args.releaseCommand,
54
+ };
55
+ if (typeof args.releaseNotes === "string" && args.releaseNotes.trim() !== "") {
56
+ releaseArgs.notes = args.releaseNotes;
57
+ }
58
+ if (typeof args.releaseType === "string" && args.releaseType.trim() !== "") {
59
+ releaseArgs.releaseType = args.releaseType;
60
+ }
61
+ if (typeof args.preset === "string" && args.preset.trim() !== "") {
62
+ releaseArgs.preset = args.preset;
63
+ }
64
+
65
+ async function executeStep(name, handlerFn, handlerArgs = []) {
66
+ if (isErrorResponse(handlerFn)) {
67
+ return handlerFn;
68
+ }
69
+ const response = await handlerFn(...handlerArgs);
70
+ if (isErrorResponse(response)) {
71
+ return response;
72
+ }
73
+ steps.push(summarizeResponse(name, response));
74
+ await workflowState.load();
75
+ return response;
76
+ }
77
+
78
+ while (true) {
79
+ const state = workflowState.state;
80
+
81
+ if (!state.bugFixed) {
82
+ const resp = await executeStep("mark_bug_fixed", handleMarkBugFixed, [{ summary: args.summary }, workflowState]);
83
+ if (isErrorResponse(resp)) return resp;
84
+ continue;
85
+ }
86
+
87
+ if (!state.testsCreated) {
88
+ const resp = await executeStep("create_tests", handleCreateTests, [workflowState]);
89
+ if (isErrorResponse(resp)) return resp;
90
+ continue;
91
+ }
92
+
93
+ if (!state.testsPassed && !state.testsSkipped) {
94
+ const resp = await executeStep("run_tests", handleRunTests, [
95
+ { passed: testsPassed, testCommand: args.testCommand, details: testDetails },
96
+ workflowState,
97
+ ]);
98
+ if (isErrorResponse(resp)) return resp;
99
+ continue;
100
+ }
101
+
102
+ if (!state.documentationCreated) {
103
+ const resp = await executeStep("create_documentation", handleCreateDocumentation, [
104
+ { documentationType: args.documentationType, summary: args.documentationSummary },
105
+ workflowState,
106
+ ]);
107
+ if (isErrorResponse(resp)) return resp;
108
+ continue;
109
+ }
110
+
111
+ if (!state.readyCheckCompleted) {
112
+ const resp = await executeStep("check_ready_to_commit", handleReadyCheck, [workflowState]);
113
+ if (isErrorResponse(resp)) return resp;
114
+ continue;
115
+ }
116
+
117
+ if (!state.commitAndPushCompleted) {
118
+ const resp = await executeStep("commit_and_push", handleCommitAndPush, [
119
+ {
120
+ commitMessage: args.commitMessage,
121
+ branch: requestedBranch,
122
+ },
123
+ { workflowState, exec, git, utils },
124
+ ]);
125
+ if (isErrorResponse(resp)) return resp;
126
+ continue;
127
+ }
128
+
129
+ if (!isReleaseSatisfied(state)) {
130
+ const resp = await executeStep("perform_release", handlePerformRelease, [
131
+ releaseArgs,
132
+ { workflowState, exec, git, utils },
133
+ ]);
134
+ if (isErrorResponse(resp)) return resp;
135
+ continue;
136
+ }
137
+
138
+ const completeResponse = await handleCompleteTask({ commitMessage: args.commitMessage }, workflowState);
139
+ if (isErrorResponse(completeResponse)) {
140
+ return completeResponse;
141
+ }
142
+ steps.push(summarizeResponse("complete_task", completeResponse));
143
+ break;
144
+ }
145
+
146
+ const summary = steps.map((line, index) => `${index + 1}. ${line}`).join("\n");
147
+ const celebration = workflowState.state.history?.length
148
+ ? "🎉 Cheers! We solved another issue and captured it in the knowledge base."
149
+ : "";
150
+ const message = [`✅ Full workflow completed successfully!`, celebration, summary].filter(Boolean).join("\n\n");
151
+ return textResponse(message);
152
+ }
153
+
154
+ export async function handleToolCall({
155
+ request,
156
+ normalizeRequestArgs,
157
+ workflowState,
158
+ exec,
159
+ git,
160
+ utils,
161
+ }) {
162
+ try {
163
+ const { name } = request.params;
164
+ const { args, error: argumentsError } = normalizeRequestArgs(request.params.arguments);
165
+
166
+ if (argumentsError) {
167
+ return textResponse(argumentsError);
168
+ }
169
+
170
+ switch (name) {
171
+ case "start_task":
172
+ return handleStartTask(args, workflowState);
173
+ case "mark_bug_fixed":
174
+ return handleMarkBugFixed(args, workflowState);
175
+ case "create_tests":
176
+ return handleCreateTests(workflowState);
177
+ case "skip_tests":
178
+ return handleSkipTests(args, workflowState);
179
+ case "run_tests":
180
+ return handleRunTests(args, workflowState);
181
+ case "create_documentation":
182
+ return handleCreateDocumentation(args, workflowState);
183
+ case "check_ready_to_commit":
184
+ return handleReadyCheck(workflowState);
185
+ case "commit_and_push":
186
+ return handleCommitAndPush(args, { workflowState, exec, git, utils });
187
+ case "perform_release":
188
+ return handlePerformRelease(args, { workflowState, exec, git, utils });
189
+ case "skip_release":
190
+ return handleSkipRelease(args, workflowState);
191
+ case "complete_task":
192
+ return handleCompleteTask(args, workflowState);
193
+ case "force_complete_task":
194
+ return handleForceCompleteTask(args, workflowState);
195
+ case "drop_task":
196
+ return handleDropTask(args, workflowState);
197
+ case "get_workflow_status":
198
+ return handleGetWorkflowStatus(workflowState);
199
+ case "view_history":
200
+ return handleViewHistory(args, workflowState);
201
+ case "project_summary":
202
+ return handleProjectSummary(workflowState);
203
+ case "project_summary_data":
204
+ return handleProjectSummaryData(workflowState);
205
+ case "project_summary_db":
206
+ return handleProjectSummaryDb(workflowState);
207
+ case "continue_workflow":
208
+ return handleContinueWorkflow(workflowState, { workflowState, exec, git, utils });
209
+ case "rerun_workflow":
210
+ return handleRerunWorkflow(workflowState);
211
+ case "run_full_workflow":
212
+ return handleRunFullWorkflow(args, { workflowState, exec, git, utils });
213
+ default:
214
+ throw new Error(`Unknown tool: ${name}`);
215
+ }
216
+ } catch (error) {
217
+ console.error("Tool handler error:", error);
218
+ return textResponse(
219
+ "⚠️ Internal server error while processing the tool request. Please try again."
220
+ );
221
+ }
222
+ }