@programinglive/dev-workflow-mcp-server 1.13.15 → 1.13.16

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.
Files changed (3) hide show
  1. package/index.js +20 -15
  2. package/package.json +1 -1
  3. package/process-lock.js +63 -6
package/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
15
  import { fileURLToPath } from "url";
16
16
  import path from "path";
17
- import { acquireProcessLock, releaseProcessLock, lockWarning } from "./process-lock.js";
17
+ import { acquireProcessLock, releaseProcessLock, lockWarning, projectLockDir } from "./process-lock.js";
18
18
  import { exec, setActiveProjectDir } from "./exec.js";
19
19
  import { normalizeRequestArgs, shellEscape } from "./utils.js";
20
20
  import { createGitFacade } from "./git-facade.js";
@@ -199,26 +199,31 @@ export async function main() {
199
199
  // Select request metadata before the first disk write. Creating an idle
200
200
  // PID fallback here leaves a phantom session alongside the real sessions.
201
201
  safeLog('error', `[${new Date().toISOString()}] Workflow state initialization complete.`);
202
-
203
- // Penanda proses: 31 Oktober 2026 dua proses MCP hidup bersamaan dan
204
- // memegang kredensial Factory yang berbeda, sehingga tool yang sama
205
- // menjawab berbeda tergantung proses mana yang melayaninya. Proses lain
206
- // TIDAK dibunuh (itu berbahaya); cukup dilaporkan supaya pengguna tahu
207
- // harus menutup OpenCode dan membukanya lagi.
208
- try {
209
- const stateDir = path.dirname(path.resolve(workflowState.stateFile));
210
- const warning = lockWarning(acquireProcessLock({ dir: stateDir }));
211
- if (warning) safeLog('error', warning);
212
- process.once('exit', () => releaseProcessLock({ dir: stateDir }));
213
- } catch (lockError) {
214
- safeLog('error', 'Warning: could not record process lock:', lockError.message);
215
- }
216
202
  } catch (error) {
217
203
  safeLog('error', `[${new Date().toISOString()}] Warning: Failed to initialize workflow state:`, error.message);
218
204
  // Continue anyway - workflow state is optional for MCP to function
219
205
  workflowState = new WorkflowState();
220
206
  }
221
207
 
208
+ // Penanda proses - DI LUAR try di atas, dengan sengaja.
209
+ //
210
+ // 31 Oktober 2026: blok ini semula berada di dalam try yang sama dengan
211
+ // inisialisasi state. Saat dua proses MCP start serempak, satu di antaranya
212
+ // gagal menginisialisasi state (berebut berkas yang sama), masuk ke catch, dan
213
+ // penanda prosesnya tidak pernah ditulis. Hasilnya dua proses hidup dengan
214
+ // satu lock file: proses ganda tidak pernah terdeteksi.
215
+ //
216
+ // Lokasinya juga harus di `.state` PROYEK, bukan di direktori user: direktori
217
+ // user bisa berbeda antar proses, sehingga keduanya tidak saling melihat.
218
+ try {
219
+ const lockDir = projectLockDir(workflowState.stateFile);
220
+ const warning = lockWarning(acquireProcessLock({ dir: lockDir }));
221
+ if (warning) safeLog('error', warning);
222
+ process.once('exit', () => releaseProcessLock({ dir: lockDir }));
223
+ } catch (lockError) {
224
+ safeLog('error', 'Warning: could not record process lock:', lockError.message);
225
+ }
226
+
222
227
  const transport = new StdioServerTransport();
223
228
  // Cover SDK-generated protocol errors as well as tool-handler responses.
224
229
  const send = transport.send.bind(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.13.15",
3
+ "version": "1.13.16",
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",
package/process-lock.js CHANGED
@@ -24,6 +24,41 @@ export function processLockPath({ dir }) {
24
24
  return path.join(dir, LOCK_FILENAME);
25
25
  }
26
26
 
27
+ /**
28
+ * Direktori `.state` milik PROYEK untuk sebuah berkas state.
29
+ *
30
+ * Kenapa bukan `path.dirname(stateFile)` saja: berkas state sesi berada di
31
+ * `.state/users/<user>/`, dan direktori user bisa BERBEDA antar proses (id user
32
+ * dibuat per direktori state). Pada 31 Oktober 2026 dua proses MCP hidup
33
+ * bersamaan tetapi hanya satu lock file yang tercipta, karena masing-masing
34
+ * menulis ke direktori user-nya sendiri - jadi keduanya tidak pernah saling
35
+ * melihat dan peringatan proses ganda tidak muncul.
36
+ *
37
+ * Lock harus satu per PROYEK, bukan per user. Fungsi ini naik sampai menemukan
38
+ * direktori bernama `.state` dan memakai direktori itu. Kalau tidak ada (state
39
+ * di luar tata letak itu), pakai direktori berkasnya sendiri.
40
+ */
41
+ export function projectLockDir(stateFilePath) {
42
+ const resolved = path.resolve(stateFilePath);
43
+ let current = path.dirname(resolved);
44
+ const { root } = path.parse(current);
45
+
46
+ while (true) {
47
+ if (path.basename(current) === ".state") {
48
+ return current;
49
+ }
50
+
51
+ const parent = path.dirname(current);
52
+ if (parent === current || current === root) {
53
+ break;
54
+ }
55
+
56
+ current = parent;
57
+ }
58
+
59
+ return path.dirname(resolved);
60
+ }
61
+
27
62
  /**
28
63
  * Membaca isi lock, atau null bila tidak ada / tidak terbaca.
29
64
  *
@@ -66,7 +101,13 @@ function defaultIsAlive(pid) {
66
101
  * { acquired: true, tookOverFrom: <pid> } -> pemegang lama sudah mati
67
102
  * { acquired: false, holder: {...} } -> ada proses lain yang hidup
68
103
  */
69
- export function acquireProcessLock({ dir, pid = process.pid, now = Date.now(), isAlive = defaultIsAlive }) {
104
+ export function acquireProcessLock({
105
+ dir,
106
+ pid = process.pid,
107
+ now = Date.now(),
108
+ isAlive = defaultIsAlive,
109
+ writeFileImpl = writeFileSync,
110
+ }) {
70
111
  const existing = readProcessLock({ dir });
71
112
 
72
113
  if (existing && existing.pid !== pid && isAlive(existing.pid)) {
@@ -79,10 +120,17 @@ export function acquireProcessLock({ dir, pid = process.pid, now = Date.now(), i
79
120
 
80
121
  try {
81
122
  mkdirSync(dir, { recursive: true });
82
- writeFileSync(processLockPath({ dir }), JSON.stringify(payload), "utf8");
83
- } catch {
84
- // Tidak bisa menulis penanda bukan alasan mematikan server.
85
- return { acquired: true, wrote: false };
123
+ writeFileImpl(processLockPath({ dir }), JSON.stringify(payload), "utf8");
124
+ } catch (error) {
125
+ // Tidak bisa menulis penanda bukan alasan mematikan server, TETAPI juga
126
+ // tidak boleh ditelan: sampai 31 Oktober 2026 kegagalan ini hanya
127
+ // mengembalikan { acquired: true } sehingga sistem tampak sehat padahal
128
+ // tidak ada penanda apa pun di disk. Simpan sebabnya supaya bisa dilaporkan.
129
+ return {
130
+ acquired: true,
131
+ wrote: false,
132
+ error: (error && (error.code || error.message)) || "unknown",
133
+ };
86
134
  }
87
135
 
88
136
  return tookOverFrom ? { acquired: true, tookOverFrom } : { acquired: true };
@@ -118,7 +166,16 @@ function safeHostname() {
118
166
  * nyata, dan supaya pemanggil bebas memilih tidak menampilkan apa pun.
119
167
  */
120
168
  export function lockWarning(result) {
121
- if (!result || result.acquired !== false || !result.holder) return null;
169
+ if (!result) return null;
170
+
171
+ if (result.wrote === false) {
172
+ return (
173
+ `Tidak bisa menulis penanda proses ke ${LOCK_FILENAME} (${result.error}). ` +
174
+ "Deteksi proses MCP ganda tidak akan bekerja sampai ini diperbaiki."
175
+ );
176
+ }
177
+
178
+ if (result.acquired !== false || !result.holder) return null;
122
179
 
123
180
  const started = Number.isFinite(result.holder.startedAt)
124
181
  ? new Date(result.holder.startedAt).toISOString()