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

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/factory-client.js CHANGED
@@ -25,6 +25,7 @@ import os from "os";
25
25
  import { assertSafeKnowledge } from './security/policy.js';
26
26
  import { sanitize } from './security/sanitizer.js';
27
27
  import { registerRuntimeSecret } from './security/runtime-secrets.js';
28
+ import { refreshFactoryCredentials, describeLoginFailure } from './factory-credentials.js';
28
29
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
29
30
  import path from "path";
30
31
 
@@ -397,6 +398,27 @@ export function buildWorkFinishPayload({ sessionId, deviceId, outcome, summary,
397
398
  // HTTP layer
398
399
  // ---------------------------------------------------------------------------
399
400
 
401
+ /**
402
+ * Kapan proses MCP ini hidup.
403
+ *
404
+ * Dipakai di pesan gagal login: tanpa ini pengguna tidak punya cara tahu bahwa
405
+ * environment yang dipakai sudah usang.
406
+ */
407
+ function processStartedAt() {
408
+ return new Date(Date.now() - process.uptime() * 1000);
409
+ }
410
+
411
+ /** Satu percobaan login dengan pasangan email/password tertentu. */
412
+ function attemptLogin(config, fetchImpl, { email, password }) {
413
+ registerRuntimeSecret(password);
414
+
415
+ return fetchImpl(`${config.pocketBaseUrl}/api/collections/activity_users/auth-with-password`, {
416
+ method: "POST",
417
+ headers: { "content-type": "application/json" },
418
+ body: JSON.stringify({ identity: email, password }),
419
+ });
420
+ }
421
+
400
422
  async function resolveToken(config, fetchImpl = fetch) {
401
423
  registerRuntimeSecret(config.token); registerRuntimeSecret(config.password);
402
424
  if (config.token) return config.token;
@@ -405,24 +427,57 @@ async function resolveToken(config, fetchImpl = fetch) {
405
427
  const cached = tokenCache.get(key);
406
428
  if (cached && cached.expiresAt > Date.now()) return cached.token;
407
429
 
408
- const response = await fetchImpl(`${config.pocketBaseUrl}/api/collections/activity_users/auth-with-password`, {
409
- method: "POST",
410
- headers: { "content-type": "application/json" },
411
- body: JSON.stringify({ identity: config.email, password: config.password }),
412
- });
430
+ const remember = (token) => {
431
+ registerRuntimeSecret(token);
432
+ tokenCache.set(key, { token, expiresAt: Date.now() + TOKEN_TTL_MS });
433
+ return token;
434
+ };
413
435
 
414
- if (!response.ok) {
415
- throw new Error(
416
- `Factory login failed (HTTP ${response.status}). Check FACTORY_EMAIL and FACTORY_PASSWORD, or set FACTORY_TOKEN.`,
417
- );
436
+ const response = await attemptLogin(config, fetchImpl, config);
437
+
438
+ if (response.ok) {
439
+ const body = await response.json();
440
+ if (!body.token) throw new Error("Factory login returned no token.");
441
+ return remember(body.token);
418
442
  }
419
443
 
420
- const body = await response.json();
421
- if (!body.token) throw new Error("Factory login returned no token.");
422
- registerRuntimeSecret(body.token);
444
+ // Kredensial ditolak. Sebelum menyerah, baca ulang berkas konfigurasi: proses
445
+ // MCP mewarisi environment saat dijalankan dan tidak pernah memuat ulang, jadi
446
+ // password yang baru diubah di opencode.json tidak pernah terpakai sampai
447
+ // OpenCode di-restart. 31 Oktober 2026 itu mematikan otomasi berjam-jam.
448
+ const refreshed = refreshFactoryCredentials(config);
449
+
450
+ if (refreshed && refreshed.changed) {
451
+ if (refreshed.token) {
452
+ return remember(refreshed.token);
453
+ }
454
+
455
+ if (refreshed.email && refreshed.password) {
456
+ const retry = await attemptLogin(config, fetchImpl, refreshed);
457
+
458
+ if (retry.ok) {
459
+ const body = await retry.json();
460
+ if (!body.token) throw new Error("Factory login returned no token.");
461
+ return remember(body.token);
462
+ }
463
+
464
+ throw new Error(
465
+ describeLoginFailure({
466
+ status: retry.status,
467
+ processStartedAt: processStartedAt(),
468
+ credentialsChanged: true,
469
+ }),
470
+ );
471
+ }
472
+ }
423
473
 
424
- tokenCache.set(key, { token: body.token, expiresAt: Date.now() + TOKEN_TTL_MS });
425
- return body.token;
474
+ throw new Error(
475
+ describeLoginFailure({
476
+ status: response.status,
477
+ processStartedAt: processStartedAt(),
478
+ credentialsChanged: Boolean(refreshed && refreshed.changed),
479
+ }),
480
+ );
426
481
  }
427
482
 
428
483
  /** Test helper: forget cached tokens (for example after a password change). */
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Membaca ulang kredensial Factory dari berkas konfigurasi OpenCode.
3
+ *
4
+ * Kenapa perlu: proses MCP mewarisi environment saat ia di-spawn dan tidak
5
+ * pernah memuat ulang. 31 Oktober 2026 otomasi Factory mati berjam-jam dengan
6
+ * pesan "login failed (HTTP 400). Check FACTORY_EMAIL and FACTORY_PASSWORD"
7
+ * padahal password di `~/.config/opencode/opencode.json` sudah benar dan login
8
+ * langsung ke PocketBase menjawab 200. Penyebabnya: proses MCP hidup sejak
9
+ * 30 Oktober dan memegang password LAMA di memorinya.
10
+ *
11
+ * Modul ini TIDAK menyimpan rahasia baru ke disk. Ia hanya membaca berkas yang
12
+ * sudah ada dan sudah berisi rahasia itu.
13
+ */
14
+
15
+ import { readFileSync } from "fs";
16
+ import os from "os";
17
+ import path from "path";
18
+
19
+ /** Nama server di blok `mcp` pada konfigurasi OpenCode. */
20
+ export const DEFAULT_SERVER_NAME = "dev-workflow";
21
+
22
+ /**
23
+ * Berkas kredensial bawaan: konfigurasi OpenCode.
24
+ *
25
+ * Bisa diarahkan lewat `FACTORY_CREDENTIALS_FILE` bila konfigurasinya di tempat
26
+ * lain (misalnya berkas khusus tanpa isi lain).
27
+ */
28
+ export function defaultCredentialsFile(env = process.env) {
29
+ const explicit = clean(env.FACTORY_CREDENTIALS_FILE);
30
+ if (explicit) return explicit;
31
+
32
+ const home = clean(env.HOME) || clean(env.USERPROFILE) || os.homedir();
33
+ const configHome = clean(env.XDG_CONFIG_HOME) || path.join(home, ".config");
34
+
35
+ return path.join(configHome, "opencode", "opencode.json");
36
+ }
37
+
38
+ function clean(value) {
39
+ return typeof value === "string" ? value.trim() : "";
40
+ }
41
+
42
+ /**
43
+ * Mengambil blok `environment` milik server MCP kita dari teks konfigurasi.
44
+ *
45
+ * Dua bentuk didukung, karena keduanya ada di lapangan:
46
+ * { mcp: { "<nama>": {...} } } (konfigurasi lama)
47
+ * { mcp: { servers: { "<nama>": {...} } } } (konfigurasi OpenCode di mesin ini)
48
+ *
49
+ * Mengembalikan null bila teks bukan JSON, tidak punya `mcp`, atau tidak punya
50
+ * blok server yang diminta. Mengambil server LAIN akan lebih buruk daripada
51
+ * tidak mengambil apa pun: kredensial proyek lain bisa terpakai.
52
+ */
53
+ export function extractFactoryEnv(text, { serverName = DEFAULT_SERVER_NAME } = {}) {
54
+ let parsed;
55
+
56
+ try {
57
+ parsed = JSON.parse(text);
58
+ } catch {
59
+ return null;
60
+ }
61
+
62
+ const mcp = parsed && parsed.mcp;
63
+ if (!mcp || typeof mcp !== "object") return null;
64
+
65
+ const block = mcp[serverName] || (mcp.servers && mcp.servers[serverName]);
66
+ if (!block || typeof block !== "object") return null;
67
+
68
+ const environment = block.environment;
69
+ if (!environment || typeof environment !== "object") return null;
70
+
71
+ return environment;
72
+ }
73
+
74
+ /**
75
+ * Membandingkan kredensial yang sedang dipakai proses dengan yang ada di berkas.
76
+ *
77
+ * Mengembalikan null bila tidak ada yang bisa disegarkan (berkas tidak ada,
78
+ * rusak, atau tanpa blok server kita). Selain itu objek dengan kredensial
79
+ * TERBARU, plus `changed` yang menandakan apakah ia berbeda dari yang dipakai.
80
+ */
81
+ export function refreshFactoryCredentials(
82
+ config = {},
83
+ { env = process.env, readFileImpl = readFileSync, serverName = DEFAULT_SERVER_NAME } = {}
84
+ ) {
85
+ const file = defaultCredentialsFile(env);
86
+
87
+ let text;
88
+ try {
89
+ text = String(readFileImpl(file, "utf8"));
90
+ } catch {
91
+ return null;
92
+ }
93
+
94
+ const environment = extractFactoryEnv(text, { serverName });
95
+ if (!environment) return null;
96
+
97
+ const token = clean(environment.FACTORY_TOKEN);
98
+ const email = clean(environment.FACTORY_EMAIL);
99
+ const password = clean(environment.FACTORY_PASSWORD);
100
+
101
+ const current = {
102
+ token: clean(config.token),
103
+ email: clean(config.email),
104
+ password: clean(config.password),
105
+ };
106
+
107
+ const changed =
108
+ token !== current.token || email !== current.email || password !== current.password;
109
+
110
+ return {
111
+ token,
112
+ email,
113
+ password,
114
+ changed,
115
+ source: "file",
116
+ file,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Pesan gagal login yang bisa ditindaklanjuti.
122
+ *
123
+ * Dua hal yang membedakannya dari pesan lama ("check FACTORY_EMAIL and
124
+ * FACTORY_PASSWORD"): ia menyebut KAPAN proses ini hidup, dan ia menyebut
125
+ * apakah kredensial di berkas berbeda dari yang sedang dipakai. Tanpa itu,
126
+ * pengguna disuruh memeriksa password yang sebenarnya sudah benar.
127
+ */
128
+ export function describeLoginFailure({ status, processStartedAt, credentialsChanged = false }) {
129
+ const started = processStartedAt instanceof Date ? processStartedAt.toISOString() : "";
130
+
131
+ const lines = [
132
+ `Factory login failed (HTTP ${status}).`,
133
+ started
134
+ ? `Proses MCP ini hidup sejak ${started} dan memakai environment yang diwarisi saat itu - ia tidak pernah memuat ulang berkas konfigurasi.`
135
+ : "Proses MCP ini memakai environment yang diwarisi saat ia dijalankan.",
136
+ ];
137
+
138
+ if (credentialsChanged) {
139
+ lines.push(
140
+ "Kredensial di berkas konfigurasi BERBEDA dari yang sedang dipakai proses ini. " +
141
+ "Kredensial terbaru sudah dicoba dan tetap ditolak, jadi kemungkinan besar " +
142
+ "password-nya memang salah, atau akunnya belum bisa login."
143
+ );
144
+ } else {
145
+ lines.push(
146
+ "Kredensial di berkas konfigurasi SAMA dengan yang dipakai, atau berkasnya tidak ditemukan."
147
+ );
148
+ }
149
+
150
+ lines.push(
151
+ "Periksa FACTORY_EMAIL/FACTORY_PASSWORD, atau set FACTORY_TOKEN. " +
152
+ "Bila password baru saja diubah, restart OpenCode agar proses MCP memakai kredensial terbaru."
153
+ );
154
+
155
+ return lines.join(" ");
156
+ }
package/index.js CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  GetPromptRequestSchema,
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
15
  import { fileURLToPath } from "url";
16
+ import path from "path";
17
+ import { acquireProcessLock, releaseProcessLock, lockWarning } from "./process-lock.js";
16
18
  import { exec, setActiveProjectDir } from "./exec.js";
17
19
  import { normalizeRequestArgs, shellEscape } from "./utils.js";
18
20
  import { createGitFacade } from "./git-facade.js";
@@ -197,6 +199,20 @@ export async function main() {
197
199
  // Select request metadata before the first disk write. Creating an idle
198
200
  // PID fallback here leaves a phantom session alongside the real sessions.
199
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
+ }
200
216
  } catch (error) {
201
217
  safeLog('error', `[${new Date().toISOString()}] Warning: Failed to initialize workflow state:`, error.message);
202
218
  // Continue anyway - workflow state is optional for MCP to function
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@programinglive/dev-workflow-mcp-server",
3
- "version": "1.13.11",
3
+ "version": "1.13.13",
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,134 @@
1
+ /**
2
+ * Satu penanda "proses MCP mana yang sedang hidup" per direktori state.
3
+ *
4
+ * Kenapa perlu: 31 Oktober 2026 ada DUA proses dev-workflow MCP hidup
5
+ * bersamaan. Yang lebih tua memegang password Factory lama di memorinya, yang
6
+ * lebih muda memegang yang baru, jadi tool yang sama menjawab berbeda
7
+ * tergantung proses mana yang melayaninya - dan keduanya menulis berkas state
8
+ * workflow yang sama.
9
+ *
10
+ * Modul ini TIDAK membunuh proses lain. Membunuh proses secara otomatis adalah
11
+ * tindakan berbahaya (ada guard tersendiri untuk itu). Yang dilakukan: proses
12
+ * kedua mengetahui bahwa ia bukan satu-satunya, lalu melaporkannya, sehingga
13
+ * pengguna bisa menutup OpenCode dan memulai ulang dengan bersih.
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
17
+ import os from "os";
18
+ import path from "path";
19
+
20
+ export const LOCK_FILENAME = "mcp-process.lock";
21
+
22
+ /** Lokasi berkas lock untuk satu direktori state. */
23
+ export function processLockPath({ dir }) {
24
+ return path.join(dir, LOCK_FILENAME);
25
+ }
26
+
27
+ /**
28
+ * Membaca isi lock, atau null bila tidak ada / tidak terbaca.
29
+ *
30
+ * Berkas rusak sengaja dianggap "tidak ada lock": penanda yang tidak bisa
31
+ * dibaca tidak boleh membuat server gagal start. Lebih baik dua proses
32
+ * bertabrakan daripada server tidak hidup sama sekali.
33
+ */
34
+ export function readProcessLock({ dir }) {
35
+ const file = processLockPath({ dir });
36
+
37
+ if (!existsSync(file)) return null;
38
+
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
41
+ if (!parsed || typeof parsed.pid !== "number") return null;
42
+ return parsed;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function defaultIsAlive(pid) {
49
+ if (!Number.isFinite(pid) || pid <= 0) return false;
50
+
51
+ try {
52
+ // Sinyal 0 tidak mengirim apa pun; ia hanya memeriksa apakah proses ada.
53
+ process.kill(pid, 0);
54
+ return true;
55
+ } catch (error) {
56
+ // EPERM berarti proses ADA tetapi milik pengguna lain.
57
+ return error && error.code === "EPERM";
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Mencoba menjadi pemegang lock.
63
+ *
64
+ * Mengembalikan:
65
+ * { acquired: true } -> kita pemegangnya
66
+ * { acquired: true, tookOverFrom: <pid> } -> pemegang lama sudah mati
67
+ * { acquired: false, holder: {...} } -> ada proses lain yang hidup
68
+ */
69
+ export function acquireProcessLock({ dir, pid = process.pid, now = Date.now(), isAlive = defaultIsAlive }) {
70
+ const existing = readProcessLock({ dir });
71
+
72
+ if (existing && existing.pid !== pid && isAlive(existing.pid)) {
73
+ return { acquired: false, holder: existing };
74
+ }
75
+
76
+ const tookOverFrom = existing && existing.pid !== pid ? existing.pid : undefined;
77
+
78
+ const payload = { pid, startedAt: now, host: safeHostname() };
79
+
80
+ try {
81
+ 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 };
86
+ }
87
+
88
+ return tookOverFrom ? { acquired: true, tookOverFrom } : { acquired: true };
89
+ }
90
+
91
+ /** Melepas lock hanya bila memang milik kita. */
92
+ export function releaseProcessLock({ dir, pid = process.pid }) {
93
+ const existing = readProcessLock({ dir });
94
+
95
+ if (!existing || existing.pid !== pid) return false;
96
+
97
+ try {
98
+ unlinkSync(processLockPath({ dir }));
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function safeHostname() {
106
+ try {
107
+ return os.hostname();
108
+ } catch {
109
+ return "";
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Pesan yang dicatat saat proses lain sudah memegang lock, atau null bila tidak
115
+ * ada yang perlu dilaporkan.
116
+ *
117
+ * Dipisah dari `acquireProcessLock` supaya bisa diuji tanpa menyentuh proses
118
+ * nyata, dan supaya pemanggil bebas memilih tidak menampilkan apa pun.
119
+ */
120
+ export function lockWarning(result) {
121
+ if (!result || result.acquired !== false || !result.holder) return null;
122
+
123
+ const started = Number.isFinite(result.holder.startedAt)
124
+ ? new Date(result.holder.startedAt).toISOString()
125
+ : "waktu tidak diketahui";
126
+
127
+ return (
128
+ `Proses dev-workflow MCP lain (PID ${result.holder.pid}, hidup sejak ${started}) ` +
129
+ "masih berjalan dan memakai direktori state yang sama. " +
130
+ "Proses yang lebih tua memegang environment saat ia dijalankan, jadi kredensial Factory " +
131
+ "dan state workflow bisa berbeda antar proses. Tutup OpenCode lalu buka lagi untuk " +
132
+ "memastikan hanya satu proses yang hidup."
133
+ );
134
+ }
package/workflow-state.js CHANGED
@@ -33,6 +33,22 @@ let cachedUserStateDir = null;
33
33
  */
34
34
  const PROCESS_TOKEN = `proc-${process.pid}-${Math.random().toString(16).slice(2, 10)}`;
35
35
 
36
+ /**
37
+ * Langkah workflow yang hanya boleh MAJU dalam satu task.
38
+ *
39
+ * Hanya flag PENCAPAIAN. Gerbang kualitas (`readyToCommit`,
40
+ * `readyCheckCompleted`, `commitAndPushCompleted`, `released`) SENGAJA tidak
41
+ * masuk: keempatnya direset ketika langkah sebelumnya diulang, dan memaksa
42
+ * nilainya tetap true akan melemahkan gerbang yang justru ingin ditegakkan.
43
+ */
44
+ const MONOTONIC_STEP_FIELDS = [
45
+ "featureFlowCreated",
46
+ "bugFixed",
47
+ "testsCreated",
48
+ "testsPassed",
49
+ "documentationCreated",
50
+ ];
51
+
36
52
  function migrateOwnerMetadata(value) {
37
53
  if(value?.owner && !value.owner.processMarker && /^proc-[A-Za-z0-9-]+$/.test(value.owner.token || '')) {
38
54
  value.owner.processMarker=value.owner.token;
@@ -820,6 +836,44 @@ export class WorkflowState {
820
836
  return created;
821
837
  }
822
838
 
839
+ /**
840
+ * Gabungkan kemajuan langkah dari disk bila task-nya sama.
841
+ *
842
+ * Hanya menyentuh flag yang MONOTON dalam satu task. Field yang bisa turun
843
+ * dengan sengaja (mis. `readyCheckCompleted` yang direset `create_tests`)
844
+ * tidak digabung, karena itu bagian dari alur normal satu proses.
845
+ *
846
+ * `reconciled` sudah memuat state disk terbaru; `this.state` adalah milik
847
+ * proses ini. Kalau task berbeda atau disk idle, tidak ada yang digabung:
848
+ * itu task lain, atau task baru yang memang harus mulai dari nol.
849
+ */
850
+ mergeMonotonicProgress(reconciled) {
851
+ const diskTask = (reconciled?.diskTask || "").trim();
852
+ const ourTask = (this.state.taskDescription || "").trim();
853
+ if (!diskTask || !ourTask || diskTask !== ourTask) {
854
+ return;
855
+ }
856
+ if ((reconciled?.diskPhase || "idle") === "idle") {
857
+ return;
858
+ }
859
+
860
+ // Harus dari `reconciled` yang BARU dibaca. `lastReconcile` adalah hasil
861
+ // penyimpanan sebelumnya, sehingga bisa tertinggal satu putaran - dan
862
+ // justru itu yang membuat penggabungan gagal menangkap langkah terbaru.
863
+ const disk = reconciled?.diskState;
864
+ if (!disk) {
865
+ return;
866
+ }
867
+
868
+ // Kemajuan hanya boleh maju. Sekali sebuah langkah tercapai di proses mana
869
+ // pun, langkah itu tetap tercapai.
870
+ for (const field of MONOTONIC_STEP_FIELDS) {
871
+ if (disk[field] === true && this.state[field] !== true) {
872
+ this.state[field] = true;
873
+ }
874
+ }
875
+ }
876
+
823
877
  async save(options = {}) {
824
878
  assertSafeKnowledge(this.state);
825
879
  const reconciled = await this.reconcileWithDisk();
@@ -839,6 +893,19 @@ export class WorkflowState {
839
893
  }
840
894
  this.lastConflict = null;
841
895
 
896
+ // Dua proses MCP bisa melayani sesi yang SAMA (OpenCode men-spawn lebih dari
897
+ // satu instance), dan keduanya menulis berkas state yang sama. Tanpa ini,
898
+ // proses yang memegang state basi menimpa langkah yang sudah dicapai proses
899
+ // lain: `mark_bug_fixed` menjawab sukses, lalu `create_tests` menjawab
900
+ // "mark your feature/bug as fixed first". Gejalanya intermiten, sehingga
901
+ // mudah disalahkan pada hal lain.
902
+ //
903
+ // Langkah workflow bersifat monoton dalam satu task: sekali tercapai, tidak
904
+ // boleh mundur. Karena itu digabung dengan OR - tetapi HANYA bila task di
905
+ // disk memang task yang sama. `start_task` mengganti task, dan pada saat itu
906
+ // flag harus benar-benar direset, bukan dihidupkan kembali.
907
+ this.mergeMonotonicProgress(reconciled);
908
+
842
909
  this.state.owner = {
843
910
  processMarker: PROCESS_TOKEN,
844
911
  pid: process.pid,
@@ -988,11 +1055,15 @@ export class WorkflowState {
988
1055
  foreignToken: null,
989
1056
  diskTask: "",
990
1057
  diskPhase: "idle",
1058
+ // Dipakai mergeMonotonicProgress: state disk apa adanya, supaya flag
1059
+ // langkah yang sudah dicapai proses lain bisa digabung.
1060
+ diskState: null,
991
1061
  };
992
1062
  const disk = await this.readDiskState();
993
1063
  if (!disk) {
994
1064
  return result;
995
1065
  }
1066
+ result.diskState = disk;
996
1067
 
997
1068
  result.diskTask = disk.taskDescription || "";
998
1069
  result.diskPhase = disk.currentPhase || "idle";