@programinglive/dev-workflow-mcp-server 1.0.2 → 1.0.7

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 CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.0.7](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.6...v1.0.7) (2025-10-20)
6
+
7
+ ### [1.0.6](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.5...v1.0.6) (2025-10-20)
8
+
9
+
10
+ ### ✅ Tests
11
+
12
+ * enforce clean release workflow ([c261a7b](https://github.com/programinglive/dev-workflow-mcp-server/commit/c261a7b3d141ba4472567bd47e4f6b6e4d2ee3fd))
13
+
14
+ ### [1.0.5](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.4...v1.0.5) (2025-10-20)
15
+
16
+
17
+ ### ✅ Tests
18
+
19
+ * update index.js and tests/workflowState.test.js ([b9df3c8](https://github.com/programinglive/dev-workflow-mcp-server/commit/b9df3c8643303769dbe4c5a0992995b6328cdfcf))
20
+
21
+ ### [1.0.4](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.3...v1.0.4) (2025-10-20)
22
+
23
+
24
+ ### ✨ Features
25
+
26
+ * enforce release step after ready check ([0725496](https://github.com/programinglive/dev-workflow-mcp-server/commit/07254967becd515cb2a4261d62af01dfeb842175))
27
+
28
+
29
+ ### 🐛 Bug Fixes
30
+
31
+ * support windows-friendly commit quoting ([47881fd](https://github.com/programinglive/dev-workflow-mcp-server/commit/47881fd0db54da3fabe368b1a135902e47ef873e))
32
+
33
+ ### [1.0.3](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.2...v1.0.3) (2025-10-20)
34
+
35
+
36
+ ### 🧹 Chores
37
+
38
+ * add gitignore ([5f695fb](https://github.com/programinglive/dev-workflow-mcp-server/commit/5f695fbc71515c7f1ffbc44fd74b52314c966ee6))
39
+
40
+
41
+ ### 📝 Documentation
42
+
43
+ * clarify cross-platform installation ([52b5ee7](https://github.com/programinglive/dev-workflow-mcp-server/commit/52b5ee7061e8f968ff94e6c226586fc1b7e2a057))
44
+
5
45
  ### [1.0.2](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.0.1...v1.0.2) (2025-10-19)
6
46
 
7
47
 
package/README.md CHANGED
@@ -11,7 +11,8 @@ This MCP server guides you through a disciplined development workflow:
11
11
  3. **Create Tests** - Always test your changes
12
12
  4. **Run Tests** - Tests must pass (GREEN)
13
13
  5. **Document** - Update documentation
14
- 6. **Commit & Push** - Only when all steps are complete
14
+ 6. **Commit & Push** - Let the server drive the git commands once verification passes
15
+ 7. **Release** - Record the release details before closing out the task
15
16
 
16
17
  ## 🚀 Installation
17
18
 
@@ -23,34 +24,94 @@ npm install
23
24
 
24
25
  ### 2. Configure in Windsurf/Claude Desktop
25
26
 
26
- Add to your MCP settings file:
27
+ Point your MCP client to the server entry point. Replace `<PROJECT_ROOT>` with the absolute path to this repository on your machine.
27
28
 
28
- **For Windsurf** (`~/Library/Application Support/Windsurf/config.json`):
29
+ #### macOS
30
+
31
+ - **Windsurf** (`~/Library/Application Support/Windsurf/config.json`):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "dev-workflow": {
37
+ "command": "node",
38
+ "args": ["<PROJECT_ROOT>/index.js"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ - **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "dev-workflow": {
50
+ "command": "node",
51
+ "args": ["<PROJECT_ROOT>/index.js"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ #### Windows
58
+
59
+ - **Windsurf** (`%APPDATA%\Windsurf\config.json`):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "dev-workflow": {
65
+ "command": "node",
66
+ "args": ["<PROJECT_ROOT>\\index.js"]
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ - **Claude Desktop** (`%APPDATA%\Claude\claude_desktop_config.json`):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "dev-workflow": {
78
+ "command": "node",
79
+ "args": ["<PROJECT_ROOT>\\index.js"]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ #### Linux
86
+
87
+ - **Windsurf** (`~/.config/windsurf/config.json`):
29
88
 
30
89
  ```json
31
90
  {
32
91
  "mcpServers": {
33
92
  "dev-workflow": {
34
93
  "command": "node",
35
- "args": ["/Users/mahardhika/code/project/mine/app/CascadeProjects/windsurf-project/index.js"]
94
+ "args": ["<PROJECT_ROOT>/index.js"]
36
95
  }
37
96
  }
38
97
  }
39
98
  ```
40
99
 
41
- **For Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
100
+ - **Claude Desktop** (`~/.config/claude/claude_desktop_config.json`):
42
101
 
43
102
  ```json
44
103
  {
45
104
  "mcpServers": {
46
105
  "dev-workflow": {
47
106
  "command": "node",
48
- "args": ["/Users/mahardhika/code/project/mine/app/CascadeProjects/windsurf-project/index.js"]
107
+ "args": ["<PROJECT_ROOT>/index.js"]
49
108
  }
50
109
  }
51
110
  }
52
111
  ```
53
112
 
113
+ > **Note:** On Windows paths in JSON require escaped backslashes (e.g., `"C:\\path\\to\\project"`).
114
+
54
115
  ### 3. Restart Windsurf/Claude Desktop
55
116
 
56
117
  After adding the configuration, restart the application to load the MCP server.
@@ -92,6 +153,11 @@ Mark that the bug/feature is fixed. **Reminder: Now you MUST create tests!**
92
153
  **Parameters:**
93
154
  - `summary` (string, required): Brief summary of what was fixed/implemented
94
155
 
156
+ ### `create_tests`
157
+ Confirm that you've created the necessary tests covering your change. Required before recording test results.
158
+
159
+ **Parameters:** _none_
160
+
95
161
  ### `run_tests`
96
162
  Record test results. **NEVER commit if tests fail!** Only proceed if all tests are green.
97
163
 
@@ -118,6 +184,20 @@ Mark that documentation has been created/updated. This is required before commit
118
184
  ### `check_ready_to_commit`
119
185
  Check if all workflow steps are completed and you're ready to commit & push.
120
186
 
187
+ ### `commit_and_push`
188
+ Automatically run `git add`, `git commit`, and `git push` after the ready check passes.
189
+
190
+ **Parameters:**
191
+ - `commitMessage` (string, required): Conventional commit message to use
192
+ - `branch` (string, optional): Target branch to push (defaults to current branch)
193
+
194
+ ### `perform_release`
195
+ Record the release after you've committed and pushed. Required before you can complete the task.
196
+
197
+ **Parameters:**
198
+ - `command` (string, required): Release command that was executed (e.g., `npm run release`)
199
+ - `notes` (string, optional): Additional release notes
200
+
121
201
  ### `complete_task`
122
202
  Mark the task as complete after successful commit & push. Resets workflow for next task.
123
203
 
@@ -180,13 +260,16 @@ Here's how you'd use this MCP server in a typical coding session:
180
260
  ```
181
261
 
182
262
  8. **Commit & Push:**
183
- ```bash
184
- git add .
185
- git commit -m "feat: add user profile page with tests and docs"
186
- git push
263
+ ```
264
+ "Commit and push: commitMessage='feat: add user profile page with tests and docs'"
265
+ ```
266
+
267
+ 9. **Record release:**
268
+ ```
269
+ "Record release: command='npm run release', notes='v1.2.3'"
187
270
  ```
188
271
 
189
- 9. **Complete:**
272
+ 10. **Complete:**
190
273
  ```
191
274
  "Complete the task with commit message: 'feat: add user profile page'"
192
275
  ```
package/index.js CHANGED
@@ -25,9 +25,17 @@ export class WorkflowState {
25
25
  currentPhase: "idle",
26
26
  taskDescription: "",
27
27
  bugFixed: false,
28
+ testsCreated: false,
28
29
  testsPassed: false,
29
30
  documentationCreated: false,
30
31
  readyToCommit: false,
32
+ readyCheckCompleted: false,
33
+ released: false,
34
+ releaseCommand: "",
35
+ releaseNotes: "",
36
+ commitAndPushCompleted: false,
37
+ lastCommitMessage: "",
38
+ lastPushBranch: "",
31
39
  history: [],
32
40
  };
33
41
  }
@@ -35,7 +43,35 @@ export class WorkflowState {
35
43
  async load() {
36
44
  try {
37
45
  const data = await fs.readFile(this.stateFile, "utf-8");
38
- this.state = JSON.parse(data);
46
+ const parsed = JSON.parse(data);
47
+ this.state = {
48
+ ...this.state,
49
+ ...parsed,
50
+ };
51
+ if (typeof this.state.testsCreated !== "boolean") {
52
+ this.state.testsCreated = false;
53
+ }
54
+ if (typeof this.state.readyCheckCompleted !== "boolean") {
55
+ this.state.readyCheckCompleted = false;
56
+ }
57
+ if (typeof this.state.released !== "boolean") {
58
+ this.state.released = false;
59
+ }
60
+ if (typeof this.state.commitAndPushCompleted !== "boolean") {
61
+ this.state.commitAndPushCompleted = false;
62
+ }
63
+ if (typeof this.state.releaseCommand !== "string") {
64
+ this.state.releaseCommand = "";
65
+ }
66
+ if (typeof this.state.releaseNotes !== "string") {
67
+ this.state.releaseNotes = "";
68
+ }
69
+ if (typeof this.state.lastCommitMessage !== "string") {
70
+ this.state.lastCommitMessage = "";
71
+ }
72
+ if (typeof this.state.lastPushBranch !== "string") {
73
+ this.state.lastPushBranch = "";
74
+ }
39
75
  } catch (error) {
40
76
  // File doesn't exist yet, use default state
41
77
  }
@@ -50,9 +86,17 @@ export class WorkflowState {
50
86
  currentPhase: "idle",
51
87
  taskDescription: "",
52
88
  bugFixed: false,
89
+ testsCreated: false,
53
90
  testsPassed: false,
54
91
  documentationCreated: false,
55
92
  readyToCommit: false,
93
+ readyCheckCompleted: false,
94
+ released: false,
95
+ releaseCommand: "",
96
+ releaseNotes: "",
97
+ commitAndPushCompleted: false,
98
+ lastCommitMessage: "",
99
+ lastPushBranch: "",
56
100
  history: this.state.history,
57
101
  };
58
102
  }
@@ -70,6 +114,18 @@ await workflowState.load();
70
114
 
71
115
  const exec = promisify(execCallback);
72
116
 
117
+ const isWindows = process.platform === "win32";
118
+
119
+ function shellEscape(value) {
120
+ const stringValue = String(value);
121
+ if (isWindows) {
122
+ // Use PowerShell-friendly single quotes, escape embedded single quotes by doubling
123
+ return `'${stringValue.replace(/'/g, "''")}'`;
124
+ }
125
+
126
+ return `'${stringValue.replace(/'/g, "'\\''")}'`;
127
+ }
128
+
73
129
  function normalizeRequestArgs(rawArgs) {
74
130
  if (rawArgs === undefined || rawArgs === null) {
75
131
  return { args: {}, error: null };
@@ -121,6 +177,297 @@ async function hasStagedChanges() {
121
177
  }
122
178
  }
123
179
 
180
+ async function hasWorkingChanges() {
181
+ try {
182
+ const { stdout } = await exec("git status --porcelain");
183
+ return stdout.split("\n").some((line) => line && line.trim().length > 0);
184
+ } catch (error) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ export function workingTreeSummary(statusOutput) {
190
+ if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
191
+ return {
192
+ hasChanges: false,
193
+ lines: [],
194
+ };
195
+ }
196
+
197
+ const lines = statusOutput
198
+ .split("\n")
199
+ .map((line) => line.replace(/\s+/g, " ").trim())
200
+ .filter(Boolean);
201
+
202
+ return {
203
+ hasChanges: lines.length > 0,
204
+ lines,
205
+ };
206
+ }
207
+
208
+ function isTestFilePath(filePath) {
209
+ if (!filePath) {
210
+ return false;
211
+ }
212
+
213
+ if (/\.(test|spec)\.[^.]+$/i.test(filePath)) {
214
+ return true;
215
+ }
216
+
217
+ return /(\/|^)(__tests__|tests?|spec)\//i.test(filePath);
218
+ }
219
+
220
+ function extractPathFromStatusLine(line) {
221
+ if (!line || line.length < 4) {
222
+ return "";
223
+ }
224
+
225
+ const rawPath = line.slice(3).trim();
226
+ if (!rawPath) {
227
+ return "";
228
+ }
229
+
230
+ if (rawPath.includes(" -> ")) {
231
+ const parts = rawPath.split(" -> ");
232
+ return parts[parts.length - 1].trim();
233
+ }
234
+
235
+ return rawPath;
236
+ }
237
+
238
+ export function containsTestFilesInStatus(statusOutput) {
239
+ if (typeof statusOutput !== "string" || statusOutput.trim() === "") {
240
+ return false;
241
+ }
242
+
243
+ return statusOutput
244
+ .split("\n")
245
+ .map((line) => extractPathFromStatusLine(line))
246
+ .some((path) => isTestFilePath(path));
247
+ }
248
+
249
+ function isDocumentationFilePath(filePath) {
250
+ if (!filePath) {
251
+ return false;
252
+ }
253
+
254
+ const normalized = filePath.toLowerCase();
255
+ return (
256
+ normalized.startsWith("docs/") ||
257
+ normalized.startsWith("documentation/") ||
258
+ normalized.endsWith(".md") ||
259
+ normalized.endsWith(".rst") ||
260
+ normalized.endsWith(".adoc") ||
261
+ normalized.includes("readme")
262
+ );
263
+ }
264
+
265
+ function parseNameStatusLine(line) {
266
+ if (!line) {
267
+ return null;
268
+ }
269
+
270
+ const parts = line.split("\t").map((part) => part.trim());
271
+ const status = parts[0];
272
+ if (!status) {
273
+ return null;
274
+ }
275
+
276
+ if (status.startsWith("R") || status.startsWith("C")) {
277
+ const originalPath = parts[1] || "";
278
+ const newPath = parts[2] || originalPath;
279
+ return {
280
+ status,
281
+ path: newPath,
282
+ originalPath,
283
+ };
284
+ }
285
+
286
+ return {
287
+ status,
288
+ path: parts[1] || "",
289
+ originalPath: "",
290
+ };
291
+ }
292
+
293
+ function formatFileList(paths) {
294
+ const uniquePaths = [...new Set(paths.filter(Boolean))];
295
+
296
+ if (uniquePaths.length === 0) {
297
+ return "updates";
298
+ }
299
+
300
+ if (uniquePaths.length === 1) {
301
+ return uniquePaths[0];
302
+ }
303
+
304
+ if (uniquePaths.length === 2) {
305
+ return `${uniquePaths[0]} and ${uniquePaths[1]}`;
306
+ }
307
+
308
+ if (uniquePaths.length === 3) {
309
+ return `${uniquePaths[0]}, ${uniquePaths[1]}, and ${uniquePaths[2]}`;
310
+ }
311
+
312
+ const remaining = uniquePaths.length - 3;
313
+ return `${uniquePaths[0]}, ${uniquePaths[1]}, ${uniquePaths[2]} +${remaining} more`;
314
+ }
315
+
316
+ function determineCommitType(changes) {
317
+ const hasTestChange = changes.some((change) =>
318
+ [change.path, change.originalPath].some((p) => isTestFilePath(p))
319
+ );
320
+ const hasDocsChange = changes.some((change) =>
321
+ [change.path, change.originalPath].some((p) => isDocumentationFilePath(p))
322
+ );
323
+
324
+ if (hasTestChange) {
325
+ return "test";
326
+ }
327
+
328
+ if (hasDocsChange) {
329
+ return "docs";
330
+ }
331
+
332
+ return "chore";
333
+ }
334
+
335
+ function describeChange(change) {
336
+ const statusCode = change.status || "";
337
+ const path = change.path || change.originalPath || "";
338
+
339
+ if (statusCode.startsWith("R")) {
340
+ return `renamed ${change.originalPath} -> ${change.path}`;
341
+ }
342
+
343
+ if (statusCode.startsWith("C")) {
344
+ return `copied ${change.originalPath} -> ${change.path}`;
345
+ }
346
+
347
+ switch (statusCode[0]) {
348
+ case "A":
349
+ return `added ${path}`;
350
+ case "M":
351
+ return `modified ${path}`;
352
+ case "D":
353
+ return `removed ${path}`;
354
+ case "U":
355
+ return `updated ${path}`;
356
+ default:
357
+ return `${statusCode} ${path}`.trim();
358
+ }
359
+ }
360
+
361
+ export function createCommitMessageParts(changes, providedSummary = "") {
362
+ const summaryInput = typeof providedSummary === "string" ? providedSummary.trim() : "";
363
+
364
+ if (!Array.isArray(changes) || changes.length === 0) {
365
+ return {
366
+ summary: summaryInput,
367
+ body: "",
368
+ };
369
+ }
370
+
371
+ const commitType = determineCommitType(changes);
372
+ const summaryPaths = changes
373
+ .map((change) => change.path || change.originalPath)
374
+ .filter(Boolean);
375
+ const generatedSummary = `${commitType}: update ${formatFileList(summaryPaths)}`;
376
+ const summary = summaryInput || generatedSummary;
377
+
378
+ const detailLines = changes
379
+ .map((change) => describeChange(change))
380
+ .filter((line) => line && line.trim().length > 0)
381
+ .map((line) => `- ${line}`);
382
+
383
+ return {
384
+ summary,
385
+ body: detailLines.join("\n"),
386
+ };
387
+ }
388
+
389
+ async function getStagedChanges() {
390
+ try {
391
+ const { stdout } = await exec("git diff --cached --name-status");
392
+ return stdout
393
+ .split("\n")
394
+ .map((line) => parseNameStatusLine(line))
395
+ .filter(Boolean);
396
+ } catch (error) {
397
+ return [];
398
+ }
399
+ }
400
+
401
+ async function hasTestChanges() {
402
+ try {
403
+ const { stdout } = await exec("git status --porcelain");
404
+ return containsTestFilesInStatus(stdout);
405
+ } catch (error) {
406
+ return false;
407
+ }
408
+ }
409
+
410
+ function normalizeCommitHeader(message) {
411
+ if (typeof message !== "string") {
412
+ return "";
413
+ }
414
+
415
+ const header = message.split("\n")[0] || "";
416
+ return header.trim();
417
+ }
418
+
419
+ function containsBreakingChange(message) {
420
+ if (typeof message !== "string") {
421
+ return false;
422
+ }
423
+
424
+ return /breaking change/i.test(message);
425
+ }
426
+
427
+ export function determineReleaseTypeFromCommit(message) {
428
+ const header = normalizeCommitHeader(message).toLowerCase();
429
+ const hasBreaking = containsBreakingChange(message) || /!/.test(header.split(":")[0] || "");
430
+
431
+ if (hasBreaking) {
432
+ return "major";
433
+ }
434
+
435
+ const typeMatch = header.match(/^([a-z]+)(\(|:)/i);
436
+ const commitType = typeMatch ? typeMatch[1] : "";
437
+
438
+ if (commitType === "feat") {
439
+ return "minor";
440
+ }
441
+
442
+ if (commitType === "fix" || commitType === "perf") {
443
+ return "patch";
444
+ }
445
+
446
+ if (!commitType && containsBreakingChange(message)) {
447
+ return "major";
448
+ }
449
+
450
+ return "patch";
451
+ }
452
+
453
+ async function getLastCommitMessage() {
454
+ try {
455
+ const { stdout } = await exec("git log -1 --pretty=%B");
456
+ return stdout.trim();
457
+ } catch (error) {
458
+ return "";
459
+ }
460
+ }
461
+
462
+ async function getCurrentBranch() {
463
+ try {
464
+ const { stdout } = await exec("git rev-parse --abbrev-ref HEAD");
465
+ return stdout.trim();
466
+ } catch (error) {
467
+ return "";
468
+ }
469
+ }
470
+
124
471
  // Create MCP server
125
472
  export const server = new Server(
126
473
  {
@@ -174,6 +521,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
174
521
  required: ["summary"],
175
522
  },
176
523
  },
524
+ {
525
+ name: "create_tests",
526
+ description: "Mark that tests covering the change have been created.",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {},
530
+ },
531
+ },
177
532
  {
178
533
  name: "run_tests",
179
534
  description:
@@ -226,6 +581,45 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
226
581
  properties: {},
227
582
  },
228
583
  },
584
+ {
585
+ name: "commit_and_push",
586
+ description:
587
+ "Automatically run git add, commit, and push once the ready check passes.",
588
+ inputSchema: {
589
+ type: "object",
590
+ properties: {
591
+ commitMessage: {
592
+ type: "string",
593
+ description: "Commit message to use",
594
+ },
595
+ branch: {
596
+ type: "string",
597
+ description:
598
+ "Optional branch name to push to (defaults to current branch)",
599
+ },
600
+ },
601
+ required: [],
602
+ },
603
+ },
604
+ {
605
+ name: "perform_release",
606
+ description:
607
+ "Record release details after committing and pushing your changes.",
608
+ inputSchema: {
609
+ type: "object",
610
+ properties: {
611
+ command: {
612
+ type: "string",
613
+ description: "Release command that was executed",
614
+ },
615
+ notes: {
616
+ type: "string",
617
+ description: "Optional release notes",
618
+ },
619
+ },
620
+ required: ["command"],
621
+ },
622
+ },
229
623
  {
230
624
  name: "complete_task",
231
625
  description:
@@ -291,13 +685,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
291
685
  workflowState.state.currentPhase = "coding";
292
686
  workflowState.state.taskDescription = args.description;
293
687
  workflowState.state.taskType = args.type;
688
+ workflowState.state.commitAndPushCompleted = false;
689
+ workflowState.state.lastCommitMessage = "";
690
+ workflowState.state.lastPushBranch = "";
294
691
  await workflowState.save();
295
692
 
296
693
  return {
297
694
  content: [
298
695
  {
299
696
  type: "text",
300
- text: `✅ 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. ⏳ Commit & push\n\nReminder: Focus on writing clean, maintainable code!`,
697
+ text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n8. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`,
301
698
  },
302
699
  ],
303
700
  };
@@ -316,15 +713,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
316
713
  }
317
714
 
318
715
  workflowState.state.bugFixed = true;
716
+ workflowState.state.testsCreated = false;
319
717
  workflowState.state.currentPhase = "testing";
320
718
  workflowState.state.fixSummary = args.summary;
719
+ workflowState.state.readyCheckCompleted = false;
720
+ workflowState.state.released = false;
721
+ workflowState.state.releaseCommand = "";
722
+ workflowState.state.releaseNotes = "";
723
+ workflowState.state.readyToCommit = false;
724
+ workflowState.state.commitAndPushCompleted = false;
725
+ workflowState.state.lastCommitMessage = "";
726
+ workflowState.state.lastPushBranch = "";
321
727
  await workflowState.save();
322
728
 
323
729
  return {
324
730
  content: [
325
731
  {
326
732
  type: "text",
327
- text: `✅ 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. ⏳ Commit & push\n\n🚫 DO NOT SKIP TESTING!`,
733
+ text: `✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🚫 DO NOT SKIP TESTING!`,
734
+ },
735
+ ],
736
+ };
737
+ }
738
+
739
+ case "create_tests": {
740
+ if (!workflowState.state.bugFixed) {
741
+ return {
742
+ content: [
743
+ {
744
+ type: "text",
745
+ text: "⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!",
746
+ },
747
+ ],
748
+ };
749
+ }
750
+
751
+ workflowState.state.testsCreated = true;
752
+ workflowState.state.readyCheckCompleted = false;
753
+ workflowState.state.released = false;
754
+ workflowState.state.commitAndPushCompleted = false;
755
+ workflowState.state.lastCommitMessage = "";
756
+ workflowState.state.lastPushBranch = "";
757
+ await workflowState.save();
758
+
759
+ return {
760
+ content: [
761
+ {
762
+ type: "text",
763
+ text: "✅ 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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\n🧪 Run your test command and record the results using 'run_tests'.",
328
764
  },
329
765
  ],
330
766
  };
@@ -342,6 +778,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
342
778
  };
343
779
  }
344
780
 
781
+ if (!workflowState.state.testsCreated) {
782
+ return {
783
+ content: [
784
+ {
785
+ type: "text",
786
+ text: "⚠️ Please create tests first using 'create_tests' before recording test results!",
787
+ },
788
+ ],
789
+ };
790
+ }
791
+
345
792
  if (
346
793
  typeof args.passed !== "boolean" ||
347
794
  typeof args.testCommand !== "string" ||
@@ -361,6 +808,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
361
808
  workflowState.state.testCommand = args.testCommand;
362
809
  workflowState.state.testDetails =
363
810
  typeof args.details === "string" ? args.details : "";
811
+ workflowState.state.readyCheckCompleted = false;
812
+ workflowState.state.released = false;
813
+ workflowState.state.commitAndPushCompleted = false;
814
+ workflowState.state.lastCommitMessage = "";
815
+ workflowState.state.lastPushBranch = "";
364
816
 
365
817
  if (!args.passed) {
366
818
  workflowState.state.currentPhase = "testing";
@@ -383,7 +835,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
383
835
  content: [
384
836
  {
385
837
  type: "text",
386
- text: `✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ⏳ Create/update documentation\n5. ⏳ Commit & push\n\nReminder: Document what you did before committing!`,
838
+ text: `✅ All tests passed! 🎉\n\nTest command: ${args.testCommand}\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. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Complete task\n\nReminder: Document what you did before committing!`,
387
839
  },
388
840
  ],
389
841
  };
@@ -406,13 +858,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
406
858
  workflowState.state.documentationSummary = args.summary;
407
859
  workflowState.state.currentPhase = "ready";
408
860
  workflowState.state.readyToCommit = true;
861
+ workflowState.state.readyCheckCompleted = false;
862
+ workflowState.state.released = false;
863
+ workflowState.state.commitAndPushCompleted = false;
864
+ workflowState.state.lastCommitMessage = "";
865
+ workflowState.state.lastPushBranch = "";
409
866
  await workflowState.save();
410
867
 
411
868
  return {
412
869
  content: [
413
870
  {
414
871
  type: "text",
415
- text: `✅ Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\n\n🎉 You're ready to commit!\n\nNext Steps:\n1. ✓ Fix/implement feature\n2. ✓ Create tests\n3. ✓ Run tests (GREEN!)\n4. ✓ Create documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Commit & push\n7. ⏳ Mark as complete with 'complete_task'\n\nRemember: git add . && git commit && git push`,
872
+ text: `✅ Documentation created!\n\nType: ${args.documentationType}\nSummary: ${args.summary}\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 documentation\n5. ⏳ Run 'check_ready_to_commit' to verify\n6. ⏳ Commit & push, then run 'perform_release'\n7. ⏳ Mark as complete with 'complete_task'\n\nRemember: git add . && git commit && git push`,
416
873
  },
417
874
  ],
418
875
  };
@@ -423,6 +880,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
423
880
  const checks = [
424
881
  { name: "Task started", done: status.currentPhase !== "idle" },
425
882
  { name: "Feature/bug fixed", done: status.bugFixed },
883
+ { name: "Tests created", done: status.testsCreated },
426
884
  { name: "Tests created and passed", done: status.testsPassed },
427
885
  { name: "Documentation created", done: status.documentationCreated },
428
886
  ];
@@ -432,12 +890,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
432
890
  .map((c) => `${c.done ? "✅" : "❌"} ${c.name}`)
433
891
  .join("\n");
434
892
 
893
+ workflowState.state.readyCheckCompleted = allDone;
894
+ if (allDone) {
895
+ workflowState.state.currentPhase = workflowState.state.commitAndPushCompleted
896
+ ? workflowState.state.released
897
+ ? "ready_to_complete"
898
+ : "release"
899
+ : "commit";
900
+ }
901
+ await workflowState.save();
902
+
435
903
  if (allDone) {
436
904
  return {
437
905
  content: [
438
906
  {
439
907
  type: "text",
440
- text: `🎉 ALL CHECKS PASSED! You're ready to commit!\n\n${checkList}\n\n✅ You can now:\n1. git add .\n2. git commit -m "your message"\n3. git push\n4. Run 'complete_task' to finish\n\nTask: ${status.taskDescription}`,
908
+ text: `🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' with your commit message\n2. Run 'perform_release' to record the release\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to push to a non-default branch.\n\nTask: ${status.taskDescription}`,
441
909
  },
442
910
  ],
443
911
  };
@@ -453,6 +921,252 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
453
921
  }
454
922
  }
455
923
 
924
+ case "commit_and_push": {
925
+ if (!workflowState.state.readyToCommit) {
926
+ return {
927
+ content: [
928
+ {
929
+ type: "text",
930
+ text: "⚠️ Not ready yet! Run 'check_ready_to_commit' and complete all steps first.",
931
+ },
932
+ ],
933
+ };
934
+ }
935
+
936
+ if (!workflowState.state.readyCheckCompleted) {
937
+ return {
938
+ content: [
939
+ {
940
+ type: "text",
941
+ text: "⚠️ Please run 'check_ready_to_commit' and resolve any outstanding items before committing!",
942
+ },
943
+ ],
944
+ };
945
+ }
946
+
947
+ const providedCommitMessage =
948
+ typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
949
+
950
+ if (!(await hasWorkingChanges())) {
951
+ return {
952
+ content: [
953
+ {
954
+ type: "text",
955
+ text: "⚠️ No changes detected. Make sure you have modifications to commit before running 'commit_and_push'.",
956
+ },
957
+ ],
958
+ };
959
+ }
960
+
961
+ if (!(await hasTestChanges())) {
962
+ return {
963
+ content: [
964
+ {
965
+ type: "text",
966
+ text: "⚠️ Please include test updates in your changes before running 'commit_and_push'. Ensure at least one test file is modified.",
967
+ },
968
+ ],
969
+ };
970
+ }
971
+
972
+ try {
973
+ await exec("git add .");
974
+ } catch (error) {
975
+ return {
976
+ content: [
977
+ {
978
+ type: "text",
979
+ text: `❌ Failed to stage files:\n\n${error.stderr || error.stdout || error.message}`,
980
+ },
981
+ ],
982
+ };
983
+ }
984
+
985
+ const stagedChanges = await getStagedChanges();
986
+ const { summary: generatedSummary, body: commitBody } =
987
+ createCommitMessageParts(stagedChanges, providedCommitMessage);
988
+
989
+ if (!generatedSummary) {
990
+ return {
991
+ content: [
992
+ {
993
+ type: "text",
994
+ text: "⚠️ Unable to generate commit message. Please provide a 'commitMessage' manually.",
995
+ },
996
+ ],
997
+ };
998
+ }
999
+
1000
+ try {
1001
+ if (commitBody) {
1002
+ await exec(
1003
+ `git commit -m ${shellEscape(generatedSummary)} -m ${shellEscape(commitBody)}`
1004
+ );
1005
+ } else {
1006
+ await exec(`git commit -m ${shellEscape(generatedSummary)}`);
1007
+ }
1008
+ } catch (error) {
1009
+ const output = error.stderr || error.stdout || error.message || "Unknown error";
1010
+ if (output.includes("nothing to commit")) {
1011
+ return {
1012
+ content: [
1013
+ {
1014
+ type: "text",
1015
+ text: "⚠️ Nothing to commit. Make additional changes before running 'commit_and_push'.",
1016
+ },
1017
+ ],
1018
+ };
1019
+ }
1020
+
1021
+ return {
1022
+ content: [
1023
+ {
1024
+ type: "text",
1025
+ text: `❌ git commit failed:\n\n${output}`,
1026
+ },
1027
+ ],
1028
+ };
1029
+ }
1030
+
1031
+ const requestedBranch =
1032
+ typeof args.branch === "string" ? args.branch.trim() : "";
1033
+ const currentBranch = await getCurrentBranch();
1034
+ const pushCommand = requestedBranch
1035
+ ? `git push origin ${shellEscape(requestedBranch)}`
1036
+ : "git push";
1037
+
1038
+ try {
1039
+ await exec(pushCommand);
1040
+ } catch (error) {
1041
+ return {
1042
+ content: [
1043
+ {
1044
+ type: "text",
1045
+ text: `❌ git push failed:\n\n${error.stderr || error.stdout || error.message}`,
1046
+ },
1047
+ ],
1048
+ };
1049
+ }
1050
+
1051
+ workflowState.state.commitAndPushCompleted = true;
1052
+ workflowState.state.lastCommitMessage = generatedSummary;
1053
+ workflowState.state.lastPushBranch =
1054
+ requestedBranch || currentBranch || "";
1055
+ workflowState.state.currentPhase = workflowState.state.released
1056
+ ? "ready_to_complete"
1057
+ : "release";
1058
+ await workflowState.save();
1059
+
1060
+ return {
1061
+ content: [
1062
+ {
1063
+ type: "text",
1064
+ text: `✅ Commit & push completed!\n\nCommit message: ${generatedSummary}\nPushed to: ${
1065
+ requestedBranch || currentBranch || "(default upstream)"
1066
+ }\n\nNext: Run 'perform_release' to record the release.`,
1067
+ },
1068
+ ],
1069
+ };
1070
+ }
1071
+
1072
+ case "perform_release": {
1073
+ if (workflowState.state.currentPhase === "idle") {
1074
+ return {
1075
+ content: [
1076
+ {
1077
+ type: "text",
1078
+ text: "⚠️ Please start a task first using 'start_task'!",
1079
+ },
1080
+ ],
1081
+ };
1082
+ }
1083
+
1084
+ if (!workflowState.state.commitAndPushCompleted) {
1085
+ return {
1086
+ content: [
1087
+ {
1088
+ type: "text",
1089
+ text: "⚠️ Please run 'commit_and_push' after the ready check before recording a release!",
1090
+ },
1091
+ ],
1092
+ };
1093
+ }
1094
+
1095
+ if (!workflowState.state.readyCheckCompleted) {
1096
+ return {
1097
+ content: [
1098
+ {
1099
+ type: "text",
1100
+ text: "⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before releasing!",
1101
+ },
1102
+ ],
1103
+ };
1104
+ }
1105
+
1106
+ const releaseCommand =
1107
+ typeof args.command === "string" ? args.command.trim() : "";
1108
+
1109
+ if (!releaseCommand) {
1110
+ return {
1111
+ content: [
1112
+ {
1113
+ type: "text",
1114
+ text: "⚠️ Please provide a non-empty 'command' when using 'perform_release'.",
1115
+ },
1116
+ ],
1117
+ };
1118
+ }
1119
+
1120
+ if (await hasWorkingChanges()) {
1121
+ return {
1122
+ content: [
1123
+ {
1124
+ type: "text",
1125
+ text: "⚠️ Working tree not clean! Please commit or stash all changes before running 'perform_release'.",
1126
+ },
1127
+ ],
1128
+ };
1129
+ }
1130
+
1131
+ const lastCommitMessage = await getLastCommitMessage();
1132
+ const releaseType = determineReleaseTypeFromCommit(
1133
+ lastCommitMessage || workflowState.state.lastCommitMessage
1134
+ );
1135
+
1136
+ const finalCommand = releaseCommand.includes("--release-as")
1137
+ ? releaseCommand
1138
+ : `${releaseCommand} -- --release-as ${releaseType}`;
1139
+
1140
+ try {
1141
+ await exec(finalCommand);
1142
+ } catch (error) {
1143
+ return {
1144
+ content: [
1145
+ {
1146
+ type: "text",
1147
+ text: `❌ Release command failed:\n\n${error.stderr || error.stdout || error.message}`,
1148
+ },
1149
+ ],
1150
+ };
1151
+ }
1152
+
1153
+ workflowState.state.released = true;
1154
+ workflowState.state.releaseCommand = finalCommand;
1155
+ workflowState.state.releaseNotes =
1156
+ typeof args.notes === "string" ? args.notes : "";
1157
+ workflowState.state.currentPhase = "ready_to_complete";
1158
+ await workflowState.save();
1159
+
1160
+ return {
1161
+ content: [
1162
+ {
1163
+ type: "text",
1164
+ text: `🚀 Release recorded!\n\nCommand: ${finalCommand}\nRelease type: ${releaseType}\n${workflowState.state.releaseNotes ? `Notes: ${workflowState.state.releaseNotes}\n` : ""}\n✅ Next: Run 'complete_task' to wrap up.`,
1165
+ },
1166
+ ],
1167
+ };
1168
+ }
1169
+
456
1170
  case "complete_task": {
457
1171
  if (!workflowState.state.readyToCommit) {
458
1172
  return {
@@ -465,6 +1179,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
465
1179
  };
466
1180
  }
467
1181
 
1182
+ if (!workflowState.state.readyCheckCompleted) {
1183
+ return {
1184
+ content: [
1185
+ {
1186
+ type: "text",
1187
+ text: "⚠️ Please run 'check_ready_to_commit' and ensure all checks pass before completing the task!",
1188
+ },
1189
+ ],
1190
+ };
1191
+ }
1192
+
1193
+ if (!workflowState.state.commitAndPushCompleted) {
1194
+ return {
1195
+ content: [
1196
+ {
1197
+ type: "text",
1198
+ text: "⚠️ Commit and push not completed! Run 'commit_and_push' first.",
1199
+ },
1200
+ ],
1201
+ };
1202
+ }
1203
+
1204
+ if (!workflowState.state.released) {
1205
+ return {
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: "⚠️ Release not recorded! Run 'perform_release' after committing and pushing.",
1210
+ },
1211
+ ],
1212
+ };
1213
+ }
1214
+
468
1215
  if (await hasStagedChanges()) {
469
1216
  return {
470
1217
  content: [
@@ -483,6 +1230,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
483
1230
  testCommand: workflowState.state.testCommand,
484
1231
  documentationType: workflowState.state.documentationType,
485
1232
  commitMessage: args.commitMessage,
1233
+ releaseCommand: workflowState.state.releaseCommand,
1234
+ releaseNotes: workflowState.state.releaseNotes,
486
1235
  });
487
1236
 
488
1237
  const completedTask = workflowState.state.taskDescription;
@@ -518,14 +1267,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
518
1267
  coding: "🔨 Currently coding",
519
1268
  testing: "🧪 Ready for testing",
520
1269
  documentation: "📝 Ready for documentation",
521
- ready: "✅ Ready to commit",
1270
+ ready: "✅ Ready for verification",
1271
+ commit: "📦 Ready to commit & push",
1272
+ release: "🚀 Ready to release",
1273
+ ready_to_complete: "✅ Release completed",
522
1274
  };
523
1275
 
524
1276
  return {
525
1277
  content: [
526
1278
  {
527
1279
  type: "text",
528
- text: `📋 Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "✅" : "⏳"} Feature/bug fixed\n${status.testsPassed ? "✅" : "⏳"} Tests passed\n${status.documentationCreated ? "✅" : "⏳"} Documentation created\n${status.readyToCommit ? "✅" : "⏳"} Ready to commit\n\nNext: ${getNextStep(status)}`,
1280
+ text: `📋 Current Workflow Status\n\nTask: ${status.taskDescription}\nPhase: ${phaseMessages[status.currentPhase]}\n\nProgress:\n${status.bugFixed ? "✅" : "⏳"} Feature/bug fixed\n${status.testsCreated ? "✅" : "⏳"} Tests created\n${status.testsPassed ? "✅" : "⏳"} Tests passed\n${status.documentationCreated ? "✅" : "⏳"} Documentation created\n${status.readyCheckCompleted ? "✅" : "⏳"} Ready check completed\n${status.commitAndPushCompleted ? "✅" : "⏳"} Commit & push completed\n${status.released ? "✅" : "⏳"} Release recorded\n\nNext: ${getNextStep(status)}`,
529
1281
  },
530
1282
  ],
531
1283
  };
@@ -582,10 +1334,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
582
1334
  // Helper function
583
1335
  export function getNextStep(status) {
584
1336
  if (!status.bugFixed) return "Mark feature/bug as fixed";
585
- if (!status.testsPassed) return "Create and run tests";
1337
+ if (!status.testsCreated) return "Create tests";
1338
+ if (!status.testsPassed) return "Run tests";
586
1339
  if (!status.documentationCreated) return "Create documentation";
587
- if (!status.readyToCommit) return "Check if ready to commit";
588
- return "Commit and push!";
1340
+ if (!status.readyCheckCompleted) return "Run 'check_ready_to_commit'";
1341
+ if (!status.commitAndPushCompleted) return "Run 'commit_and_push'";
1342
+ if (!status.released) return "Run 'perform_release'";
1343
+ return "Complete the task";
589
1344
  }
590
1345
 
591
1346
  // Define prompts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.0.2",
3
+ "version": "1.0.7",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -3,7 +3,14 @@ import assert from "node:assert/strict";
3
3
  import { mkdtemp, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
- import { WorkflowState, getNextStep } from "../index.js";
6
+ import {
7
+ WorkflowState,
8
+ getNextStep,
9
+ containsTestFilesInStatus,
10
+ createCommitMessageParts,
11
+ determineReleaseTypeFromCommit,
12
+ workingTreeSummary,
13
+ } from "../index.js";
7
14
 
8
15
  async function withWorkflowState(callback) {
9
16
  const tempDir = await mkdtemp(path.join(tmpdir(), "workflow-state-"));
@@ -23,6 +30,14 @@ test("WorkflowState persists data across save/load", async () => {
23
30
  state.state.currentPhase = "testing";
24
31
  state.state.taskDescription = "Test persistence";
25
32
  state.state.bugFixed = true;
33
+ state.state.testsCreated = true;
34
+ state.state.readyCheckCompleted = true;
35
+ state.state.released = true;
36
+ state.state.releaseCommand = "npm run release";
37
+ state.state.releaseNotes = "Automated release";
38
+ state.state.commitAndPushCompleted = true;
39
+ state.state.lastCommitMessage = "test: persistence";
40
+ state.state.lastPushBranch = "main";
26
41
  await state.save();
27
42
 
28
43
  const reloaded = new WorkflowState(state.stateFile);
@@ -31,21 +46,37 @@ test("WorkflowState persists data across save/load", async () => {
31
46
  assert.equal(reloaded.state.currentPhase, "testing");
32
47
  assert.equal(reloaded.state.taskDescription, "Test persistence");
33
48
  assert.equal(reloaded.state.bugFixed, true);
49
+ assert.equal(reloaded.state.testsCreated, true);
50
+ assert.equal(reloaded.state.readyCheckCompleted, true);
51
+ assert.equal(reloaded.state.released, true);
52
+ assert.equal(reloaded.state.releaseCommand, "npm run release");
53
+ assert.equal(reloaded.state.releaseNotes, "Automated release");
54
+ assert.equal(reloaded.state.commitAndPushCompleted, true);
55
+ assert.equal(reloaded.state.lastCommitMessage, "test: persistence");
56
+ assert.equal(reloaded.state.lastPushBranch, "main");
34
57
  });
35
58
  });
36
59
 
37
60
  test("WorkflowState reset keeps history but clears progress", async () => {
38
61
  await withWorkflowState(async (state) => {
39
62
  state.state.currentPhase = "ready";
63
+ state.state.testsCreated = true;
40
64
  state.state.testsPassed = true;
41
65
  state.state.documentationCreated = true;
66
+ state.state.readyCheckCompleted = true;
67
+ state.state.released = true;
68
+ state.state.commitAndPushCompleted = true;
42
69
  state.addToHistory({ taskDescription: "Sample task" });
43
70
 
44
71
  state.reset();
45
72
 
46
73
  assert.equal(state.state.currentPhase, "idle");
74
+ assert.equal(state.state.testsCreated, false);
47
75
  assert.equal(state.state.testsPassed, false);
48
76
  assert.equal(state.state.documentationCreated, false);
77
+ assert.equal(state.state.readyCheckCompleted, false);
78
+ assert.equal(state.state.released, false);
79
+ assert.equal(state.state.commitAndPushCompleted, false);
49
80
  assert.equal(state.state.history.length, 1);
50
81
  assert.equal(state.state.history[0].taskDescription, "Sample task");
51
82
  });
@@ -54,9 +85,12 @@ test("WorkflowState reset keeps history but clears progress", async () => {
54
85
  test("getNextStep guides workflow progression", () => {
55
86
  const baseState = {
56
87
  bugFixed: false,
88
+ testsCreated: false,
57
89
  testsPassed: false,
58
90
  documentationCreated: false,
59
- readyToCommit: false,
91
+ readyCheckCompleted: false,
92
+ commitAndPushCompleted: false,
93
+ released: false,
60
94
  };
61
95
 
62
96
  assert.equal(
@@ -67,21 +101,104 @@ test("getNextStep guides workflow progression", () => {
67
101
  const afterFix = { ...baseState, bugFixed: true };
68
102
  assert.equal(
69
103
  getNextStep(afterFix),
70
- "Create and run tests"
104
+ "Create tests"
71
105
  );
72
106
 
73
- const afterTests = { ...afterFix, testsPassed: true };
107
+ const afterTestsCreated = { ...afterFix, testsCreated: true };
108
+ assert.equal(getNextStep(afterTestsCreated), "Run tests");
109
+
110
+ const afterTests = { ...afterTestsCreated, testsPassed: true };
74
111
  assert.equal(
75
112
  getNextStep(afterTests),
76
113
  "Create documentation"
77
114
  );
78
115
 
79
116
  const afterDocs = { ...afterTests, documentationCreated: true };
80
- assert.equal(
81
- getNextStep(afterDocs),
82
- "Check if ready to commit"
83
- );
117
+ assert.equal(getNextStep(afterDocs), "Run 'check_ready_to_commit'");
118
+
119
+ const afterReadyCheck = { ...afterDocs, readyCheckCompleted: true };
120
+ assert.equal(getNextStep(afterReadyCheck), "Run 'commit_and_push'");
121
+
122
+ const afterCommit = { ...afterReadyCheck, commitAndPushCompleted: true };
123
+ assert.equal(getNextStep(afterCommit), "Run 'perform_release'");
124
+
125
+ const afterRelease = { ...afterCommit, released: true };
126
+ assert.equal(getNextStep(afterRelease), "Complete the task");
127
+ });
128
+
129
+ test("containsTestFilesInStatus detects modified test files", () => {
130
+ const statusOutput = [
131
+ " M src/app.test.js",
132
+ "A tests/helpers/util.js",
133
+ "R src/old.js -> src/new.spec.ts",
134
+ ].join("\n");
135
+
136
+ assert.equal(containsTestFilesInStatus(statusOutput), true);
137
+ });
138
+
139
+ test("containsTestFilesInStatus returns false when no test files present", () => {
140
+ const statusOutput = [
141
+ " M src/app.js",
142
+ "A docs/README.md",
143
+ "?? assets/logo.png",
144
+ ].join("\n");
145
+
146
+ assert.equal(containsTestFilesInStatus(statusOutput), false);
147
+ });
148
+
149
+ test("createCommitMessageParts generates test prefixed summary with details", () => {
150
+ const changes = [
151
+ { status: "M", path: "src/app.test.js" },
152
+ { status: "A", path: "tests/helpers/util.js" },
153
+ ];
154
+
155
+ const { summary, body } = createCommitMessageParts(changes);
156
+
157
+ assert.equal(summary.startsWith("test:"), true);
158
+ assert.equal(body.includes("- modified src/app.test.js"), true);
159
+ assert.equal(body.includes("- added tests/helpers/util.js"), true);
160
+ });
161
+
162
+ test("createCommitMessageParts respects provided summary when present", () => {
163
+ const changes = [{ status: "M", path: "docs/guide.md" }];
164
+ const provided = "docs: update guide";
165
+
166
+ const { summary, body } = createCommitMessageParts(changes, provided);
167
+
168
+ assert.equal(summary, provided);
169
+ assert.equal(body.includes("- modified docs/guide.md"), true);
170
+ });
171
+
172
+ test("determineReleaseTypeFromCommit returns major for breaking change", () => {
173
+ const message = "feat!: overhaul API\n\nBREAKING CHANGE: new behavior";
174
+ assert.equal(determineReleaseTypeFromCommit(message), "major");
175
+ });
176
+
177
+ test("determineReleaseTypeFromCommit returns minor for feat", () => {
178
+ const message = "feat: add dashboard";
179
+ assert.equal(determineReleaseTypeFromCommit(message), "minor");
180
+ });
181
+
182
+ test("determineReleaseTypeFromCommit returns patch for fix", () => {
183
+ const message = "fix: resolve login issue";
184
+ assert.equal(determineReleaseTypeFromCommit(message), "patch");
185
+ });
186
+
187
+ test("determineReleaseTypeFromCommit defaults to patch for other commit types", () => {
188
+ const message = "chore: update dependencies";
189
+ assert.equal(determineReleaseTypeFromCommit(message), "patch");
190
+ });
191
+
192
+ test("workingTreeSummary reports no changes for empty output", () => {
193
+ const summary = workingTreeSummary("\n \n");
194
+ assert.equal(summary.hasChanges, false);
195
+ assert.deepEqual(summary.lines, []);
196
+ });
197
+
198
+ test("workingTreeSummary lists trimmed status lines", () => {
199
+ const statusOutput = " M src/app.js\nA tests/new.test.js\n";
200
+ const summary = workingTreeSummary(statusOutput);
84
201
 
85
- const ready = { ...afterDocs, readyToCommit: true };
86
- assert.equal(getNextStep(ready), "Commit and push!");
202
+ assert.equal(summary.hasChanges, true);
203
+ assert.deepEqual(summary.lines, ["M src/app.js", "A tests/new.test.js"]);
87
204
  });