@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.
@@ -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
+ });
@@ -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
- workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
408
- ? workflowState.state.released
409
- ? "ready_to_complete"
410
- : "release"
411
- : "commit";
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.released ? "ready_to_complete" : "release";
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
- if (!(await git.hasTestChanges()) && !workflowState.state.testsSkipped) {
461
- return textResponse(
462
- "⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified."
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.released ? "ready_to_complete" : "release";
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.released) {
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
- const request = require('supertest');
2
- const express = require('express');
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
- beforeAll(() => {
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
- expect(response.body).toHaveProperty('history');
44
- expect(Array.isArray(response.body.history)).toBe(true);
45
- expect(response.body.history.length).toBeGreaterThan(0);
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
- expect(item).toHaveProperty('id');
55
- expect(item).toHaveProperty('task_type');
56
- expect(item).toHaveProperty('description');
57
- expect(item).toHaveProperty('commit_message');
58
- expect(item).toHaveProperty('completed_at');
59
- expect(item).toHaveProperty('tests_passed');
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
  });
@@ -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.11</p>
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>
@@ -73,7 +73,7 @@ export default function LoginPage() {
73
73
  Sign in to access your workflow history
74
74
  </p>
75
75
  <p className="mt-1 text-xs text-gray-400 font-mono">
76
- v1.7.18 (JWT Fix)
76
+ v1.7.15
77
77
  </p>
78
78
  </div>
79
79
 
@@ -17,7 +17,7 @@ export default function Hero() {
17
17
  .catch(() => setDownloads(null));
18
18
 
19
19
  // Version is hardcoded for static export
20
- setVersion("1.7.13");
20
+ setVersion("1.7.15");
21
21
  }, []);
22
22
 
23
23
  const formatDownloads = (num: number) => {
package/web/netlify.toml CHANGED
@@ -1,3 +1,8 @@
1
1
  [build]
2
2
  command = "npm run build"
3
3
  publish = "out"
4
+
5
+ [[redirects]]
6
+ from = "/*"
7
+ to = "/index.html"
8
+ status = 200
@@ -3,6 +3,7 @@ import type { NextConfig } from "next";
3
3
  const nextConfig: NextConfig = {
4
4
  // Static export for Netlify deployment
5
5
  output: 'export',
6
+ trailingSlash: true,
6
7
  // Set turbopack root to silence multi-lockfile warning
7
8
  turbopack: {
8
9
  root: __dirname,