@programinglive/dev-workflow-mcp-server 1.7.17 → 1.8.1
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 +16 -0
- package/README.md +8 -0
- package/docs/{product/PRD.md → PRD.md} +5 -2
- package/docs/release-notes/RELEASE_NOTES.md +18 -0
- package/git-helpers.js +9 -0
- package/package.json +1 -1
- package/tests/handlers-commit.test.js +174 -0
- package/tests/handlers-release.test.js +212 -0
- package/tests/handlers-workflow.test.js +68 -0
- package/tests/handlers.test.js +40 -0
- package/tests/test-helpers.js +50 -0
- package/tools/handler-modules/commit-handlers.js +159 -0
- package/tools/handler-modules/index.js +222 -0
- package/tools/handler-modules/release-handlers.js +193 -0
- package/tools/handler-modules/status-handlers.js +134 -0
- package/tools/handler-modules/summary-handlers.js +46 -0
- package/tools/handler-modules/workflow-handlers.js +219 -0
- package/tools/handlers-new.js +1 -0
- package/tools/handlers.js +1 -1091
package/tests/handlers.test.js
CHANGED
|
@@ -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
|
|
|
@@ -994,6 +1028,8 @@ test("skip_release records justification and advances to completion phase", asyn
|
|
|
994
1028
|
getCurrentBranch: async () => "main",
|
|
995
1029
|
getPrimaryBranch: async () => "main",
|
|
996
1030
|
getLastCommitMessage: async () => "chore: docs",
|
|
1031
|
+
getStatusOutput: async () => "",
|
|
1032
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
997
1033
|
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
998
1034
|
},
|
|
999
1035
|
utils,
|
|
@@ -1033,6 +1069,8 @@ test("perform_release respects explicit releaseType option", async () => {
|
|
|
1033
1069
|
getStagedChanges: async () => [],
|
|
1034
1070
|
getCurrentBranch: async () => "main",
|
|
1035
1071
|
getLastCommitMessage: async () => "feat: import improvements",
|
|
1072
|
+
getStatusOutput: async () => "",
|
|
1073
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
1036
1074
|
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
1037
1075
|
};
|
|
1038
1076
|
|
|
@@ -1079,6 +1117,8 @@ test("perform_release requires commit_and_push completion when tree is clean", a
|
|
|
1079
1117
|
getStagedChanges: async () => [],
|
|
1080
1118
|
getCurrentBranch: async () => "main",
|
|
1081
1119
|
getLastCommitMessage: async () => "chore: already pushed",
|
|
1120
|
+
getStatusOutput: async () => "",
|
|
1121
|
+
containsTestFilesInStatus: (statusOutput) => statusOutput.includes("tests/"),
|
|
1082
1122
|
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
1083
1123
|
};
|
|
1084
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
|
+
}
|
|
@@ -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
|
+
}
|