@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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -2
- package/docs/release-notes/RELEASE_NOTES.md +10 -0
- package/git-helpers.js +9 -0
- package/package.json +4 -2
- package/scripts/release.cjs +28 -1
- package/tests/build.test.js +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 +79 -0
- package/tests/test-helpers.js +50 -0
- package/tools/definitions.js +15 -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/shared.js +187 -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 -1030
- package/workflow-state.js +10 -0
- /package/docs/{product/PRD.md → PRD.md} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { textResponse, isReleaseSatisfied, resetToCommitIfWorkingChanges, summarizeHistory } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
export async function handleSkipRelease(args, workflowState) {
|
|
4
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
5
|
+
return textResponse("⚠️ Please start a task first using 'start_task' before skipping release.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
9
|
+
if (!reason) {
|
|
10
|
+
return textResponse("⚠️ Provide a non-empty 'reason' explaining why the release step is being skipped.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (workflowState.state.releaseSkipped) {
|
|
14
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
15
|
+
await workflowState.save();
|
|
16
|
+
return textResponse(
|
|
17
|
+
`ℹ️ Release step was already skipped for this task.\nUpdated reason: ${reason}\n\nNext: run 'complete_task' to wrap up.`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (workflowState.state.released) {
|
|
22
|
+
return textResponse("⚠️ A release command has already been recorded. Continue with 'complete_task'.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
26
|
+
return textResponse("⚠️ Please run 'check_ready_to_commit' and ensure all prerequisites pass before skipping release.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
30
|
+
return textResponse("⚠️ Please complete 'commit_and_push' before skipping release.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
workflowState.state.releaseSkipped = true;
|
|
34
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
35
|
+
workflowState.state.released = false;
|
|
36
|
+
workflowState.state.releaseCommand = "(skipped)";
|
|
37
|
+
workflowState.state.releaseNotes = "";
|
|
38
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
39
|
+
await workflowState.save();
|
|
40
|
+
|
|
41
|
+
return textResponse(
|
|
42
|
+
`⚠️ Release step skipped.\nReason: ${reason}\n\n✅ Next: run 'complete_task' with your final commit message to close out the workflow.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handlePerformRelease(args, context) {
|
|
47
|
+
const { workflowState, exec, git, utils } = context;
|
|
48
|
+
|
|
49
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
50
|
+
return textResponse("⚠️ Please start a task first using 'start_task'!");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (await resetToCommitIfWorkingChanges(workflowState, git)) {
|
|
54
|
+
return textResponse(
|
|
55
|
+
"⚠️ Detected new changes after the last commit. Please run 'commit_and_push' again before recording a release."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
60
|
+
return textResponse(
|
|
61
|
+
"⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
66
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
67
|
+
|
|
68
|
+
if (hasWorkingChanges) {
|
|
69
|
+
return textResponse(
|
|
70
|
+
"⚠️ Please run 'commit_and_push' after the ready check before recording a release!"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
workflowState.state.currentPhase = "commit";
|
|
75
|
+
await workflowState.save();
|
|
76
|
+
|
|
77
|
+
return textResponse(
|
|
78
|
+
"⚠️ Please run 'commit_and_push' after the ready check before recording a release!"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rawReleaseCommand = typeof args.command === "string" ? args.command.trim() : "";
|
|
83
|
+
const releaseTypeArg =
|
|
84
|
+
typeof args.releaseType === "string" ? args.releaseType.trim().toLowerCase() : "";
|
|
85
|
+
const presetArg = typeof args.preset === "string" ? args.preset.trim().toLowerCase() : "";
|
|
86
|
+
const recognizedTypes = new Set(["major", "minor", "patch"]);
|
|
87
|
+
|
|
88
|
+
const tokens = rawReleaseCommand
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.map((token) => token.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
|
|
94
|
+
let releaseType = "";
|
|
95
|
+
if (recognizedTypes.has(releaseTypeArg)) {
|
|
96
|
+
releaseType = releaseTypeArg;
|
|
97
|
+
} else if (recognizedTypes.has(presetArg)) {
|
|
98
|
+
releaseType = presetArg;
|
|
99
|
+
} else if (tokens.length > 0) {
|
|
100
|
+
const possibleType = tokens[tokens.length - 1];
|
|
101
|
+
if (recognizedTypes.has(possibleType)) {
|
|
102
|
+
releaseType = possibleType;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let releaseCommand = rawReleaseCommand;
|
|
107
|
+
if (releaseCommand && recognizedTypes.has(releaseCommand.toLowerCase())) {
|
|
108
|
+
releaseType = releaseCommand.toLowerCase();
|
|
109
|
+
releaseCommand = "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const scriptTypeMatch = /release:(major|minor|patch)/i.exec(releaseCommand);
|
|
113
|
+
if (scriptTypeMatch) {
|
|
114
|
+
releaseType = scriptTypeMatch[1].toLowerCase();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!releaseCommand && releaseType) {
|
|
118
|
+
releaseCommand = `npm run release:${releaseType}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!releaseCommand) {
|
|
122
|
+
return textResponse("⚠️ Please provide a non-empty 'command' when using 'perform_release'.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let statusOutput;
|
|
126
|
+
try {
|
|
127
|
+
const { stdout } = await exec("git status --porcelain");
|
|
128
|
+
statusOutput = stdout;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const details = error.stderr || error.stdout || error.message || "Unknown error";
|
|
131
|
+
return textResponse(
|
|
132
|
+
`⚠️ Unable to verify Git status. Refusing to run the release command until the repository state can be checked.\n\nDetails:\n${details}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const statusSummary = git.workingTreeSummary(statusOutput);
|
|
137
|
+
if (statusSummary.hasChanges) {
|
|
138
|
+
const changeDetails =
|
|
139
|
+
statusSummary.lines.length > 0
|
|
140
|
+
? statusSummary.lines.map((line) => `• ${line}`).join("\n")
|
|
141
|
+
: "• (unable to list changes)";
|
|
142
|
+
|
|
143
|
+
return textResponse(
|
|
144
|
+
`⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.\n\nDetected changes:\n${changeDetails}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lastCommitMessage = await git.getLastCommitMessage();
|
|
149
|
+
if (!releaseType) {
|
|
150
|
+
releaseType = utils.determineReleaseTypeFromCommit(
|
|
151
|
+
lastCommitMessage || workflowState.state.lastCommitMessage
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const finalCommand =
|
|
156
|
+
releaseCommand.includes("--release-as") || /release:(major|minor|patch)/i.test(releaseCommand)
|
|
157
|
+
? releaseCommand
|
|
158
|
+
: `${releaseCommand} -- --release-as ${releaseType}`;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await exec(finalCommand);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return textResponse(
|
|
164
|
+
`❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const branchForPush = workflowState.state.lastPushBranch || (await git.getCurrentBranch()) || "";
|
|
169
|
+
const pushCommand = branchForPush
|
|
170
|
+
? `git push --follow-tags origin ${utils.shellEscape(branchForPush)}`
|
|
171
|
+
: "git push --follow-tags";
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await exec(pushCommand);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return textResponse(
|
|
177
|
+
`❌ git push (with tags) failed:\n\n${error.stderr || error.stdout || error.message}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
workflowState.state.released = true;
|
|
182
|
+
workflowState.state.releaseCommand = finalCommand;
|
|
183
|
+
workflowState.state.releaseNotes = typeof args.notes === "string" ? args.notes : "";
|
|
184
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
185
|
+
await workflowState.save();
|
|
186
|
+
|
|
187
|
+
const message = summarizeHistory(workflowState.state.history, args.limit || 10);
|
|
188
|
+
if (!message) {
|
|
189
|
+
return textResponse("📜 No workflow history yet.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return textResponse(message);
|
|
193
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
export const PROJECT_SUMMARY_FILENAME = "project-summary.json";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export const textResponse = (text) => ({
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export function isReleaseSatisfied(state) {
|
|
21
|
+
return Boolean(state?.released || state?.releaseSkipped);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
25
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hasWorkingChanges = await git.hasWorkingChanges();
|
|
30
|
+
const hasStagedChanges = await git.hasStagedChanges();
|
|
31
|
+
|
|
32
|
+
if (!hasWorkingChanges && !hasStagedChanges) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
workflowState.state.commitAndPushCompleted = false;
|
|
37
|
+
workflowState.state.released = false;
|
|
38
|
+
workflowState.state.releaseSkipped = false;
|
|
39
|
+
workflowState.state.releaseSkippedReason = "";
|
|
40
|
+
workflowState.state.currentPhase = "commit";
|
|
41
|
+
workflowState.state.releaseCommand = "";
|
|
42
|
+
workflowState.state.releaseNotes = "";
|
|
43
|
+
await workflowState.save();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatChecklist(checks) {
|
|
48
|
+
return checks.map((c) => `${c.done ? "✅" : "❌"} ${c.name}`).join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function summarizeHistory(history, limit) {
|
|
52
|
+
const recent = history.slice(-limit).reverse();
|
|
53
|
+
if (recent.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const historyText = recent
|
|
58
|
+
.map(
|
|
59
|
+
(entry, index) =>
|
|
60
|
+
`${index + 1}. [${new Date(entry.timestamp).toLocaleString()}]\n Task: ${entry.taskDescription}\n Type: ${entry.taskType}\n Commit: ${entry.commitMessage}\n`
|
|
61
|
+
)
|
|
62
|
+
.join("\n");
|
|
63
|
+
|
|
64
|
+
return `📜 Workflow History (last ${recent.length} tasks)\n\n${historyText}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function generateProjectSummary(history) {
|
|
68
|
+
if (!history || history.length === 0) {
|
|
69
|
+
return "No workflow history available.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const taskTypes = {};
|
|
73
|
+
const recentTasks = history.slice(-20);
|
|
74
|
+
|
|
75
|
+
for (const entry of recentTasks) {
|
|
76
|
+
const type = entry.taskType || "other";
|
|
77
|
+
taskTypes[type] = (taskTypes[type] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const typeSummary = Object.entries(taskTypes)
|
|
81
|
+
.map(([type, count]) => `${type}: ${count}`)
|
|
82
|
+
.join(", ");
|
|
83
|
+
|
|
84
|
+
const totalTasks = history.length;
|
|
85
|
+
const lastTask = history[history.length - 1];
|
|
86
|
+
const lastActive = lastTask ? new Date(lastTask.timestamp).toLocaleDateString() : "Never";
|
|
87
|
+
|
|
88
|
+
return `🧠 Project Knowledge Summary\n\n📊 Stats\n- Total tasks completed: ${totalTasks}\n- Recent task mix: ${typeSummary}\n- Last active: ${lastActive}\n\n📝 Recent tasks (last 5)\n${history.slice(-5).reverse().map((e, i) => `${i + 1}. ${e.taskDescription} (${e.taskType})`).join("\n")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getContinueGuidance(status) {
|
|
92
|
+
switch (status.currentPhase) {
|
|
93
|
+
case "coding": {
|
|
94
|
+
const task = status.taskDescription ? ` "${status.taskDescription}"` : "";
|
|
95
|
+
return `Finish implementing${task} and then run 'mark_bug_fixed' with a brief summary of the change.`;
|
|
96
|
+
}
|
|
97
|
+
case "testing":
|
|
98
|
+
if (!status.testsCreated) {
|
|
99
|
+
return "Create automated coverage now using 'create_tests' before recording any results.";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (status.testsPassed) {
|
|
103
|
+
return "With tests green, move on to 'create_documentation' and capture what changed.";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (status.testsSkipped) {
|
|
107
|
+
return "Document why tests were skipped using 'create_documentation' so the workflow can proceed.";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return status.testCommand
|
|
111
|
+
? `Fix any failing tests and re-run '${status.testCommand}'. Once they pass, record the results with 'run_tests'.`
|
|
112
|
+
: "Run your automated tests and record the outcome with 'run_tests'. Fix any failures before moving forward.";
|
|
113
|
+
case "documentation":
|
|
114
|
+
return status.testsSkipped
|
|
115
|
+
? `Document the change (including manual QA for skipped tests) with 'create_documentation'. Reason recorded: ${status.testsSkippedReason || "(none provided)"}.`
|
|
116
|
+
: "Capture your updates with 'create_documentation' (provide the documentation type and a short summary).";
|
|
117
|
+
case "ready":
|
|
118
|
+
return "Run 'check_ready_to_commit' to verify all prerequisite steps before committing.";
|
|
119
|
+
case "commit":
|
|
120
|
+
return "All checks passed—run 'commit_and_push' with your commit message (and optional branch).";
|
|
121
|
+
case "release":
|
|
122
|
+
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes, or call 'skip_release' with a justification if no release is required.";
|
|
123
|
+
case "ready_to_complete":
|
|
124
|
+
return "Release recorded. Finish up by calling 'complete_task' with the commit message you used.";
|
|
125
|
+
default:
|
|
126
|
+
return "Review your workflow progress and complete the next required step.";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function handleReadyCheck(workflowState) {
|
|
131
|
+
const status = workflowState.state;
|
|
132
|
+
const testsCreatedDone = status.testsSkipped || status.testsCreated;
|
|
133
|
+
const testsPassedDone = status.testsSkipped || status.testsPassed;
|
|
134
|
+
const checks = [
|
|
135
|
+
{ name: "Task started", done: status.currentPhase !== "idle" },
|
|
136
|
+
{ name: "Feature/bug fixed", done: status.bugFixed },
|
|
137
|
+
{
|
|
138
|
+
name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
|
|
139
|
+
done: testsCreatedDone,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
|
|
143
|
+
done: testsPassedDone,
|
|
144
|
+
},
|
|
145
|
+
{ name: "Documentation created", done: status.documentationCreated },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const allDone = checks.every((c) => c.done);
|
|
149
|
+
const checkList = formatChecklist(checks);
|
|
150
|
+
|
|
151
|
+
workflowState.state.readyCheckCompleted = allDone;
|
|
152
|
+
if (allDone) {
|
|
153
|
+
if (workflowState.state.commitAndPushCompleted) {
|
|
154
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
155
|
+
} else {
|
|
156
|
+
workflowState.state.currentPhase = "commit";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await workflowState.save();
|
|
160
|
+
|
|
161
|
+
if (allDone) {
|
|
162
|
+
return textResponse(
|
|
163
|
+
`🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' (commits and pushes your changes)\n2. Run 'perform_release' (handles versioning, tags, and final push)\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to 'commit_and_push' to push to a non-default branch.\n\nTask: ${status.taskDescription}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function loadProjectSummary(workflowState) {
|
|
171
|
+
const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
|
|
172
|
+
try {
|
|
173
|
+
const data = await fs.readFile(summaryPath, "utf-8");
|
|
174
|
+
return JSON.parse(data);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error?.code === "ENOENT") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const prdPath = path.join(__dirname, "..", "docs", "product", "PRD.md");
|
|
184
|
+
|
|
185
|
+
export function ensurePrdExists() {
|
|
186
|
+
return existsSync(prdPath);
|
|
187
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { textResponse, formatChecklist, isReleaseSatisfied, getContinueGuidance, resetToCommitIfWorkingChanges } from "./shared.js";
|
|
2
|
+
import { handleCommitAndPush } from "./commit-handlers.js";
|
|
3
|
+
|
|
4
|
+
export function handleGetWorkflowStatus(workflowState) {
|
|
5
|
+
const status = workflowState.state;
|
|
6
|
+
const nextStep = status ? status.currentPhase : "idle";
|
|
7
|
+
|
|
8
|
+
return textResponse(
|
|
9
|
+
`📊 Current Workflow Status:
|
|
10
|
+
|
|
11
|
+
Phase: ${status.currentPhase}
|
|
12
|
+
Task: ${status.taskDescription || "(none)"}
|
|
13
|
+
Bug fixed: ${status.bugFixed ? "✅" : "❌"}
|
|
14
|
+
Tests created: ${status.testsCreated ? "✅" : "❌"}
|
|
15
|
+
Tests passed: ${status.testsPassed ? "✅" : "❌"}
|
|
16
|
+
Documentation: ${status.documentationCreated ? "✅" : "❌"}
|
|
17
|
+
Ready check: ${status.readyCheckCompleted ? "✅" : "❌"}
|
|
18
|
+
Commit & push: ${status.commitAndPushCompleted ? "✅" : "❌"}
|
|
19
|
+
Released: ${status.released ? "✅" : "❌"}
|
|
20
|
+
|
|
21
|
+
Next step: ${nextStep}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function handleReadyCheck(workflowState) {
|
|
26
|
+
const status = workflowState.state;
|
|
27
|
+
const testsCreatedDone = status.testsSkipped || status.testsCreated;
|
|
28
|
+
const testsPassedDone = status.testsSkipped || status.testsPassed;
|
|
29
|
+
const checks = [
|
|
30
|
+
{ name: "Task started", done: status.currentPhase !== "idle" },
|
|
31
|
+
{ name: "Feature/bug fixed", done: status.bugFixed },
|
|
32
|
+
{
|
|
33
|
+
name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
|
|
34
|
+
done: testsCreatedDone,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
|
|
38
|
+
done: testsPassedDone,
|
|
39
|
+
},
|
|
40
|
+
{ name: "Documentation created", done: status.documentationCreated },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const allDone = checks.every((c) => c.done);
|
|
44
|
+
const checkList = formatChecklist(checks);
|
|
45
|
+
|
|
46
|
+
workflowState.state.readyCheckCompleted = allDone;
|
|
47
|
+
if (allDone) {
|
|
48
|
+
if (workflowState.state.commitAndPushCompleted) {
|
|
49
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
50
|
+
} else {
|
|
51
|
+
workflowState.state.currentPhase = "commit";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await workflowState.save();
|
|
55
|
+
|
|
56
|
+
if (allDone) {
|
|
57
|
+
return textResponse(
|
|
58
|
+
`🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' (commits and pushes your changes)\n2. Run 'perform_release' (handles versioning, tags, and final push)\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to 'commit_and_push' to push to a non-default branch.\n\nTask: ${status.taskDescription}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function handleContinueWorkflow(workflowState, context) {
|
|
66
|
+
const status = workflowState.state;
|
|
67
|
+
|
|
68
|
+
if (!status || status.currentPhase === "idle") {
|
|
69
|
+
return textResponse("⚠️ No active workflow. Use 'start_task' to kick off a new task before continuing.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (await resetToCommitIfWorkingChanges(workflowState, context.git)) {
|
|
73
|
+
return textResponse(
|
|
74
|
+
"⚠️ 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."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (status.currentPhase === "ready") {
|
|
79
|
+
return handleReadyCheck(workflowState);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (status.currentPhase === "commit") {
|
|
83
|
+
return handleCommitAndPush({}, context);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status.currentPhase === "release") {
|
|
87
|
+
return textResponse(
|
|
88
|
+
"⚠️ Release phase requires explicit command. Run 'perform_release' with your release command (e.g., 'npm run release')."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sections = [
|
|
93
|
+
`📊 Current Phase: ${status.currentPhase}`,
|
|
94
|
+
`📝 Task: ${status.taskDescription || "(none)"}`,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
if (status.testsSkipped) {
|
|
98
|
+
sections.push(`⚠️ Tests skipped: ${status.testsSkippedReason || "(no reason provided)"}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
status.currentPhase === "testing" &&
|
|
103
|
+
status.testsCreated &&
|
|
104
|
+
!status.testsPassed &&
|
|
105
|
+
typeof status.testCommand === "string" &&
|
|
106
|
+
status.testCommand.trim() !== ""
|
|
107
|
+
) {
|
|
108
|
+
sections.push(`🧪 Last test command: ${status.testCommand}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
sections.push(`➡️ ${getContinueGuidance(status)}`);
|
|
112
|
+
|
|
113
|
+
return textResponse(sections.join("\n\n"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function handleRerunWorkflow(workflowState) {
|
|
117
|
+
const currentDescription = workflowState.state.taskDescription;
|
|
118
|
+
const currentType = workflowState.state.taskType;
|
|
119
|
+
|
|
120
|
+
if (!currentDescription) {
|
|
121
|
+
return textResponse("⚠️ No active task to rerun. Use 'start_task' to begin a new workflow.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
workflowState.reset();
|
|
125
|
+
|
|
126
|
+
workflowState.state.currentPhase = "coding";
|
|
127
|
+
workflowState.state.taskDescription = currentDescription;
|
|
128
|
+
workflowState.state.taskType = currentType;
|
|
129
|
+
await workflowState.save();
|
|
130
|
+
|
|
131
|
+
return textResponse(
|
|
132
|
+
`🔄 Rerunning workflow from the start!\n\nTask: ${currentDescription}\nType: ${currentType}\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Run 'commit_and_push' (commits and pushes)\n8. ⏳ Run 'perform_release' (handles versioning and tags)\n9. ⏳ Complete task\n\n🎯 Be conscious about what you're coding!`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { textResponse, generateProjectSummary, summarizeHistory, PROJECT_SUMMARY_FILENAME } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export function handleProjectSummary(workflowState) {
|
|
6
|
+
const summary = generateProjectSummary(workflowState.state.history);
|
|
7
|
+
return textResponse(summary);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function handleProjectSummaryData(workflowState) {
|
|
11
|
+
const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
|
|
12
|
+
try {
|
|
13
|
+
const data = await fs.readFile(summaryPath, "utf-8");
|
|
14
|
+
const parsed = JSON.parse(data);
|
|
15
|
+
const formatted = `🧠 Project Knowledge Summary (from persisted data)\n\n📊 Stats\n- Total tasks completed: ${parsed.totalTasks}\n- Recent task mix: ${Object.entries(parsed.taskTypes).map(([t, c]) => `${t}: ${c}`).join(", ")}\n- Last active: ${parsed.lastActive ? new Date(parsed.lastActive).toLocaleDateString() : "Never"}\n- Updated: ${new Date(parsed.updatedAt).toLocaleString()}\n\n📝 Recent tasks (last 5)\n${parsed.recentTasks.map((e, i) => `${i + 1}. ${e.description} (${e.type})`).join("\n")}`;
|
|
16
|
+
return textResponse(formatted);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return handleProjectSummary(workflowState);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function handleProjectSummaryDb(workflowState) {
|
|
23
|
+
try {
|
|
24
|
+
const { getSummaryForUser } = await import("../../db/index.js");
|
|
25
|
+
const userId = process.env.DEV_WORKFLOW_USER_ID || "default";
|
|
26
|
+
const projectPath = path.dirname(path.dirname(path.dirname(path.dirname(workflowState.stateFile))));
|
|
27
|
+
|
|
28
|
+
const summary = await getSummaryForUser(userId, projectPath);
|
|
29
|
+
if (!summary) {
|
|
30
|
+
return textResponse("📜 No project summary in database yet.");
|
|
31
|
+
}
|
|
32
|
+
const formatted = `🧠 Project Knowledge Summary (from SQLite)\n\n📊 Stats\n- Total tasks completed: ${summary.totalTasks}\n- Recent task mix: ${Object.entries(summary.taskTypes).map(([t, c]) => `${t}: ${c}`).join(", ")}\n- Last active: ${summary.lastActive ? new Date(summary.lastActive).toLocaleDateString() : "Never"}\n- Updated: ${new Date(summary.updatedAt).toLocaleString()}\n\n📝 Recent tasks (last 5)\n${summary.recentTasks.map((e, i) => `${i + 1}. ${e.description} (${e.type})`).join("\n")}`;
|
|
33
|
+
return textResponse(formatted);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return textResponse("📜 Database module not available. Use 'project_summary' or 'project_summary_data' instead.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function handleViewHistory(args, workflowState) {
|
|
40
|
+
const message = summarizeHistory(workflowState.state.history, args.limit || 10);
|
|
41
|
+
if (!message) {
|
|
42
|
+
return textResponse("📜 No workflow history yet.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return textResponse(message);
|
|
46
|
+
}
|