@programinglive/dev-workflow-mcp-server 1.1.16 → 1.1.18

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,15 @@
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.18](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.17...v1.1.18) (2025-11-03)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * auto-detect primary branch for commit_and_push ([a68d38b](https://github.com/programinglive/dev-workflow-mcp-server/commit/a68d38ba547e6007bd2c0cfe5d231ebf417a2010))
11
+
12
+ ### [1.1.17](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.16...v1.1.17) (2025-10-31)
13
+
5
14
  ### [1.1.16](https://github.com/programinglive/dev-workflow-mcp-server/compare/v1.1.15...v1.1.16) (2025-10-31)
6
15
 
7
16
 
package/README.md CHANGED
@@ -378,9 +378,11 @@ Check if all workflow steps are completed and you're ready to commit & push.
378
378
  ### `commit_and_push`
379
379
  Automatically run `git add`, `git commit`, and `git push` after the ready check passes.
380
380
 
381
+ **Auto-detection of primary branch:** If no `branch` is specified, the tool automatically detects your project's primary branch by checking for `origin/main` first, then falling back to `origin/master`. This eliminates the need to specify the branch parameter for most projects.
382
+
381
383
  **Parameters:**
382
384
  - `commitMessage` (string, required): Conventional commit message to use
383
- - `branch` (string, optional): Target branch to push (defaults to current branch)
385
+ - `branch` (string, optional): Target branch to push. If omitted, auto-detects primary branch (main or master)
384
386
 
385
387
  ### `perform_release`
386
388
  Record the release after you've committed and pushed. Required before you can complete the task.
package/git-helpers.js CHANGED
@@ -150,3 +150,28 @@ export async function getCurrentBranch() {
150
150
  return "";
151
151
  }
152
152
  }
153
+
154
+ export async function getPrimaryBranch() {
155
+ try {
156
+ // Check if 'main' exists on origin
157
+ const { stdout: mainCheck } = await exec("git rev-parse --verify origin/main");
158
+ if (mainCheck.trim()) {
159
+ return "main";
160
+ }
161
+ } catch {
162
+ // main doesn't exist, fall through to master check
163
+ }
164
+
165
+ try {
166
+ // Check if 'master' exists on origin
167
+ const { stdout: masterCheck } = await exec("git rev-parse --verify origin/master");
168
+ if (masterCheck.trim()) {
169
+ return "master";
170
+ }
171
+ } catch {
172
+ // master doesn't exist either
173
+ }
174
+
175
+ // Fallback to current branch if neither main nor master exist
176
+ return await getCurrentBranch();
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.1.16",
3
+ "version": "1.1.18",
4
4
  "description": "MCP server to enforce development workflow discipline",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -62,6 +62,7 @@ test("commit_and_push commits changes and pushes", async () => {
62
62
  hasTestChanges: async () => true,
63
63
  getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
64
64
  getCurrentBranch: async () => "main",
65
+ getPrimaryBranch: async () => "main",
65
66
  getLastCommitMessage: async () => "",
66
67
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
67
68
  };
@@ -129,6 +130,7 @@ test("run_full_workflow iterates until commit/release complete with clean tree",
129
130
  hasTestChanges: async () => true,
130
131
  getStagedChanges: async () => [],
131
132
  getCurrentBranch: async () => "main",
133
+ getPrimaryBranch: async () => "main",
132
134
  getLastCommitMessage: async () => "chore: existing",
133
135
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
134
136
  };
@@ -163,6 +165,172 @@ test("run_full_workflow iterates until commit/release complete with clean tree",
163
165
  });
164
166
  });
165
167
 
168
+ test("commit_and_push uses primary branch when no branch specified", async () => {
169
+ await withWorkflowState(async (workflowState) => {
170
+ workflowState.state.readyToCommit = true;
171
+ workflowState.state.readyCheckCompleted = true;
172
+ workflowState.state.testsSkipped = false;
173
+ workflowState.state.testsCreated = true;
174
+ workflowState.state.testsPassed = true;
175
+ workflowState.state.currentPhase = "commit";
176
+ await workflowState.save();
177
+
178
+ const commands = [];
179
+ const execStub = async (command) => {
180
+ commands.push(command);
181
+ return { stdout: "" };
182
+ };
183
+
184
+ const git = {
185
+ hasWorkingChanges: async () => true,
186
+ hasStagedChanges: async () => false,
187
+ hasTestChanges: async () => true,
188
+ getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
189
+ getCurrentBranch: async () => "feature/test",
190
+ getPrimaryBranch: async () => "main",
191
+ getLastCommitMessage: async () => "",
192
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
193
+ };
194
+
195
+ const response = await handleToolCall({
196
+ request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
197
+ normalizeRequestArgs,
198
+ workflowState,
199
+ exec: execStub,
200
+ git,
201
+ utils,
202
+ });
203
+
204
+ const pushCommand = commands.find((command) => command.startsWith("git push"));
205
+ assert.ok(pushCommand, "git push should be executed");
206
+ assert.ok(
207
+ pushCommand.includes("main"),
208
+ "push command should use detected primary branch (main)"
209
+ );
210
+ assert.ok(
211
+ response.content[0].text.includes("Commit and push completed!"),
212
+ "response should confirm commit and push"
213
+ );
214
+ });
215
+ });
216
+
217
+ test("commit_and_push falls back to master when main not found", async () => {
218
+ await withWorkflowState(async (workflowState) => {
219
+ workflowState.state.readyToCommit = true;
220
+ workflowState.state.readyCheckCompleted = true;
221
+ workflowState.state.testsSkipped = false;
222
+ workflowState.state.testsCreated = true;
223
+ workflowState.state.testsPassed = true;
224
+ workflowState.state.currentPhase = "commit";
225
+ await workflowState.save();
226
+
227
+ const commands = [];
228
+ const execStub = async (command) => {
229
+ commands.push(command);
230
+ return { stdout: "" };
231
+ };
232
+
233
+ const git = {
234
+ hasWorkingChanges: async () => true,
235
+ hasStagedChanges: async () => false,
236
+ hasTestChanges: async () => true,
237
+ getStagedChanges: async () => [{ status: "M", path: "src/index.js" }],
238
+ getCurrentBranch: async () => "feature/test",
239
+ getPrimaryBranch: async () => "master",
240
+ getLastCommitMessage: async () => "",
241
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
242
+ };
243
+
244
+ const response = await handleToolCall({
245
+ request: createRequest("commit_and_push", { commitMessage: "feat: test" }),
246
+ normalizeRequestArgs,
247
+ workflowState,
248
+ exec: execStub,
249
+ git,
250
+ utils,
251
+ });
252
+
253
+ const pushCommand = commands.find((command) => command.startsWith("git push"));
254
+ assert.ok(pushCommand, "git push should be executed");
255
+ assert.ok(
256
+ pushCommand.includes("master"),
257
+ "push command should use detected primary branch (master)"
258
+ );
259
+ });
260
+ });
261
+
262
+ test("run_full_workflow uses provided branch for release push when commit is clean", async () => {
263
+ await withWorkflowState(async (workflowState) => {
264
+ workflowState.state.taskDescription = "Ship feature";
265
+ workflowState.state.taskType = "feature";
266
+ workflowState.state.currentPhase = "commit";
267
+ workflowState.state.bugFixed = true;
268
+ workflowState.state.testsCreated = true;
269
+ workflowState.state.testsPassed = true;
270
+ workflowState.state.documentationCreated = true;
271
+ workflowState.state.readyToCommit = true;
272
+ workflowState.state.readyCheckCompleted = true;
273
+ workflowState.state.commitAndPushCompleted = false;
274
+ workflowState.state.released = false;
275
+ workflowState.state.documentationType = "README";
276
+ workflowState.state.documentationSummary = "Docs";
277
+ workflowState.state.fixSummary = "Feature done";
278
+ await workflowState.save();
279
+
280
+ const commands = [];
281
+ const execStub = async (command) => {
282
+ commands.push(command);
283
+ if (command === "git status --porcelain") {
284
+ return { stdout: "" };
285
+ }
286
+ return { stdout: "" };
287
+ };
288
+
289
+ const git = {
290
+ hasWorkingChanges: async () => false,
291
+ hasStagedChanges: async () => false,
292
+ hasTestChanges: async () => true,
293
+ getStagedChanges: async () => [],
294
+ getCurrentBranch: async () => "feature/local",
295
+ getPrimaryBranch: async () => "main",
296
+ getLastCommitMessage: async () => "chore: existing",
297
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
298
+ };
299
+
300
+ const branchName = "master";
301
+ const response = await handleToolCall({
302
+ request: createRequest("run_full_workflow", {
303
+ summary: "Feature done",
304
+ testCommand: "npm test",
305
+ documentationType: "README",
306
+ documentationSummary: "Docs",
307
+ commitMessage: "feat: ship feature",
308
+ branch: branchName,
309
+ releaseCommand: "npm run release:patch",
310
+ }),
311
+ normalizeRequestArgs,
312
+ workflowState,
313
+ exec: execStub,
314
+ git,
315
+ utils,
316
+ });
317
+
318
+ assert.ok(
319
+ response.content[0].text.includes("✅ Full workflow completed successfully"),
320
+ "run_full_workflow should finish successfully"
321
+ );
322
+
323
+ const tagPushCommand = commands.find((command) => command.startsWith("git push --follow-tags"));
324
+ assert.ok(tagPushCommand, "release should push tags");
325
+ assert.ok(
326
+ /origin\s+'?master'?/.test(tagPushCommand),
327
+ `release push should target the provided branch (${branchName})`
328
+ );
329
+
330
+ assert.equal(workflowState.state.currentPhase, "idle");
331
+ });
332
+ });
333
+
166
334
  test("project_summary_data reads persisted file and falls back", async () => {
167
335
  await withWorkflowState(async (workflowState) => {
168
336
  // Seed some history
@@ -174,7 +342,16 @@ test("project_summary_data reads persisted file and falls back", async () => {
174
342
  normalizeRequestArgs,
175
343
  workflowState,
176
344
  exec: async () => ({ stdout: "" }),
177
- git: {},
345
+ git: {
346
+ hasWorkingChanges: async () => false,
347
+ hasStagedChanges: async () => false,
348
+ hasTestChanges: async () => true,
349
+ getStagedChanges: async () => [],
350
+ getCurrentBranch: async () => "main",
351
+ getPrimaryBranch: async () => "main",
352
+ getLastCommitMessage: async () => "",
353
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
354
+ },
178
355
  utils,
179
356
  });
180
357
 
@@ -199,7 +376,16 @@ test("project_summary aggregates task types and recent activity", async () => {
199
376
  normalizeRequestArgs,
200
377
  workflowState,
201
378
  exec: async () => ({ stdout: "" }),
202
- git: {},
379
+ git: {
380
+ hasWorkingChanges: async () => false,
381
+ hasStagedChanges: async () => false,
382
+ hasTestChanges: async () => true,
383
+ getStagedChanges: async () => [],
384
+ getCurrentBranch: async () => "main",
385
+ getPrimaryBranch: async () => "main",
386
+ getLastCommitMessage: async () => "",
387
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
388
+ },
203
389
  utils,
204
390
  });
205
391
 
@@ -239,6 +425,7 @@ test("run_full_workflow executes all steps successfully", async () => {
239
425
  hasTestChanges: async () => true,
240
426
  getStagedChanges: async () => [{ status: "M", path: "src/app.js" }],
241
427
  getCurrentBranch: async () => "main",
428
+ getPrimaryBranch: async () => "main",
242
429
  getLastCommitMessage: async () => "",
243
430
  workingTreeSummary: () =>
244
431
  workingChanges
@@ -315,6 +502,7 @@ test("run_full_workflow resumes from current phase when steps are already comple
315
502
  hasTestChanges: async () => true,
316
503
  getStagedChanges: async () => [],
317
504
  getCurrentBranch: async () => "main",
505
+ getPrimaryBranch: async () => "main",
318
506
  getLastCommitMessage: async () => "",
319
507
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
320
508
  };
@@ -373,7 +561,16 @@ test("run_full_workflow validates required arguments", async () => {
373
561
  normalizeRequestArgs,
374
562
  workflowState,
375
563
  exec: async () => ({ stdout: "" }),
376
- git: {},
564
+ git: {
565
+ hasWorkingChanges: async () => false,
566
+ hasStagedChanges: async () => false,
567
+ hasTestChanges: async () => true,
568
+ getStagedChanges: async () => [],
569
+ getCurrentBranch: async () => "main",
570
+ getPrimaryBranch: async () => "main",
571
+ getLastCommitMessage: async () => "",
572
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
573
+ },
377
574
  utils,
378
575
  });
379
576
 
@@ -400,7 +597,16 @@ test("force_complete_task records entry and resets state", async () => {
400
597
  normalizeRequestArgs,
401
598
  workflowState,
402
599
  exec: async () => ({ stdout: "" }),
403
- git: {},
600
+ git: {
601
+ hasWorkingChanges: async () => false,
602
+ hasStagedChanges: async () => false,
603
+ hasTestChanges: async () => true,
604
+ getStagedChanges: async () => [],
605
+ getCurrentBranch: async () => "main",
606
+ getPrimaryBranch: async () => "main",
607
+ getLastCommitMessage: async () => "",
608
+ workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
609
+ },
404
610
  utils,
405
611
  });
406
612
 
@@ -436,7 +642,8 @@ test("perform_release blocks when new changes detected", async () => {
436
642
  hasTestChanges: async () => true,
437
643
  getStagedChanges: async () => [],
438
644
  getCurrentBranch: async () => "main",
439
- getLastCommitMessage: async () => "fix: adjust layout",
645
+ getPrimaryBranch: async () => "main",
646
+ getLastCommitMessage: async () => "",
440
647
  workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
441
648
  };
442
649
 
@@ -466,6 +673,7 @@ test("continue_workflow warns when workflow is idle", async () => {
466
673
  hasTestChanges: async () => false,
467
674
  getStagedChanges: async () => [],
468
675
  getCurrentBranch: async () => "main",
676
+ getPrimaryBranch: async () => "main",
469
677
  getLastCommitMessage: async () => "",
470
678
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
471
679
  };
@@ -499,8 +707,9 @@ test("continue_workflow resets to commit when new changes detected", async () =>
499
707
  hasTestChanges: async () => true,
500
708
  getStagedChanges: async () => [],
501
709
  getCurrentBranch: async () => "main",
502
- getLastCommitMessage: async () => "fix: adjust layout",
503
- workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
710
+ getPrimaryBranch: async () => "main",
711
+ getLastCommitMessage: async () => "",
712
+ workingTreeSummary: () => ({ hasChanges: true, lines: ["?? new-file.js"] }),
504
713
  };
505
714
 
506
715
  const response = await handleToolCall({
@@ -543,6 +752,7 @@ test("commit_and_push recognizes already committed work", async () => {
543
752
  hasTestChanges: async () => true,
544
753
  getStagedChanges: async () => [],
545
754
  getCurrentBranch: async () => "main",
755
+ getPrimaryBranch: async () => "main",
546
756
  getLastCommitMessage: async () => "fix: previous work",
547
757
  workingTreeSummary: () => ({ hasChanges: false, lines: [] }),
548
758
  };
package/tools/handlers.js CHANGED
@@ -426,8 +426,9 @@ async function handleCommitAndPush(args, context) {
426
426
  typeof args.commitMessage === "string" ? args.commitMessage.trim() : "";
427
427
 
428
428
  const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
429
+ const primaryBranch = await git.getPrimaryBranch();
429
430
  const currentBranch = await git.getCurrentBranch();
430
- const branchForPush = requestedBranch || currentBranch || "";
431
+ const branchForPush = requestedBranch || primaryBranch || currentBranch || "";
431
432
 
432
433
  const hasWorkingChanges = await git.hasWorkingChanges();
433
434
 
@@ -817,6 +818,12 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
817
818
 
818
819
  await workflowState.ensurePrimaryFile();
819
820
 
821
+ const requestedBranch = typeof args.branch === "string" ? args.branch.trim() : "";
822
+ if (requestedBranch) {
823
+ workflowState.state.lastPushBranch = requestedBranch;
824
+ await workflowState.save();
825
+ }
826
+
820
827
  const requiredStrings = [
821
828
  { key: "summary", message: "Provide a non-empty 'summary' for mark_bug_fixed." },
822
829
  { key: "testCommand", message: "Provide a non-empty 'testCommand' for run_tests." },
@@ -906,7 +913,7 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
906
913
  const resp = await executeStep("commit_and_push", handleCommitAndPush, [
907
914
  {
908
915
  commitMessage: args.commitMessage,
909
- branch: typeof args.branch === "string" ? args.branch : "",
916
+ branch: requestedBranch,
910
917
  },
911
918
  { workflowState, exec, git, utils },
912
919
  ]);