@programinglive/dev-workflow-mcp-server 1.13.13 → 1.13.15
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.
|
|
3
|
+
"version": "1.13.15",
|
|
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,20 +164,40 @@ 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) {
|
|
146
186
|
const lastCommitMessage = await git.getLastCommitMessage();
|
|
147
187
|
const effectiveCommitMessage = lastCommitMessage || providedCommitMessage;
|
|
148
188
|
|
|
149
|
-
// "Tidak ada perubahan" bukan berarti "sudah ter-push"
|
|
150
|
-
//
|
|
151
|
-
//
|
|
189
|
+
// "Tidak ada perubahan" bukan berarti "sudah ter-push", dan juga bukan
|
|
190
|
+
// berarti "panggilan ini sudah commit". HEAD bisa saja berupa commit yang
|
|
191
|
+
// belum pernah dikirim, dan bisa juga sudah terkirim oleh panggilan
|
|
192
|
+
// SEBELUMNYA. Memeriksanya mencegah tool menandai selesai padahal remote
|
|
193
|
+
// masih tertinggal - dan kalimat "Assuming commit and push already
|
|
194
|
+
// completed" (dipakai sampai 31 Oktober 2026) membuat agen percaya
|
|
195
|
+
// pekerjaannya sudah masuk padahal tidak ada yang dikerjakan sama sekali.
|
|
152
196
|
const pushed = await verifyPushed(git, branchForPush);
|
|
153
197
|
if (!pushed.ok) {
|
|
154
198
|
return textResponse(
|
|
155
199
|
`⚠️ Tidak ada perubahan untuk di-commit, tetapi HEAD BELUM ada di remote.\n` +
|
|
156
|
-
`Repo: ${
|
|
200
|
+
`Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
|
|
157
201
|
`Sebab: ${pushed.detail}\n\n` +
|
|
158
202
|
`Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
|
|
159
203
|
);
|
|
@@ -165,9 +209,20 @@ export async function handleCommitAndPush(args, context) {
|
|
|
165
209
|
workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
|
|
166
210
|
await workflowState.save();
|
|
167
211
|
|
|
212
|
+
const noActiveTask = workflowState.state.currentPhase === "idle" || !workflowState.state.taskDescription;
|
|
213
|
+
|
|
168
214
|
return textResponse(
|
|
169
|
-
`ℹ️ No changes detected.
|
|
170
|
-
}\
|
|
215
|
+
`ℹ️ No changes detected. No commit was made by this call.\n` +
|
|
216
|
+
`Repo: ${repoName}\nBranch: ${branchForPush} (verified on remote, HEAD ${pushed.head.slice(0, 8)})\n` +
|
|
217
|
+
`Commit terakhir: ${effectiveCommitMessage || "(tidak terbaca)"}\n\n` +
|
|
218
|
+
(noActiveTask
|
|
219
|
+
? `Tidak ada task aktif di state workflow (currentPhase "idle"), jadi memang tidak ada ` +
|
|
220
|
+
`yang diharapkan dari panggilan ini.\n\n`
|
|
221
|
+
: "") +
|
|
222
|
+
`Yang terbukti: commit terakhir sudah ada di remote. Yang TIDAK terbukti: commit itu berisi ` +
|
|
223
|
+
`pekerjaan task ini. Kalau perubahan seharusnya ada, berarti belum ditulis, sudah di-commit ` +
|
|
224
|
+
`oleh panggilan sebelumnya, atau berada di repo/branch lain.\n\n` +
|
|
225
|
+
`Next: Run 'perform_release' to handle the release and push tags.`
|
|
171
226
|
);
|
|
172
227
|
}
|
|
173
228
|
|
|
@@ -228,7 +283,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
228
283
|
if (!pushed.ok) {
|
|
229
284
|
return textResponse(
|
|
230
285
|
`❌ Commit dibuat, tetapi TIDAK ter-push.\n` +
|
|
231
|
-
`Repo: ${
|
|
286
|
+
`Repo: ${repoName}\nBranch: ${branchForPush || "(tidak diketahui)"}\n` +
|
|
232
287
|
`Sebab: ${pushed.detail}\n\n` +
|
|
233
288
|
`Commit lokal: ${generatedSummary}\n\n` +
|
|
234
289
|
`Jalankan 'commit_and_push' lagi dengan 'branch' yang benar, atau push manual.`
|
|
@@ -242,7 +297,7 @@ export async function handleCommitAndPush(args, context) {
|
|
|
242
297
|
await workflowState.save();
|
|
243
298
|
|
|
244
299
|
return textResponse(
|
|
245
|
-
`✅ Commit and push completed!\nRepo: ${
|
|
300
|
+
`✅ 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
301
|
);
|
|
247
302
|
}
|
|
248
303
|
|
|
@@ -134,8 +134,19 @@ export async function handleClaimFactoryTask(args = {}, { workflowState } = {})
|
|
|
134
134
|
// dikerjakan, dan papan terlihat penuh task mati.
|
|
135
135
|
startKeeping(config, session, step);
|
|
136
136
|
|
|
137
|
+
// Arsipkan task lokal yang sedang berjalan, jangan hapus diam-diam.
|
|
138
|
+
//
|
|
139
|
+
// 31 Oktober 2026 claim_factory_task dipanggil di tengah task lain yang baru
|
|
140
|
+
// sampai tahap rilis. `reset()` tanpa archive membuat task itu lenyap: tidak
|
|
141
|
+
// ada entri history, tidak ada peringatan, tidak disebut di jawaban.
|
|
142
|
+
// `start_task` sudah mengarsipkan sejak awal; claim tidak.
|
|
143
|
+
let archivedTask = null;
|
|
137
144
|
if (workflowState) {
|
|
138
|
-
workflowState.reset(
|
|
145
|
+
const archived = workflowState.reset({
|
|
146
|
+
archive: true,
|
|
147
|
+
reason: "Diganti claim_factory_task sebelum task selesai",
|
|
148
|
+
});
|
|
149
|
+
archivedTask = archived?.archivedTask || null;
|
|
139
150
|
workflowState.state.currentPhase = "coding";
|
|
140
151
|
workflowState.state.taskDescription = `[${task.taskKey || task.id}] ${task.title}`;
|
|
141
152
|
workflowState.state.taskType = "other";
|
|
@@ -145,6 +156,10 @@ export async function handleClaimFactoryTask(args = {}, { workflowState } = {})
|
|
|
145
156
|
return textResponse(
|
|
146
157
|
[
|
|
147
158
|
`Claimed [${task.taskKey || task.id}] ${task.title}`,
|
|
159
|
+
archivedTask
|
|
160
|
+
? `⚠️ Task lokal yang belum selesai diarsipkan ke history: "${archivedTask}". ` +
|
|
161
|
+
`Pekerjaannya tidak dihapus, tetapi tidak lagi aktif di sesi ini.`
|
|
162
|
+
: "",
|
|
148
163
|
session.tookOver && session.previousHolder
|
|
149
164
|
? `Took over from ${session.previousHolder.deviceName} (${session.previousHolder.agent}). The reason is recorded on the board.`
|
|
150
165
|
: "",
|
|
@@ -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
|
-
`❌
|
|
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))) {
|