@programinglive/dev-workflow-mcp-server 1.13.10 → 1.13.11

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/git-facade.js CHANGED
@@ -10,8 +10,10 @@
10
10
 
11
11
  import {
12
12
  getCurrentBranch,
13
+ getHeadSha,
13
14
  getLastCommitMessage,
14
15
  getPrimaryBranch,
16
+ getRemoteBranchSha,
15
17
  getStagedChanges,
16
18
  getStatusOutput,
17
19
  hasStagedChanges,
@@ -27,7 +29,9 @@ export function createGitFacade() {
27
29
  hasStagedChanges,
28
30
  getStagedChanges,
29
31
  getCurrentBranch,
32
+ getHeadSha,
30
33
  getPrimaryBranch,
34
+ getRemoteBranchSha,
31
35
  getLastCommitMessage,
32
36
  getStatusOutput,
33
37
  workingTreeSummary,
package/git-helpers.js CHANGED
@@ -145,6 +145,29 @@ export async function getCurrentBranch() {
145
145
  return stdout.trim();
146
146
  }
147
147
 
148
+ /**
149
+ * SHA commit yang sedang di HEAD.
150
+ *
151
+ * Dipakai untuk membuktikan bahwa push benar-benar mengirim commit ini, bukan
152
+ * hanya "git push" yang keluar 0 tanpa memindahkan apa pun.
153
+ */
154
+ export async function getHeadSha() {
155
+ const { stdout } = await exec("git rev-parse HEAD");
156
+ return stdout.trim();
157
+ }
158
+
159
+ /**
160
+ * SHA yang tercatat pada remote-tracking ref `origin/<branch>`.
161
+ * Melempar bila branch itu belum ada di remote.
162
+ */
163
+ export async function getRemoteBranchSha(branch) {
164
+ const { stdout } = await exec("git rev-parse", {
165
+ executable: "git",
166
+ arguments: ["rev-parse", `origin/${branch}`],
167
+ });
168
+ return stdout.trim();
169
+ }
170
+
148
171
  export async function getStatusOutput() {
149
172
  const { stdout } = await exec("git status --porcelain");
150
173
  return stdout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.13.10",
3
+ "version": "1.13.11",
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",
@@ -0,0 +1,111 @@
1
+ // Verifikasi end-to-end commit_and_push: harus mem-push branch AKTIF.
2
+ // Memanggil handler asli (bukan mock) terhadap repo sementara + remote bare.
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import path from "node:path";
7
+
8
+ import { handleToolCall } from "../tools/handlers.js";
9
+ import { WorkflowState } from "../workflow-state.js";
10
+ import { shellEscape, normalizeRequestArgs } from "../utils.js";
11
+ import { createCommitMessageParts, determineReleaseTypeFromCommit } from "../commit-helpers.js";
12
+ import { createGitFacade } from "../git-facade.js";
13
+ import { exec } from "../exec.js";
14
+
15
+ const git = (cwd, ...args) =>
16
+ execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17
+
18
+ const root = mkdtempSync(path.join(tmpdir(), "push-verify-"));
19
+ const remote = path.join(root, "remote.git");
20
+ const work = path.join(root, "work");
21
+
22
+ try {
23
+ git(root, "init", "--bare", "-q", remote);
24
+ git(root, "clone", "-q", remote, work);
25
+ git(work, "config", "user.email", "test@example.test");
26
+ git(work, "config", "user.name", "Test");
27
+
28
+ writeFileSync(path.join(work, "base.txt"), "awal\n");
29
+ git(work, "add", ".");
30
+ git(work, "commit", "-q", "-m", "chore: initial");
31
+ git(work, "branch", "-M", "main");
32
+ git(work, "push", "-q", "origin", "main");
33
+
34
+ // Branch fitur dengan commit yang BELUM di-push.
35
+ git(work, "checkout", "-q", "-b", "feat/verifikasi");
36
+ writeFileSync(path.join(work, "fitur.txt"), "fitur\n");
37
+ git(work, "add", ".");
38
+ git(work, "commit", "-q", "-m", "feat: pekerjaan di branch fitur");
39
+
40
+ const headBefore = git(work, "rev-parse", "HEAD");
41
+ const mainBefore = git(work, "rev-parse", "main");
42
+
43
+ // Perubahan yang BELUM di-commit, supaya tool menempuh jalur commit + push
44
+ // (bukan jalur "no changes detected").
45
+ writeFileSync(path.join(work, "belum-commit.txt"), "perubahan\n");
46
+
47
+ console.log("branch aktif :", git(work, "branch", "--show-current"));
48
+ console.log("HEAD awal :", headBefore.slice(0, 8));
49
+ let remoteFeatBefore = "(belum ada)";
50
+ try {
51
+ remoteFeatBefore = git(work, "rev-parse", "--verify", "-q", "origin/feat/verifikasi").slice(0, 8);
52
+ } catch { /* belum ada */ }
53
+ console.log("origin/feat :", remoteFeatBefore);
54
+
55
+ const stateFile = path.join(root, "state.json");
56
+ const workflowState = new WorkflowState(stateFile);
57
+ await workflowState.load();
58
+ Object.assign(workflowState.state, {
59
+ currentPhase: "commit",
60
+ taskDescription: "verifikasi push branch",
61
+ taskType: "other",
62
+ featureFlowCreated: true,
63
+ bugFixed: true,
64
+ testsCreated: true,
65
+ testsPassed: true,
66
+ testsSkipped: true,
67
+ documentationCreated: true,
68
+ readyToCommit: true,
69
+ readyCheckCompleted: true,
70
+ commitAndPushCompleted: false,
71
+ projectDir: work,
72
+ history: [],
73
+ });
74
+ await workflowState.save();
75
+
76
+ const response = await handleToolCall({
77
+ request: {
78
+ params: {
79
+ name: "commit_and_push",
80
+ arguments: { commitMessage: "feat: uji push", projectDir: work },
81
+ },
82
+ },
83
+ normalizeRequestArgs,
84
+ workflowState,
85
+ exec,
86
+ git: createGitFacade(),
87
+ utils: { shellEscape, determineReleaseTypeFromCommit, createCommitMessageParts },
88
+ });
89
+
90
+ console.log("\n=== output tool ===");
91
+ console.log(response.content[0].text);
92
+
93
+ console.log("\n=== hasil di remote ===");
94
+ let remoteFeat = "";
95
+ try {
96
+ remoteFeat = git(work, "rev-parse", "--verify", "-q", "origin/feat/verifikasi");
97
+ } catch { /* tidak ada */ }
98
+ const remoteMain = git(work, "rev-parse", "main");
99
+ console.log("origin/feat/verifikasi :", remoteFeat ? remoteFeat.slice(0, 8) : "(tidak ada)");
100
+ console.log("origin/main :", remoteMain.slice(0, 8));
101
+
102
+ const headAfter = git(work, "rev-parse", "HEAD");
103
+ const okFeat = remoteFeat === headAfter;
104
+ const okMain = remoteMain === mainBefore;
105
+ console.log("");
106
+ console.log(okFeat ? "BENAR: remote menerima commit di branch fitur" : "SALAH: remote/feat tidak menerima HEAD");
107
+ console.log(okMain ? "BENAR: main tidak tersentuh" : "SALAH: main ikut berubah");
108
+ process.exitCode = okFeat && okMain ? 0 : 1;
109
+ } finally {
110
+ rmSync(root, { recursive: true, force: true });
111
+ }
@@ -43,6 +43,62 @@ function applyProjectDirOverride(args) {
43
43
  }
44
44
  }
45
45
 
46
+ /**
47
+ * Branch yang benar-benar dipakai untuk push.
48
+ *
49
+ * Urutan: `branch` eksplisit -> branch aktif -> primary branch.
50
+ *
51
+ * Sebelum 31 Okt 2026 urutannya primaryBranch lebih dulu, dan `getPrimaryBranch()`
52
+ * selalu menjawab "main" bila `origin/main` ada - tanpa melihat branch aktif.
53
+ * Akibatnya di worktree yang sedang di `feat/...`, tool menjalankan
54
+ * `git push origin main`. Karena `main` lokal sudah sama dengan `origin/main`,
55
+ * perintah itu keluar 0 tanpa mengirim apa pun, lalu tool melaporkan
56
+ * "Commit and push completed". Dua commit nyata tidak pernah ter-push.
57
+ *
58
+ * HEAD yang detached ("HEAD") bukan branch, jadi tidak bisa dipakai.
59
+ */
60
+ function pickPushBranch({ requestedBranch, currentBranch, primaryBranch }) {
61
+ if (requestedBranch) return requestedBranch;
62
+ if (currentBranch && currentBranch !== "HEAD") return currentBranch;
63
+ return primaryBranch || currentBranch || "";
64
+ }
65
+
66
+ /**
67
+ * Pastikan remote benar-benar menerima HEAD.
68
+ *
69
+ * `git push` bisa keluar 0 tanpa memindahkan apa pun - itulah yang menyamarkan
70
+ * bug di atas. Satu-satunya bukti yang bisa dipercaya adalah membandingkan SHA.
71
+ */
72
+ async function verifyPushed(git, branch) {
73
+ let head;
74
+ try {
75
+ head = await git.getHeadSha();
76
+ } catch (error) {
77
+ return { ok: false, detail: `tidak bisa membaca HEAD: ${error.message}` };
78
+ }
79
+
80
+ let remote;
81
+ try {
82
+ remote = await git.getRemoteBranchSha(branch);
83
+ } catch {
84
+ return {
85
+ ok: false,
86
+ detail: `branch origin/${branch} tidak ditemukan di remote setelah push`,
87
+ };
88
+ }
89
+
90
+ if (remote !== head) {
91
+ return {
92
+ ok: false,
93
+ detail:
94
+ `origin/${branch} berada di ${remote.slice(0, 8)}, ` +
95
+ `sedangkan HEAD di ${head.slice(0, 8)}`,
96
+ };
97
+ }
98
+
99
+ return { ok: true, head };
100
+ }
101
+
46
102
  export async function handleCommitAndPush(args, context) {
47
103
  const { workflowState, exec, git, utils } = context;
48
104
 
@@ -84,12 +140,25 @@ export async function handleCommitAndPush(args, context) {
84
140
  );
85
141
  }
86
142
 
87
- const branchForPush = requestedBranch || primaryBranch || currentBranch || "";
143
+ const branchForPush = pickPushBranch({ requestedBranch, currentBranch, primaryBranch });
88
144
 
89
145
  if (!hasWorkingChanges) {
90
146
  const lastCommitMessage = await git.getLastCommitMessage();
91
147
  const effectiveCommitMessage = lastCommitMessage || providedCommitMessage;
92
148
 
149
+ // "Tidak ada perubahan" bukan berarti "sudah ter-push". HEAD bisa saja
150
+ // berupa commit yang belum pernah dikirim. Memeriksanya mencegah tool
151
+ // menandai selesai padahal remote masih tertinggal.
152
+ const pushed = await verifyPushed(git, branchForPush);
153
+ if (!pushed.ok) {
154
+ return textResponse(
155
+ `⚠️ Tidak ada perubahan untuk di-commit, tetapi HEAD BELUM ada di remote.\n` +
156
+ `Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
157
+ `Sebab: ${pushed.detail}\n\n` +
158
+ `Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
159
+ );
160
+ }
161
+
93
162
  workflowState.state.commitAndPushCompleted = true;
94
163
  workflowState.state.lastCommitMessage = effectiveCommitMessage || "";
95
164
  workflowState.state.lastPushBranch = branchForPush;
@@ -97,7 +166,7 @@ export async function handleCommitAndPush(args, context) {
97
166
  await workflowState.save();
98
167
 
99
168
  return textResponse(
100
- `ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${repoLabel()}\nLast commit: ${effectiveCommitMessage || "(unavailable)"
169
+ `ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${repoLabel()}\nBranch: ${branchForPush} (verified on remote)\nLast commit: ${effectiveCommitMessage || "(unavailable)"
101
170
  }\n\nNext: Run 'perform_release' to handle the release and push tags.`
102
171
  );
103
172
  }
@@ -152,6 +221,20 @@ export async function handleCommitAndPush(args, context) {
152
221
  );
153
222
  }
154
223
 
224
+ // Exit code 0 dari `git push` BUKAN bukti bahwa commit ini terkirim. Kalau
225
+ // branch yang dipilih sudah sinkron, git tidak melakukan apa pun dan tetap
226
+ // keluar 0. Satu-satunya bukti yang bisa dipercaya: SHA HEAD ada di remote.
227
+ const pushed = await verifyPushed(git, branchForPush);
228
+ if (!pushed.ok) {
229
+ return textResponse(
230
+ `❌ Commit dibuat, tetapi TIDAK ter-push.\n` +
231
+ `Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
232
+ `Sebab: ${pushed.detail}\n\n` +
233
+ `Commit lokal: ${generatedSummary}\n\n` +
234
+ `Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
235
+ );
236
+ }
237
+
155
238
  workflowState.state.commitAndPushCompleted = true;
156
239
  workflowState.state.lastCommitMessage = generatedSummary;
157
240
  workflowState.state.lastPushBranch = branchForPush;
@@ -159,8 +242,7 @@ export async function handleCommitAndPush(args, context) {
159
242
  await workflowState.save();
160
243
 
161
244
  return textResponse(
162
- `✅ Commit and push completed!\nRepo: ${repoLabel()}\n\nCommit message: ${generatedSummary}\nPushed to: ${branchForPush || "(default upstream)"
163
- }\n\nNext: Run 'perform_release' to handle the release and push tags.`
245
+ `✅ Commit and push completed!\nRepo: ${repoLabel()}\n\nCommit message: ${generatedSummary}\nPushed to: ${branchForPush} (verified at ${pushed.head.slice(0, 8)})\n\nNext: Run 'perform_release' to handle the release and push tags.`
164
246
  );
165
247
  }
166
248