@programinglive/dev-workflow-mcp-server 1.7.14 → 1.7.17
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 +46 -0
- package/README.md +72 -3
- package/db/adapters/postgres.js +85 -28
- package/db/adapters/sqlite.js +7 -1
- package/docs/product/PRD.md +4 -2
- package/docs/release-notes/RELEASE_NOTES.md +23 -0
- package/index.js +12 -2
- package/package.json +4 -2
- package/scripts/release.cjs +28 -1
- package/tests/build.test.js +1 -1
- package/tests/handlers.test.js +39 -0
- package/tests/startup-resilience.test.js +73 -0
- package/tools/definitions.js +15 -0
- package/tools/handler-modules/shared.js +187 -0
- package/tools/handlers.js +80 -19
- package/web/__tests__/api/workflow-history.test.js +14 -12
- package/web/app/docs/page.tsx +1 -1
- package/web/app/login/page.tsx +1 -1
- package/web/components/Hero.tsx +1 -1
- package/web/netlify.toml +5 -0
- package/web/next.config.ts +1 -0
- package/web/package-lock.json +5868 -2499
- package/web/package.json +4 -2
- package/workflow-state.js +13 -2
package/tests/handlers.test.js
CHANGED
|
@@ -970,6 +970,45 @@ test("perform_release accepts shorthand patch command", async () => {
|
|
|
970
970
|
});
|
|
971
971
|
});
|
|
972
972
|
|
|
973
|
+
test("skip_release records justification and advances to completion phase", async () => {
|
|
974
|
+
await withWorkflowState(async (workflowState) => {
|
|
975
|
+
workflowState.state.currentPhase = "release";
|
|
976
|
+
workflowState.state.commitAndPushCompleted = true;
|
|
977
|
+
workflowState.state.readyCheckCompleted = true;
|
|
978
|
+
workflowState.state.lastCommitMessage = "chore: docs";
|
|
979
|
+
workflowState.state.releaseSkipped = false;
|
|
980
|
+
workflowState.state.released = false;
|
|
981
|
+
workflowState.state.releaseCommand = "";
|
|
982
|
+
await workflowState.save();
|
|
983
|
+
|
|
984
|
+
const response = await handleToolCall({
|
|
985
|
+
request: createRequest("skip_release", { reason: "Python project – no npm release" }),
|
|
986
|
+
normalizeRequestArgs,
|
|
987
|
+
workflowState,
|
|
988
|
+
exec: async () => ({ stdout: "" }),
|
|
989
|
+
git: {
|
|
990
|
+
hasWorkingChanges: async () => false,
|
|
991
|
+
hasStagedChanges: async () => false,
|
|
992
|
+
hasTestChanges: async () => true,
|
|
993
|
+
getStagedChanges: async () => [],
|
|
994
|
+
getCurrentBranch: async () => "main",
|
|
995
|
+
getPrimaryBranch: async () => "main",
|
|
996
|
+
getLastCommitMessage: async () => "chore: docs",
|
|
997
|
+
workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
|
|
998
|
+
},
|
|
999
|
+
utils,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const text = response.content[0].text;
|
|
1003
|
+
assert.ok(text.includes("Release step skipped"), "response should mention release was skipped");
|
|
1004
|
+
assert.equal(workflowState.state.releaseSkipped, true);
|
|
1005
|
+
assert.equal(workflowState.state.releaseSkippedReason, "Python project – no npm release");
|
|
1006
|
+
assert.equal(workflowState.state.releaseCommand, "(skipped)");
|
|
1007
|
+
assert.equal(workflowState.state.currentPhase, "ready_to_complete");
|
|
1008
|
+
assert.equal(workflowState.state.released, false);
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
973
1012
|
test("perform_release respects explicit releaseType option", async () => {
|
|
974
1013
|
await withWorkflowState(async (workflowState) => {
|
|
975
1014
|
workflowState.state.currentPhase = "release";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const ROOT = path.join(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
test('Server startup performance and resilience', async (t) => {
|
|
12
|
+
await t.test('Server starts in under 2 seconds (Studio timeout safety)', async () => {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
const server = spawn('node', [path.join(ROOT, 'index.js')], {
|
|
15
|
+
env: { ...process.env, DEV_WORKFLOW_DB_TYPE: 'sqlite' },
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let output = '';
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
server.kill();
|
|
23
|
+
reject(new Error('Server timed out starting'));
|
|
24
|
+
}, 5000);
|
|
25
|
+
|
|
26
|
+
server.stderr.on('data', (data) => {
|
|
27
|
+
output += data.toString();
|
|
28
|
+
if (output.includes('running on stdio')) {
|
|
29
|
+
const duration = Date.now() - start;
|
|
30
|
+
console.log(`Verified startup duration: ${duration}ms`);
|
|
31
|
+
assert.ok(duration < 2000, `Startup should be fast (took ${duration}ms)`);
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
server.kill();
|
|
34
|
+
resolve();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.on('error', (err) => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
reject(err);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await t.test('Resilience: Server starts even with unreachable database', async () => {
|
|
46
|
+
const server = spawn('node', [path.join(ROOT, 'index.js')], {
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
DEV_WORKFLOW_DB_TYPE: 'postgres',
|
|
50
|
+
DEV_WORKFLOW_DB_URL: 'postgres://user:pass@1.2.3.4:5432/invalid'
|
|
51
|
+
},
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
let output = '';
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
server.kill();
|
|
59
|
+
reject(new Error('Server hung on unreachable DB'));
|
|
60
|
+
}, 15000); // Allow time for the 5s DB timeout + startup
|
|
61
|
+
|
|
62
|
+
server.stderr.on('data', (data) => {
|
|
63
|
+
output += data.toString();
|
|
64
|
+
if (output.includes('running on stdio')) {
|
|
65
|
+
assert.ok(true, 'Server started despite invalid DB');
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
server.kill();
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
package/tools/definitions.js
CHANGED
|
@@ -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,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
|
+
}
|
package/tools/handlers.js
CHANGED
|
@@ -16,6 +16,10 @@ const textResponse = (text) => ({
|
|
|
16
16
|
],
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
function isReleaseSatisfied(state) {
|
|
20
|
+
return Boolean(state?.released || state?.releaseSkipped);
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
20
24
|
if (!workflowState.state.commitAndPushCompleted) {
|
|
21
25
|
return false;
|
|
@@ -30,6 +34,8 @@ async function resetToCommitIfWorkingChanges(workflowState, git) {
|
|
|
30
34
|
|
|
31
35
|
workflowState.state.commitAndPushCompleted = false;
|
|
32
36
|
workflowState.state.released = false;
|
|
37
|
+
workflowState.state.releaseSkipped = false;
|
|
38
|
+
workflowState.state.releaseSkippedReason = "";
|
|
33
39
|
workflowState.state.currentPhase = "commit";
|
|
34
40
|
workflowState.state.releaseCommand = "";
|
|
35
41
|
workflowState.state.releaseNotes = "";
|
|
@@ -113,7 +119,7 @@ function getContinueGuidance(status) {
|
|
|
113
119
|
case "commit":
|
|
114
120
|
return "All checks passed—run 'commit_and_push' with your commit message (and optional branch).";
|
|
115
121
|
case "release":
|
|
116
|
-
return "Commit recorded. Execute 'perform_release' with the release command you ran to publish changes.";
|
|
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.";
|
|
117
123
|
case "ready_to_complete":
|
|
118
124
|
return "Release recorded. Finish up by calling 'complete_task' with the commit message you used.";
|
|
119
125
|
default:
|
|
@@ -185,7 +191,7 @@ async function handleStartTask(args, workflowState) {
|
|
|
185
191
|
await workflowState.save();
|
|
186
192
|
|
|
187
193
|
return textResponse(
|
|
188
|
-
`✅ Task Started: ${args.description}\n\n🎯 Be conscious about what you're coding!\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\nReminder: Focus on writing clean, maintainable code!`
|
|
194
|
+
`✅ Task Started: ${args.description}\n\n🎯 Be conscious about what you're coding!\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) or 'skip_release' (when no release is needed)\n9. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
|
|
189
195
|
);
|
|
190
196
|
}
|
|
191
197
|
|
|
@@ -225,6 +231,8 @@ async function handleMarkBugFixed(args, workflowState) {
|
|
|
225
231
|
workflowState.state.fixSummary = args.summary;
|
|
226
232
|
workflowState.state.readyCheckCompleted = false;
|
|
227
233
|
workflowState.state.released = false;
|
|
234
|
+
workflowState.state.releaseSkipped = false;
|
|
235
|
+
workflowState.state.releaseSkippedReason = "";
|
|
228
236
|
workflowState.state.releaseCommand = "";
|
|
229
237
|
workflowState.state.releaseNotes = "";
|
|
230
238
|
workflowState.state.commitAndPushCompleted = false;
|
|
@@ -233,7 +241,7 @@ async function handleMarkBugFixed(args, workflowState) {
|
|
|
233
241
|
await workflowState.save();
|
|
234
242
|
|
|
235
243
|
return textResponse(
|
|
236
|
-
`✅ Feature/Bug marked as fixed!\n\n⚠️ CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ⏳ Create tests for: ${args.summary}\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`
|
|
244
|
+
`✅ Feature/Bug marked as fixed!\n\n⚠️ CRITICAL REMINDER: You MUST create tests now!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ⏳ Create tests for: ${args.summary}\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release' (or 'skip_release' if no release applies)\n8. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`
|
|
237
245
|
);
|
|
238
246
|
}
|
|
239
247
|
|
|
@@ -247,13 +255,15 @@ async function handleCreateTests(workflowState) {
|
|
|
247
255
|
workflowState.state.testsSkippedReason = "";
|
|
248
256
|
workflowState.state.readyCheckCompleted = false;
|
|
249
257
|
workflowState.state.released = false;
|
|
258
|
+
workflowState.state.releaseSkipped = false;
|
|
259
|
+
workflowState.state.releaseSkippedReason = "";
|
|
250
260
|
workflowState.state.commitAndPushCompleted = false;
|
|
251
261
|
workflowState.state.lastCommitMessage = "";
|
|
252
262
|
workflowState.state.lastPushBranch = "";
|
|
253
263
|
await workflowState.save();
|
|
254
264
|
|
|
255
265
|
return textResponse(
|
|
256
|
-
"✅ Tests recorded!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'."
|
|
266
|
+
"✅ Tests recorded!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ⏳ Run tests (must be green!)\n4. ⏳ Create documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release' (or 'skip_release' if appropriate)\n8. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'."
|
|
257
267
|
);
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -280,6 +290,8 @@ async function handleSkipTests(args, workflowState) {
|
|
|
280
290
|
workflowState.state.readyToCommit = false;
|
|
281
291
|
workflowState.state.readyCheckCompleted = false;
|
|
282
292
|
workflowState.state.released = false;
|
|
293
|
+
workflowState.state.releaseSkipped = false;
|
|
294
|
+
workflowState.state.releaseSkippedReason = "";
|
|
283
295
|
workflowState.state.commitAndPushCompleted = false;
|
|
284
296
|
workflowState.state.lastCommitMessage = "";
|
|
285
297
|
workflowState.state.lastPushBranch = "";
|
|
@@ -297,7 +309,7 @@ Next Steps:
|
|
|
297
309
|
3. ⏳ Create/update documentation
|
|
298
310
|
4. ⏳ Run 'check_ready_to_commit'
|
|
299
311
|
5. ⏳ Run 'commit_and_push'
|
|
300
|
-
6. ⏳ Run 'perform_release'
|
|
312
|
+
6. ⏳ Run 'perform_release' or 'skip_release'
|
|
301
313
|
7. ⏳ Complete task`
|
|
302
314
|
);
|
|
303
315
|
}
|
|
@@ -328,6 +340,8 @@ async function handleRunTests(args, workflowState) {
|
|
|
328
340
|
workflowState.state.testsSkippedReason = "";
|
|
329
341
|
workflowState.state.readyCheckCompleted = false;
|
|
330
342
|
workflowState.state.released = false;
|
|
343
|
+
workflowState.state.releaseSkipped = false;
|
|
344
|
+
workflowState.state.releaseSkippedReason = "";
|
|
331
345
|
workflowState.state.commitAndPushCompleted = false;
|
|
332
346
|
workflowState.state.lastCommitMessage = "";
|
|
333
347
|
workflowState.state.lastPushBranch = "";
|
|
@@ -345,7 +359,7 @@ async function handleRunTests(args, workflowState) {
|
|
|
345
359
|
await workflowState.save();
|
|
346
360
|
|
|
347
361
|
return textResponse(
|
|
348
|
-
`✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\n\n📝 Now create or update documentation using 'create_documentation' with:\n- documentationType: "PRD", "README", "RELEASE_NOTES", "inline-comments", "API-docs", "changelog", or "other"\n- summary: Brief description of what was documented\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ⏳ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Complete task`
|
|
362
|
+
`✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\n\n📝 Now create or update documentation using 'create_documentation' with:\n- documentationType: "PRD", "README", "RELEASE_NOTES", "inline-comments", "API-docs", "changelog", or "other"\n- summary: Brief description of what was documented\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ⏳ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit'\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release' (or use 'skip_release' if no release is needed)\n8. ⏳ Complete task`
|
|
349
363
|
);
|
|
350
364
|
}
|
|
351
365
|
|
|
@@ -371,13 +385,58 @@ async function handleCreateDocumentation(args, workflowState) {
|
|
|
371
385
|
workflowState.state.readyToCommit = true;
|
|
372
386
|
workflowState.state.readyCheckCompleted = false;
|
|
373
387
|
workflowState.state.released = false;
|
|
388
|
+
workflowState.state.releaseSkipped = false;
|
|
389
|
+
workflowState.state.releaseSkippedReason = "";
|
|
374
390
|
workflowState.state.commitAndPushCompleted = false;
|
|
375
391
|
workflowState.state.lastCommitMessage = "";
|
|
376
392
|
workflowState.state.lastPushBranch = "";
|
|
377
393
|
await workflowState.save();
|
|
378
394
|
|
|
379
395
|
return textResponse(
|
|
380
|
-
`✅ Documentation created/updated!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n✅ PRD verified: docs/product/PRD.md exists\n\n🎉 You're ready to verify your work!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ✓ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release'\n8. ⏳ Mark as complete with 'complete_task'`
|
|
396
|
+
`✅ Documentation created/updated!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n✅ PRD verified: docs/product/PRD.md exists\n\n🎉 You're ready to verify your work!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ✓ Create/update documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Run 'commit_and_push'\n7. ⏳ Run 'perform_release' (or 'skip_release' when applicable)\n8. ⏳ Mark as complete with 'complete_task'`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function handleSkipRelease(args, workflowState) {
|
|
401
|
+
if (workflowState.state.currentPhase === "idle") {
|
|
402
|
+
return textResponse("⚠️ Please start a task first using 'start_task' before skipping release.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
406
|
+
if (!reason) {
|
|
407
|
+
return textResponse("⚠️ Provide a non-empty 'reason' explaining why the release step is being skipped.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (workflowState.state.releaseSkipped) {
|
|
411
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
412
|
+
await workflowState.save();
|
|
413
|
+
return textResponse(
|
|
414
|
+
`ℹ️ Release step was already skipped for this task.\nUpdated reason: ${reason}\n\nNext: run 'complete_task' to wrap up.`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (workflowState.state.released) {
|
|
419
|
+
return textResponse("⚠️ A release command has already been recorded. Continue with 'complete_task'.");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!workflowState.state.readyCheckCompleted) {
|
|
423
|
+
return textResponse("⚠️ Please run 'check_ready_to_commit' and ensure all prerequisites pass before skipping release.");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!workflowState.state.commitAndPushCompleted) {
|
|
427
|
+
return textResponse("⚠️ Please complete 'commit_and_push' before skipping release.");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
workflowState.state.releaseSkipped = true;
|
|
431
|
+
workflowState.state.releaseSkippedReason = reason;
|
|
432
|
+
workflowState.state.released = false;
|
|
433
|
+
workflowState.state.releaseCommand = "(skipped)";
|
|
434
|
+
workflowState.state.releaseNotes = "";
|
|
435
|
+
workflowState.state.currentPhase = "ready_to_complete";
|
|
436
|
+
await workflowState.save();
|
|
437
|
+
|
|
438
|
+
return textResponse(
|
|
439
|
+
`⚠️ Release step skipped.\nReason: ${reason}\n\n✅ Next: run 'complete_task' with your final commit message to close out the workflow.`
|
|
381
440
|
);
|
|
382
441
|
}
|
|
383
442
|
|
|
@@ -404,11 +463,11 @@ async function handleReadyCheck(workflowState) {
|
|
|
404
463
|
|
|
405
464
|
workflowState.state.readyCheckCompleted = allDone;
|
|
406
465
|
if (allDone) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
466
|
+
if (workflowState.state.commitAndPushCompleted) {
|
|
467
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
468
|
+
} else {
|
|
469
|
+
workflowState.state.currentPhase = "commit";
|
|
470
|
+
}
|
|
412
471
|
}
|
|
413
472
|
await workflowState.save();
|
|
414
473
|
|
|
@@ -448,7 +507,7 @@ async function handleCommitAndPush(args, context) {
|
|
|
448
507
|
workflowState.state.commitAndPushCompleted = true;
|
|
449
508
|
workflowState.state.lastCommitMessage = effectiveCommitMessage || "";
|
|
450
509
|
workflowState.state.lastPushBranch = branchForPush;
|
|
451
|
-
workflowState.state.currentPhase = workflowState.state
|
|
510
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
452
511
|
await workflowState.save();
|
|
453
512
|
|
|
454
513
|
return textResponse(
|
|
@@ -457,10 +516,10 @@ async function handleCommitAndPush(args, context) {
|
|
|
457
516
|
);
|
|
458
517
|
}
|
|
459
518
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
);
|
|
519
|
+
const statusOutput = await git.getStatusOutput();
|
|
520
|
+
|
|
521
|
+
if (!workflowState.state.testsSkipped && !git.containsTestFilesInStatus(statusOutput)) {
|
|
522
|
+
return textResponse("⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.");
|
|
464
523
|
}
|
|
465
524
|
|
|
466
525
|
try {
|
|
@@ -514,7 +573,7 @@ async function handleCommitAndPush(args, context) {
|
|
|
514
573
|
workflowState.state.commitAndPushCompleted = true;
|
|
515
574
|
workflowState.state.lastCommitMessage = generatedSummary;
|
|
516
575
|
workflowState.state.lastPushBranch = branchForPush;
|
|
517
|
-
workflowState.state.currentPhase = workflowState.state
|
|
576
|
+
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
518
577
|
await workflowState.save();
|
|
519
578
|
|
|
520
579
|
return textResponse(
|
|
@@ -936,7 +995,7 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
|
|
|
936
995
|
continue;
|
|
937
996
|
}
|
|
938
997
|
|
|
939
|
-
if (!state
|
|
998
|
+
if (!isReleaseSatisfied(state)) {
|
|
940
999
|
const resp = await executeStep("perform_release", handlePerformRelease, [
|
|
941
1000
|
releaseArgs,
|
|
942
1001
|
{ workflowState, exec, git, utils },
|
|
@@ -996,6 +1055,8 @@ export async function handleToolCall({
|
|
|
996
1055
|
return handleCommitAndPush(args, { workflowState, exec, git, utils });
|
|
997
1056
|
case "perform_release":
|
|
998
1057
|
return handlePerformRelease(args, { workflowState, exec, git, utils });
|
|
1058
|
+
case "skip_release":
|
|
1059
|
+
return handleSkipRelease(args, workflowState);
|
|
999
1060
|
case "complete_task":
|
|
1000
1061
|
return handleCompleteTask(args, workflowState);
|
|
1001
1062
|
case "force_complete_task":
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import express from 'express';
|
|
3
5
|
|
|
4
6
|
describe('Workflow History API', () => {
|
|
5
7
|
let app;
|
|
6
8
|
let server;
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
test('setup app', () => {
|
|
9
11
|
// Mock Express app for testing
|
|
10
12
|
app = express();
|
|
11
13
|
app.use(express.json());
|
|
@@ -40,9 +42,9 @@ describe('Workflow History API', () => {
|
|
|
40
42
|
.get('/api/workflow/history')
|
|
41
43
|
.expect(200);
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
assert(response.body.history, 'History property should exist');
|
|
46
|
+
assert(Array.isArray(response.body.history), 'History should be an array');
|
|
47
|
+
assert(response.body.history.length > 0, 'History should not be empty');
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('History items have required fields', async () => {
|
|
@@ -51,11 +53,11 @@ describe('Workflow History API', () => {
|
|
|
51
53
|
.expect(200);
|
|
52
54
|
|
|
53
55
|
const item = response.body.history[0];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
assert(item.id, 'Item should have id');
|
|
57
|
+
assert(item.task_type, 'Item should have task_type');
|
|
58
|
+
assert(item.description, 'Item should have description');
|
|
59
|
+
assert(item.commit_message, 'Item should have commit_message');
|
|
60
|
+
assert(item.completed_at, 'Item should have completed_at');
|
|
61
|
+
assert(item.tests_passed !== undefined, 'Item should have tests_passed');
|
|
60
62
|
});
|
|
61
63
|
});
|
package/web/app/docs/page.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export default function DocsPage() {
|
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
24
|
<div className="bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-8">
|
|
25
|
-
<p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.
|
|
25
|
+
<p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.15</p>
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<h2 className="text-3xl font-bold mt-12 mb-6 text-gray-900 dark:text-white">Features</h2>
|
package/web/app/login/page.tsx
CHANGED
package/web/components/Hero.tsx
CHANGED
package/web/netlify.toml
CHANGED
package/web/next.config.ts
CHANGED