@programinglive/dev-workflow-mcp-server 1.13.13 → 1.13.14

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
@@ -16,6 +16,7 @@ import {
16
16
  getRemoteBranchSha,
17
17
  getStagedChanges,
18
18
  getStatusOutput,
19
+ getToplevel,
19
20
  hasStagedChanges,
20
21
  hasTestChanges,
21
22
  hasWorkingChanges,
@@ -34,6 +35,7 @@ export function createGitFacade() {
34
35
  getRemoteBranchSha,
35
36
  getLastCommitMessage,
36
37
  getStatusOutput,
38
+ getToplevel,
37
39
  workingTreeSummary,
38
40
  };
39
41
  }
package/git-helpers.js CHANGED
@@ -173,6 +173,18 @@ export async function getStatusOutput() {
173
173
  return stdout;
174
174
  }
175
175
 
176
+ /**
177
+ * Direktori teratas repo tempat git BENAR-BENAR berjalan.
178
+ *
179
+ * `git rev-parse --show-toplevel` menjawab dari repo yang aktif, bukan dari
180
+ * tebakan kita tentang direktori mana yang dimaksud. Dipakai untuk memastikan
181
+ * laporan `commit_and_push` menyebut repo yang sama dengan yang di-commit.
182
+ */
183
+ export async function getToplevel() {
184
+ const { stdout } = await exec("git rev-parse --show-toplevel");
185
+ return stdout.trim();
186
+ }
187
+
176
188
  export async function getPrimaryBranch() {
177
189
  try {
178
190
  // Check if 'main' exists on origin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.13.13",
3
+ "version": "1.13.14",
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",
@@ -21,7 +21,7 @@
21
21
  "start": "node index.js",
22
22
  "local": "node index.js",
23
23
  "setup": "node scripts/setup-stack.js",
24
- "test": "node --no-deprecation --test",
24
+ "test": "node --no-deprecation --import ./tests/setup-env.js --test",
25
25
  "test:security": "node --no-deprecation --test tests/security*.test.js",
26
26
  "security:audit": "node index.js call security_audit",
27
27
  "lint": "node scripts/lint.mjs",
@@ -2,6 +2,7 @@ import { textResponse, isReleaseSatisfied, formatChecklist } from "./shared.js";
2
2
  import { handleReadyCheck } from "./status-handlers.js";
3
3
  import { containsTestFilesInStatus } from "../../git-helpers.js";
4
4
  import { resolveProjectDir, setActiveProjectDir, getActiveProjectDir } from "../../exec.js";
5
+ import path from "path";
5
6
 
6
7
  /**
7
8
  * Label repo yang benar-benar dipakai, untuk ditampilkan di setiap jawaban.
@@ -24,6 +25,18 @@ function repoLabel() {
24
25
  }
25
26
  }
26
27
 
28
+ /**
29
+ * Apakah dua path menunjuk direktori yang sama.
30
+ *
31
+ * Windows tidak membedakan huruf besar/kecil dan kadang memberi garis miring
32
+ * di akhir, jadi perbandingan string mentah akan salah.
33
+ */
34
+ function samePath(a, b) {
35
+ if (!a || !b) return false;
36
+ const normal = (value) => path.resolve(value).replace(/[\\/]+$/, "").toLowerCase();
37
+ return normal(a) === normal(b);
38
+ }
39
+
27
40
  /**
28
41
  * Terapkan `projectDir` dari argumen tool bila ada.
29
42
  * Mengembalikan pesan kesalahan, atau null bila tidak ada masalah.
@@ -127,10 +140,21 @@ export async function handleCommitAndPush(args, context) {
127
140
  let primaryBranch;
128
141
  let currentBranch;
129
142
  let hasWorkingChanges;
143
+ let gitToplevel = "";
130
144
  try {
131
145
  primaryBranch = await git.getPrimaryBranch();
132
146
  currentBranch = await git.getCurrentBranch();
133
147
  hasWorkingChanges = await git.hasWorkingChanges();
148
+
149
+ // Tempat git BENAR-BENAR berjalan, menurut git sendiri.
150
+ //
151
+ // 31 Oktober 2026 laporan menyebut `portal-beautyworld` untuk commit yang
152
+ // masuk ke repo lain, karena labelnya dibaca dari variabel global yang bisa
153
+ // berubah - bukan dari repo tempat git berjalan. Laporan seperti itu membuat
154
+ // agen percaya commit-nya ada di tempat yang salah.
155
+ if (typeof git.getToplevel === "function") {
156
+ gitToplevel = String((await git.getToplevel()) || "").trim();
157
+ }
134
158
  } catch (error) {
135
159
  return textResponse(
136
160
  `❌ Tidak bisa memeriksa repository Git, jadi tidak ada yang di-commit.\n\n` +
@@ -140,6 +164,22 @@ export async function handleCommitAndPush(args, context) {
140
164
  );
141
165
  }
142
166
 
167
+ // Kalau git berjalan di repo lain daripada yang diminta, BERHENTI. Commit ke
168
+ // repo yang salah jauh lebih mahal daripada satu pesan kesalahan.
169
+ const requestedRepo = getActiveProjectDir();
170
+ if (gitToplevel && requestedRepo && !samePath(gitToplevel, requestedRepo)) {
171
+ return textResponse(
172
+ `❌ Dibatalkan: git berjalan di repo yang BERBEDA dari yang diminta.\n\n` +
173
+ `Diminta : ${requestedRepo}\n` +
174
+ `Dipakai git: ${gitToplevel}\n\n` +
175
+ `Tidak ada yang di-commit. Sebutkan 'projectDir' yang benar pada 'commit_and_push', ` +
176
+ `atau jalankan ulang 'start_task' dengan 'projectDir' repo tersebut.`
177
+ );
178
+ }
179
+
180
+ // Label laporan memakai repo yang benar-benar dipakai git bila diketahui.
181
+ const repoName = gitToplevel || repoLabel();
182
+
143
183
  const branchForPush = pickPushBranch({ requestedBranch, currentBranch, primaryBranch });
144
184
 
145
185
  if (!hasWorkingChanges) {
@@ -153,7 +193,7 @@ export async function handleCommitAndPush(args, context) {
153
193
  if (!pushed.ok) {
154
194
  return textResponse(
155
195
  `⚠️ Tidak ada perubahan untuk di-commit, tetapi HEAD BELUM ada di remote.\n` +
156
- `Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
196
+ `Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
157
197
  `Sebab: ${pushed.detail}\n\n` +
158
198
  `Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
159
199
  );
@@ -166,7 +206,7 @@ export async function handleCommitAndPush(args, context) {
166
206
  await workflowState.save();
167
207
 
168
208
  return textResponse(
169
- `ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${repoLabel()}\nBranch: ${branchForPush} (verified on remote)\nLast commit: ${effectiveCommitMessage || "(unavailable)"
209
+ `ℹ️ No changes detected. Assuming commit and push already completed.\nRepo: ${repoName}\nBranch: ${branchForPush} (verified on remote)\nLast commit: ${effectiveCommitMessage || "(unavailable)"
170
210
  }\n\nNext: Run 'perform_release' to handle the release and push tags.`
171
211
  );
172
212
  }
@@ -228,7 +268,7 @@ export async function handleCommitAndPush(args, context) {
228
268
  if (!pushed.ok) {
229
269
  return textResponse(
230
270
  `❌ Commit dibuat, tetapi TIDAK ter-push.\n` +
231
- `Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
271
+ `Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
232
272
  `Sebab: ${pushed.detail}\n\n` +
233
273
  `Commit lokal: ${generatedSummary}\n\n` +
234
274
  `Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
@@ -242,7 +282,7 @@ export async function handleCommitAndPush(args, context) {
242
282
  await workflowState.save();
243
283
 
244
284
  return textResponse(
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.`
285
+ `✅ Commit and push completed!\nRepo: ${repoName}\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.`
246
286
  );
247
287
  }
248
288
 
@@ -1,6 +1,42 @@
1
1
  import { textResponse, isReleaseSatisfied, resetToCommitIfWorkingChanges, summarizeHistory } from "./shared.js";
2
2
  import { resolveProjectDir, setActiveProjectDir, getActiveProjectDir } from "../../exec.js";
3
3
 
4
+ /**
5
+ * Label repo yang dipakai, untuk ditampilkan di laporan.
6
+ *
7
+ * Sama seperti di commit-handlers: laporan harus menyebut repo yang benar-benar
8
+ * dipakai, bukan tebakan.
9
+ */
10
+ function repoLabel() {
11
+ try {
12
+ const resolved = resolveProjectDir({
13
+ explicit: getActiveProjectDir(),
14
+ env: process.env,
15
+ candidates: [process.cwd(), process.env.INIT_CWD],
16
+ });
17
+ const origin = resolved.source === "env" ? " — dari WORKFLOW_PROJECT_DIR (global)" : "";
18
+ return resolved.dir + origin;
19
+ } catch {
20
+ return "(tidak bisa ditentukan)";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Menjalankan perintah git yang hasilnya hanya dipakai untuk laporan.
26
+ *
27
+ * Kegagalan di sini tidak boleh menggagalkan rilis: kalau `git describe`
28
+ * tidak menemukan tag, itu bukan alasan untuk berhenti. Yang penting adalah
29
+ * rilis tetap berjalan dan laporannya jujur tentang apa yang tidak diketahui.
30
+ */
31
+ async function safeExecStdout(exec, command) {
32
+ try {
33
+ const { stdout } = await exec(command);
34
+ return String(stdout || "").trim();
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
39
+
4
40
  export async function handleSkipRelease(args, workflowState) {
5
41
  if (workflowState.state.currentPhase === "idle") {
6
42
  return textResponse("⚠️ Please start a task first using 'start_task' before skipping release.");
@@ -185,11 +221,45 @@ export async function handlePerformRelease(args, context) {
185
221
  );
186
222
  }
187
223
 
224
+ // Tipe rilis WAJIB sah sebelum perintah disusun.
225
+ //
226
+ // 31 Oktober 2026: `determineReleaseTypeFromCommit` mengembalikan string kosong,
227
+ // lalu perintahnya menjadi `npm run release -- --release-as ` + "null" ->
228
+ // standard-version menulis `"version": null` dan membuat tag `vnull`, lalu
229
+ // keduanya di-push. Yang salah bukan hanya hasilnya: perintah yang mustahil
230
+ // itu tetap dijalankan tanpa perlawanan.
231
+ if (!recognizedTypes.has(releaseType)) {
232
+ return textResponse(
233
+ `⚠️ Tipe rilis tidak bisa ditentukan dari commit terakhir, jadi rilis TIDAK dijalankan.\n\n` +
234
+ `Commit terakhir: ${lastCommitMessage || "(kosong)"}\n\n` +
235
+ `Sebutkan tipe rilisnya secara eksplisit:\n` +
236
+ ` • 'perform_release' dengan releaseType: "patch" | "minor" | "major"\n` +
237
+ ` • atau command: "npm run release:patch"\n\n` +
238
+ `Tanpa itu perintahnya akan menjadi "--release-as null", yang menghasilkan tag vnull ` +
239
+ `dan package.json berversi null.`
240
+ );
241
+ }
242
+
188
243
  const finalCommand =
189
244
  releaseCommand.includes("--release-as") || /release:(major|minor|patch)/i.test(releaseCommand)
190
245
  ? releaseCommand
191
246
  : `${releaseCommand} -- --release-as ${releaseType}`;
192
247
 
248
+ // Jaring pengaman terakhir: apa pun yang terjadi di atas, jangan pernah
249
+ // menjalankan perintah yang membawa tipe rilis kosong.
250
+ if (/--release-as\s+(?:null|undefined|)\s*$/i.test(finalCommand) || /\snull\b/i.test(finalCommand)) {
251
+ return textResponse(
252
+ `⚠️ Perintah rilis akan berisi tipe rilis yang tidak sah, jadi TIDAK dijalankan:\n\n` +
253
+ ` ${finalCommand}\n\n` +
254
+ `Sebutkan tipe rilisnya: releaseType "patch" | "minor" | "major".`
255
+ );
256
+ }
257
+
258
+ // Catat keadaan SEBELUM rilis supaya, kalau push gagal, kita bisa menyebut
259
+ // persis apa yang tertinggal di mesin.
260
+ const headBefore = await safeExecStdout(exec, "git rev-parse HEAD");
261
+ const tagBefore = await safeExecStdout(exec, "git describe --tags --abbrev=0");
262
+
193
263
  try {
194
264
  await exec(finalCommand);
195
265
  } catch (error) {
@@ -198,6 +268,9 @@ export async function handlePerformRelease(args, context) {
198
268
  );
199
269
  }
200
270
 
271
+ const headAfter = await safeExecStdout(exec, "git rev-parse HEAD");
272
+ const tagAfter = await safeExecStdout(exec, "git describe --tags --abbrev=0");
273
+
201
274
  const branchForPush = workflowState.state.lastPushBranch || (await git.getCurrentBranch()) || "";
202
275
  const pushCommand = branchForPush
203
276
  ? `git push --follow-tags origin ${utils.shellEscape(branchForPush)}`
@@ -206,8 +279,29 @@ export async function handlePerformRelease(args, context) {
206
279
  try {
207
280
  await exec(pushCommand);
208
281
  } catch (error) {
282
+ // Rilis SUDAH membuat commit dan tag di mesin; hanya push yang gagal.
283
+ // Tanpa menyebut yang tertinggal, tidak jelas apa yang harus dibereskan -
284
+ // dan pada 31 Oktober 2026 hal itu membuat rilis tampak selesai.
285
+ const leftBehind = [];
286
+ if (headAfter && headAfter !== headBefore) {
287
+ leftBehind.push(` • commit rilis ${headAfter.slice(0, 8)}`);
288
+ }
289
+ if (tagAfter && tagAfter !== tagBefore) {
290
+ leftBehind.push(` • tag ${tagAfter}`);
291
+ }
292
+
209
293
  return textResponse(
210
- `❌ git push (with tags) failed:\n\n${error.stderr || error.stdout || error.message}`
294
+ `❌ Push rilis GAGAL. Rilis TIDAK tercatat sebagai selesai.\n\n` +
295
+ `Repo: ${repoLabel()}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
296
+ `Penyebab: ${error.stderr || error.stdout || error.message}\n\n` +
297
+ (leftBehind.length > 0
298
+ ? `Tertinggal di mesin (belum sampai ke remote):\n${leftBehind.join("\n")}\n\n`
299
+ : "") +
300
+ `Pemulihan:\n` +
301
+ ` 1. periksa: git log --oneline -3 && git tag --points-at HEAD\n` +
302
+ ` 2. kirim ulang: git push --follow-tags origin ${branchForPush || "<branch>"}\n` +
303
+ ` 3. atau batalkan rilis lokal: git reset --hard HEAD~1 && git tag -d ${tagAfter || "<tag>"}\n\n` +
304
+ `Jangan tandai task selesai sebelum tag benar-benar ada di remote.`
211
305
  );
212
306
  }
213
307
 
package/workflow-state.js CHANGED
@@ -400,6 +400,19 @@ async function pathExists(filePath) {
400
400
  }
401
401
 
402
402
  async function ensureCompatibilityLinks(primaryPath) {
403
+ // Cermin kompatibilitas ditulis ke `<paket>/.state/workflow-state.json`,
404
+ // BUKAN ke lokasi state yang sebenarnya. Itu benar untuk pemakaian nyata
405
+ // (CLI dan dasbor web membaca jalur lama), tetapi merusak saat tes: setiap
406
+ // WorkflowState yang dibuat tes - meski di direktori sementara - menimpa
407
+ // state repo, dan 31 Oktober 2026 itu menghapus task yang sedang berjalan di
408
+ // tengah `npm test`.
409
+ //
410
+ // Tes mematikan cermin ini lewat DEV_WORKFLOW_NO_COMPAT_LINKS=1 (lihat
411
+ // tests/setup-env.js). Perilaku produksi tidak berubah.
412
+ if (process.env.DEV_WORKFLOW_NO_COMPAT_LINKS === "1") {
413
+ return;
414
+ }
415
+
403
416
  const resolvedPrimary = path.resolve(primaryPath);
404
417
 
405
418
  if (!(await pathExists(resolvedPrimary))) {