@programinglive/dev-workflow-mcp-server 1.7.16 → 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 CHANGED
@@ -2,6 +2,23 @@
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.7.17](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.16...v1.7.17) (2025-12-26)
6
+
7
+
8
+ ### šŸ“ Documentation
9
+
10
+ * update release notes for v1.7.16 ([0afd96b](https://github.com/programinglive/dev-workflow-mcp-server/commit/0afd96b754717953eb162f65426129457fc8626e))
11
+
12
+
13
+ ### ✨ Features
14
+
15
+ * allow skipping release step ([004e4fa](https://github.com/programinglive/dev-workflow-mcp-server/commit/004e4fae071e064b2c2efb9d69ecd97e794d3cb8))
16
+
17
+
18
+ ### 🧹 Chores
19
+
20
+ * add @programinglive/commiter setup ([1d2a19d](https://github.com/programinglive/dev-workflow-mcp-server/commit/1d2a19d0453b50c6f0db35b15924b51a27c4891a))
21
+
5
22
  ### [1.7.16](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.7.15...v1.7.16) (2025-12-24)
6
23
 
7
24
 
package/README.md CHANGED
@@ -512,7 +512,7 @@ This keeps workflow state local to each developer's machine.
512
512
  - `create_documentation` - Mark documentation as created
513
513
  - `check_ready_to_commit` - Verify all steps are complete
514
514
  - `commit_and_push` - Commit and push changes
515
- - `perform_release` - Record release details
515
+ - `perform_release` - Record release details (or use `skip_release` when the project has no release automation)
516
516
  - `complete_task` - Mark task as complete and reset
517
517
  - `force_complete_task` - Force completion with reason
518
518
  - `drop_task` - Abandon current task
@@ -553,6 +553,7 @@ The tool will:
553
553
  5. `check_ready_to_commit`
554
554
  6. `commit_and_push` with `commitMessage` and optional `branch`
555
555
  7. `perform_release` with `releaseCommand`, plus optional `releaseNotes`, `releaseType`, and `preset`
556
+ - Alternatively call `skip_release` with a justification when the repository has no Node-based release step (e.g., Python-only or docs-only tasks)
556
557
  8. `complete_task` reusing `commitMessage`
557
558
 
558
559
  All arguments except the optional flags are required and must be non-empty strings.
@@ -571,7 +572,7 @@ The package ships with a release guard (`release-wrapper.js`) that backs the `np
571
572
 
572
573
  If any requirements are missing, the guard exits with guidance to return to the MCP tools. This prevents accidentally bumping versions or tagging releases outside the managed workflow. To release correctly:
573
574
 
574
- 1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client.
575
+ 1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client, or `skip_release {"reason":"<explanation>"}` if no release applies.
575
576
  2. The guard runs automatically, verifies the workflow state, and records the release before letting you finish with `complete_task`.
576
577
 
577
578
  ### Automated npm Publishing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.7.16",
3
+ "version": "1.7.17",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "homepage": "https://devworkflow.programinglive.com/",
6
6
  "mcpName": "io.github.programinglive/dev-workflow-mcp-server",
@@ -62,11 +62,13 @@
62
62
  "devDependencies": {
63
63
  "@commitlint/cli": "^20.2.0",
64
64
  "@commitlint/config-conventional": "^20.2.0",
65
- "@programinglive/commiter": "^1.2.16",
65
+ "@programinglive/commiter": "^1.2.17",
66
66
  "autoprefixer": "^10.4.22",
67
+ "cross-env": "^10.1.0",
67
68
  "husky": "^9.1.7",
68
69
  "postcss": "^8.5.6",
69
70
  "standard-version": "^9.5.0",
71
+ "supertest": "^7.1.4",
70
72
  "tailwindcss": "^3.4.18"
71
73
  },
72
74
  "standard-version": {
@@ -11,6 +11,9 @@ const FS_FOK_PRELOAD_FLAG = buildPreloadFlag(fsFokPreloadPath);
11
11
 
12
12
  require('./preload/fs-f-ok.cjs');
13
13
 
14
+ const DEFAULT_PRD_RELATIVE_PATH = path.join('docs', 'PRD.md');
15
+ const DEFAULT_PRD_DISPLAY_PATH = 'docs/PRD.md';
16
+
14
17
  const VALID_RELEASE_TYPES = new Set([
15
18
  'major',
16
19
  'minor',
@@ -161,6 +164,8 @@ function runRelease({
161
164
  throw new Error('Working tree has uncommitted changes. Commit or stash before running release.');
162
165
  }
163
166
 
167
+ ensurePrdPresence({ cwd, dependencies });
168
+
164
169
  const testResult = runProjectTests({ spawn, env });
165
170
  if (testResult && typeof testResult.status === 'number' && testResult.status !== 0) {
166
171
  return testResult;
@@ -219,9 +224,31 @@ module.exports = {
219
224
  detectPackageManager,
220
225
  runProjectTests,
221
226
  runRelease,
222
- isWorkingTreeClean
227
+ isWorkingTreeClean,
228
+ ensurePrdPresence
223
229
  };
224
230
 
231
+ function ensurePrdPresence({ cwd = process.cwd(), dependencies = {} } = {}) {
232
+ const {
233
+ fsExistsSync = fs.existsSync,
234
+ logger = console,
235
+ prdRelativePath = DEFAULT_PRD_RELATIVE_PATH,
236
+ prdDisplayPath = DEFAULT_PRD_DISPLAY_PATH
237
+ } = dependencies;
238
+
239
+ const resolvedPath = path.isAbsolute(prdRelativePath)
240
+ ? prdRelativePath
241
+ : path.join(cwd, prdRelativePath);
242
+
243
+ if (fsExistsSync(resolvedPath)) {
244
+ return true;
245
+ }
246
+
247
+ const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : console.warn.bind(console);
248
+ warn(`āš ļø Product Requirements Document missing (${prdDisplayPath}). Add ${DEFAULT_PRD_DISPLAY_PATH} so teams understand release expectations.`);
249
+ return false;
250
+ }
251
+
225
252
  function buildPreloadFlag(filePath) {
226
253
  const resolved = path.resolve(filePath);
227
254
  const escaped = resolved.includes(' ')
@@ -8,7 +8,7 @@ import path from 'path';
8
8
  const exec = promisify(execCallback);
9
9
 
10
10
  test('build generates dist/index.mjs file', async () => {
11
- const { stderr } = await exec('NODE_ENV=development npm run build:server');
11
+ const { stderr } = await exec('npx cross-env NODE_ENV=development npm run build:server');
12
12
  // Allow the known vite warning about node:process
13
13
  const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
14
  const tailwindOutput = 'ā‰ˆ tailwindcss';
@@ -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";
@@ -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":
package/workflow-state.js CHANGED
@@ -440,6 +440,8 @@ export class WorkflowState {
440
440
  readyToCommit: false,
441
441
  readyCheckCompleted: false,
442
442
  released: false,
443
+ releaseSkipped: false,
444
+ releaseSkippedReason: "",
443
445
  releaseCommand: "",
444
446
  releaseNotes: "",
445
447
  commitAndPushCompleted: false,
@@ -477,6 +479,12 @@ export class WorkflowState {
477
479
  if (typeof this.state.released !== "boolean") {
478
480
  this.state.released = false;
479
481
  }
482
+ if (typeof this.state.releaseSkipped !== "boolean") {
483
+ this.state.releaseSkipped = false;
484
+ }
485
+ if (typeof this.state.releaseSkippedReason !== "string") {
486
+ this.state.releaseSkippedReason = "";
487
+ }
480
488
  if (typeof this.state.commitAndPushCompleted !== "boolean") {
481
489
  this.state.commitAndPushCompleted = false;
482
490
  }
@@ -622,6 +630,8 @@ export class WorkflowState {
622
630
  readyToCommit: false,
623
631
  readyCheckCompleted: false,
624
632
  released: false,
633
+ releaseSkipped: false,
634
+ releaseSkippedReason: "",
625
635
  releaseCommand: "",
626
636
  releaseNotes: "",
627
637
  commitAndPushCompleted: false,