@programinglive/dev-workflow-mcp-server 1.1.3 → 1.1.5

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,20 @@
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.1.5](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.4...v1.1.5) (2025-10-25)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * enforce release guard and per-project workflow state ([1c845a1](https://github.com/programinglive/dev-workflow-mcp-server/commit/1c845a1eb164863ab0269510b142e28ed1ea2136))
11
+
12
+ ### [1.1.4](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.3...v1.1.4) (2025-10-25)
13
+
14
+
15
+ ### 🐛 Bug Fixes
16
+
17
+ * ensure install script uses project root for workflow state ([8b8b32f](https://github.com/programinglive/dev-workflow-mcp-server/commit/8b8b32f5c436b256ec1edb05a406405a5c2455c1))
18
+
5
19
  ### [1.1.3](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.2...v1.1.3) (2025-10-25)
6
20
 
7
21
 
package/README.md CHANGED
@@ -24,7 +24,7 @@ Each project gets its own isolated workflow state file.
24
24
  npm install @programinglive/dev-workflow-mcp-server
25
25
  ```
26
26
 
27
- This will automatically create a `.workflow-state.json` file in your project root, keeping workflow history separate per project.
27
+ This will automatically create a `.workflow-state.json` file in the **project where you ran `npm install`** (using npm's `INIT_CWD`), keeping workflow history separate per project. If you're installing the package itself (inside `node_modules`), the script skips creation so it never pollutes the shared package directory.
28
28
 
29
29
  ### Option 2: Install from Source
30
30
 
@@ -149,6 +149,19 @@ If you're using this package, add this to your project's `.gitignore`:
149
149
 
150
150
  This keeps workflow state local to each developer's machine.
151
151
 
152
+ ## đŸšĢ Releasing Without the Workflow Steps
153
+
154
+ The package ships with a release guard (`release-wrapper.js`) that backs the `npm run release:*` scripts. The guard refuses to run unless:
155
+
156
+ - The current workflow phase is **release**
157
+ - `check_ready_to_commit` and `commit_and_push` have been completed
158
+ - A release has not already been recorded for the active task
159
+
160
+ 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:
161
+
162
+ 1. Use `perform_release {"command":"patch"}` (or `minor`/`major`) via the MCP client.
163
+ 2. The guard runs automatically, verifies the workflow state, and records the release before letting you finish with `complete_task`.
164
+
152
165
  ## đŸ› ī¸ Available Tools
153
166
 
154
167
  ### Tool Argument Requirements
package/install.js CHANGED
@@ -14,8 +14,17 @@ const __dirname = path.dirname(__filename);
14
14
 
15
15
  async function install() {
16
16
  try {
17
- // Get the project root (where npm install was run)
18
- const projectRoot = process.cwd();
17
+ // npm sets INIT_CWD to the directory where `npm install` was originally invoked.
18
+ // Fallback to process.cwd() if it's not present (e.g., older npm versions).
19
+ const projectRoot = process.env.INIT_CWD || process.cwd();
20
+
21
+ // If INIT_CWD points inside node_modules, this script is being executed
22
+ // while installing the package itself. In that case, skip creating a state file.
23
+ if (projectRoot.includes(`node_modules${path.sep}`)) {
24
+ console.log('â„šī¸ Skipping workflow state creation inside node_modules.');
25
+ return;
26
+ }
27
+
19
28
  const stateFile = path.join(projectRoot, '.workflow-state.json');
20
29
 
21
30
  // Check if state file already exists
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -15,9 +15,9 @@
15
15
  "prepare": "husky",
16
16
  "postinstall": "node install.js",
17
17
  "release": "standard-version",
18
- "release:major": "npm run release -- --release-as major",
19
- "release:minor": "npm run release -- --release-as minor",
20
- "release:patch": "npm run release -- --release-as patch"
18
+ "release:major": "node release-wrapper.js major",
19
+ "release:minor": "node release-wrapper.js minor",
20
+ "release:patch": "node release-wrapper.js patch"
21
21
  },
22
22
  "keywords": [
23
23
  "mcp",
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+
7
+ const RELEASE_TYPES = new Set(['major', 'minor', 'patch']);
8
+
9
+ async function main() {
10
+ const releaseTypeArg = (process.argv[2] || '').toLowerCase();
11
+
12
+ if (!RELEASE_TYPES.has(releaseTypeArg)) {
13
+ console.error(
14
+ '❌ Release guard: expected release type argument (major | minor | patch). Example: npm run release:patch'
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ const projectRoot = process.env.INIT_CWD || process.cwd();
20
+ if (projectRoot.includes(`node_modules${path.sep}`)) {
21
+ console.error('❌ Release guard: refusing to run inside node_modules.');
22
+ process.exit(1);
23
+ }
24
+
25
+ const statePath = path.join(projectRoot, '.workflow-state.json');
26
+ let state;
27
+
28
+ try {
29
+ const raw = await fs.readFile(statePath, 'utf8');
30
+ state = JSON.parse(raw);
31
+ } catch (error) {
32
+ console.error(
33
+ `❌ Release guard: cannot locate workflow state at ${statePath}. Make sure the dev-workflow MCP is installed and you have started a task.`
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ const errors = [];
39
+ if (state.currentPhase !== 'release') {
40
+ errors.push(
41
+ "Current workflow phase isn't 'release'. Complete documentation, commit, and push steps first."
42
+ );
43
+ }
44
+ if (!state.readyCheckCompleted) {
45
+ errors.push("'check_ready_to_commit' has not been completed.");
46
+ }
47
+ if (!state.commitAndPushCompleted) {
48
+ errors.push("'commit_and_push' has not been completed.");
49
+ }
50
+ if (state.released) {
51
+ errors.push('A release has already been recorded for this task.');
52
+ }
53
+
54
+ if (errors.length) {
55
+ console.error('❌ Release guard blocked the release:');
56
+ for (const message of errors) {
57
+ console.error(` â€ĸ ${message}`);
58
+ }
59
+ console.error("\nUse 'perform_release' through the dev-workflow MCP after completing the missing steps.");
60
+ process.exit(1);
61
+ }
62
+
63
+ const runRelease = async () => {
64
+ if (process.env.DEV_WORKFLOW_SKIP_RELEASE === '1') {
65
+ console.log('â„šī¸ DEV_WORKFLOW_SKIP_RELEASE=1 detected. Skipping actual release command.');
66
+ return 0;
67
+ }
68
+
69
+ return await new Promise((resolve, reject) => {
70
+ const child = spawn(
71
+ 'npm',
72
+ ['run', 'release', '--', '--release-as', releaseTypeArg],
73
+ { stdio: 'inherit', shell: process.platform === 'win32', cwd: projectRoot }
74
+ );
75
+
76
+ child.on('error', (error) => reject(error));
77
+ child.on('close', (code) => resolve(code));
78
+ });
79
+ };
80
+
81
+ try {
82
+ const exitCode = await runRelease();
83
+ if (exitCode !== 0) {
84
+ console.error(`❌ Release command exited with code ${exitCode}.`);
85
+ process.exit(exitCode);
86
+ }
87
+ } catch (error) {
88
+ console.error(`❌ Failed to run release command: ${error.message}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ state.released = true;
93
+ state.releaseCommand = `npm run release:${releaseTypeArg}`;
94
+ state.currentPhase = 'ready_to_complete';
95
+
96
+ try {
97
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2));
98
+ } catch (error) {
99
+ console.error(`❌ Release guard: failed to update workflow state. ${error.message}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ console.log('✅ Release recorded in workflow state. You can now run complete_task.');
104
+ }
105
+
106
+ main();
@@ -0,0 +1,91 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+
8
+ const PROJECT_ROOT = path.resolve(new URL("..", import.meta.url).pathname);
9
+ const RELEASE_WRAPPER = path.join(PROJECT_ROOT, "release-wrapper.js");
10
+
11
+ async function runWrapper({ state, args = ["patch"], env = {} }) {
12
+ const dir = await mkdtemp(path.join(tmpdir(), "release-wrapper-test-"));
13
+ const statePath = path.join(dir, ".workflow-state.json");
14
+ await writeFile(statePath, JSON.stringify(state, null, 2));
15
+
16
+ const childEnv = {
17
+ ...process.env,
18
+ INIT_CWD: dir,
19
+ DEV_WORKFLOW_SKIP_RELEASE: "1",
20
+ ...env,
21
+ };
22
+
23
+ return await new Promise((resolve) => {
24
+ const child = spawn(
25
+ process.execPath,
26
+ [RELEASE_WRAPPER, ...args],
27
+ {
28
+ cwd: PROJECT_ROOT,
29
+ env: childEnv,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ }
32
+ );
33
+
34
+ let stdout = "";
35
+ let stderr = "";
36
+
37
+ child.stdout.on("data", (chunk) => {
38
+ stdout += chunk;
39
+ });
40
+ child.stderr.on("data", (chunk) => {
41
+ stderr += chunk;
42
+ });
43
+
44
+ child.on("close", async (code) => {
45
+ let updatedState = null;
46
+ try {
47
+ const rawState = await readFile(statePath, "utf8");
48
+ updatedState = JSON.parse(rawState);
49
+ } catch {
50
+ // ignore
51
+ }
52
+
53
+ await rm(dir, { recursive: true, force: true });
54
+ resolve({ code, stdout, stderr, updatedState });
55
+ });
56
+ });
57
+ }
58
+
59
+ test("release-wrapper blocks when workflow phase is not release", async () => {
60
+ const result = await runWrapper({
61
+ state: {
62
+ currentPhase: "commit",
63
+ readyCheckCompleted: true,
64
+ commitAndPushCompleted: true,
65
+ released: false,
66
+ },
67
+ });
68
+
69
+ assert.equal(result.code, 1);
70
+ assert.ok(result.stderr.includes("Release guard blocked the release"));
71
+ assert.ok(result.stderr.includes("Current workflow phase"));
72
+ assert.equal(result.updatedState.released, false);
73
+ });
74
+
75
+ test("release-wrapper marks release after validation passes", async () => {
76
+ const result = await runWrapper({
77
+ state: {
78
+ currentPhase: "release",
79
+ readyCheckCompleted: true,
80
+ commitAndPushCompleted: true,
81
+ released: false,
82
+ releaseCommand: "",
83
+ },
84
+ });
85
+
86
+ assert.equal(result.code, 0);
87
+ assert.ok(result.stdout.includes("Release recorded"));
88
+ assert.equal(result.updatedState.released, true);
89
+ assert.equal(result.updatedState.currentPhase, "ready_to_complete");
90
+ assert.equal(result.updatedState.releaseCommand, "npm run release:patch");
91
+ });
package/workflow-state.js CHANGED
@@ -5,8 +5,30 @@ import { fileURLToPath } from "url";
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
+ function resolveDefaultStateFile() {
9
+ if (process.env.DEV_WORKFLOW_STATE_FILE) {
10
+ return path.resolve(process.env.DEV_WORKFLOW_STATE_FILE);
11
+ }
12
+
13
+ const cwd = process.cwd();
14
+ if (!cwd.includes(`node_modules${path.sep}`)) {
15
+ return path.join(cwd, ".workflow-state.json");
16
+ }
17
+
18
+ if (process.env.INIT_CWD) {
19
+ return path.join(process.env.INIT_CWD, ".workflow-state.json");
20
+ }
21
+
22
+ const [beforeNodeModules] = cwd.split(`node_modules${path.sep}`);
23
+ if (beforeNodeModules) {
24
+ return path.join(beforeNodeModules, ".workflow-state.json");
25
+ }
26
+
27
+ return path.join(__dirname, ".workflow-state.json");
28
+ }
29
+
8
30
  export class WorkflowState {
9
- constructor(stateFilePath = path.join(__dirname, ".workflow-state.json")) {
31
+ constructor(stateFilePath = resolveDefaultStateFile()) {
10
32
  this.stateFile = stateFilePath;
11
33
  this.state = {
12
34
  currentPhase: "idle",