@programinglive/dev-workflow-mcp-server 1.11.0 → 1.12.0
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/package.json +1 -1
- package/workflow-state.js +109 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
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/workflow-state.js
CHANGED
|
@@ -408,6 +408,55 @@ async function ensureCompatibilityLinks(primaryPath) {
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Identitas sesi agen ini.
|
|
413
|
+
*
|
|
414
|
+
* MENGAPA ADA
|
|
415
|
+
* -----------
|
|
416
|
+
* Berkas state dulu disimpan per USER, jadi empat proses MCP berbagi satu
|
|
417
|
+
* `workflow-state.json`. Akibatnya `start_task` di satu sesi mengganti task
|
|
418
|
+
* sesi lain di tengah jalan, dan gerbang kualitas (ready check, tests, docs)
|
|
419
|
+
* ikut dibagi sehingga sesi B bisa memakai hasil sesi A untuk commit.
|
|
420
|
+
*
|
|
421
|
+
* Sekarang task dan gerbangnya milik SESI; `history` tetap milik PROYEK.
|
|
422
|
+
*
|
|
423
|
+
* Urutannya sengaja: env eksplisit dulu (untuk tes dan pemanggil lain), lalu
|
|
424
|
+
* id sesi editor, lalu proses. Jatuh ke proses membuat identitas tetap stabil
|
|
425
|
+
* walau modul di-reload dalam proses yang sama.
|
|
426
|
+
*/
|
|
427
|
+
export function resolveSessionKey(env = process.env) {
|
|
428
|
+
for (const candidate of [env.DEV_WORKFLOW_SESSION_ID, env.OPENCODE_SESSION_ID, env.CLIENT_SESSION_ID]) {
|
|
429
|
+
const value = typeof candidate === "string" ? candidate.trim() : "";
|
|
430
|
+
if (value) return sanitizeSessionKey(value);
|
|
431
|
+
}
|
|
432
|
+
return `pid-${process.pid}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function sanitizeSessionKey(value) {
|
|
436
|
+
const cleaned = String(value).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
437
|
+
return (cleaned || "session").slice(0, 64);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Nama berkas state untuk sesi ini, di direktori user yang sama. */
|
|
441
|
+
function sessionStateFilename(env = process.env) {
|
|
442
|
+
return `workflow-state.${resolveSessionKey(env)}.json`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Berkas tempat `history` bersama disimpan.
|
|
447
|
+
*
|
|
448
|
+
* Dalam tata letak nyata (`.state/users/<user>/`) history dipisahkan dari state
|
|
449
|
+
* sesi supaya catatan proyek tidak terpecah per sesi. Pemanggil yang memberi
|
|
450
|
+
* path eksplisit di luar tata letak itu tetap memakai satu berkas, persis
|
|
451
|
+
* seperti sebelumnya.
|
|
452
|
+
*/
|
|
453
|
+
export function resolveHistoryFile(stateFilePath) {
|
|
454
|
+
const resolved = path.resolve(stateFilePath);
|
|
455
|
+
const dir = path.dirname(resolved);
|
|
456
|
+
const inUserTree = path.basename(path.dirname(dir)) === "users";
|
|
457
|
+
return inUserTree ? path.join(dir, STATE_FILENAME) : resolved;
|
|
458
|
+
}
|
|
459
|
+
|
|
411
460
|
export function resolveStateFile() {
|
|
412
461
|
const customPath = process.env.DEV_WORKFLOW_STATE_FILE;
|
|
413
462
|
if (customPath) {
|
|
@@ -438,12 +487,15 @@ export function resolveStateFile() {
|
|
|
438
487
|
|
|
439
488
|
const stateDir = path.join(projectRoot, STATE_DIR);
|
|
440
489
|
const userDir = getUserScopedDir(stateDir);
|
|
441
|
-
return path.join(userDir,
|
|
490
|
+
return path.join(userDir, sessionStateFilename());
|
|
442
491
|
}
|
|
443
492
|
|
|
444
493
|
export class WorkflowState {
|
|
445
494
|
constructor(stateFilePath = resolveStateFile()) {
|
|
446
495
|
this.stateFile = stateFilePath;
|
|
496
|
+
// History milik proyek, state milik sesi. Dalam tata letak nyata keduanya
|
|
497
|
+
// berkas berbeda; untuk path eksplisit keduanya menunjuk berkas yang sama.
|
|
498
|
+
this.historyFile = resolveHistoryFile(stateFilePath);
|
|
447
499
|
this.state = {
|
|
448
500
|
currentPhase: "idle",
|
|
449
501
|
taskDescription: "",
|
|
@@ -482,6 +534,7 @@ export class WorkflowState {
|
|
|
482
534
|
// Ensure compatibility mirrors exist whenever the primary file is present
|
|
483
535
|
// Run this as non-blocking to speed up startup
|
|
484
536
|
ensureCompatibilityLinks(this.stateFile).catch(() => { });
|
|
537
|
+
await this.mergeSharedHistory();
|
|
485
538
|
if (typeof this.state.testsCreated !== "boolean") {
|
|
486
539
|
this.state.testsCreated = false;
|
|
487
540
|
}
|
|
@@ -530,9 +583,61 @@ export class WorkflowState {
|
|
|
530
583
|
}
|
|
531
584
|
// Missing file is fine; bootstrap fresh artifacts for downstream consumers.
|
|
532
585
|
await this.ensurePrimaryFile();
|
|
586
|
+
// Sesi baru tetap harus melihat riwayat proyek yang sudah ada.
|
|
587
|
+
await this.mergeSharedHistory();
|
|
533
588
|
}
|
|
534
589
|
}
|
|
535
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Menyatukan `history` sesi ini dengan `history` bersama.
|
|
593
|
+
*
|
|
594
|
+
* History adalah catatan proyek: task yang selesai di satu sesi harus
|
|
595
|
+
* terlihat dari sesi lain, dan entri yang sama tidak boleh berganda walau
|
|
596
|
+
* berkali-kali disimpan.
|
|
597
|
+
*/
|
|
598
|
+
async mergeSharedHistory() {
|
|
599
|
+
if (this.historyFile === this.stateFile) {
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const shared = await this.readHistoryFile();
|
|
604
|
+
const merged = this.mergeDiskHistory(shared);
|
|
605
|
+
if (merged > 0) {
|
|
606
|
+
await this.writeHistoryFile();
|
|
607
|
+
}
|
|
608
|
+
return merged;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async readHistoryFile() {
|
|
612
|
+
try {
|
|
613
|
+
const data = await fs.readFile(this.historyFile, "utf-8");
|
|
614
|
+
return JSON.parse(data);
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Menulis hanya `history` ke berkas bersama, tanpa task milik sesi ini. */
|
|
621
|
+
async writeHistoryFile() {
|
|
622
|
+
if (this.historyFile === this.stateFile) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const shared = (await this.readHistoryFile()) || {};
|
|
627
|
+
const merged = Array.isArray(shared.history) ? shared.history.slice() : [];
|
|
628
|
+
const seen = new Set(merged.map((entry) => this.historyKey(entry)));
|
|
629
|
+
for (const entry of this.state.history || []) {
|
|
630
|
+
const key = this.historyKey(entry);
|
|
631
|
+
if (!seen.has(key)) {
|
|
632
|
+
merged.push(entry);
|
|
633
|
+
seen.add(key);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
await fs.mkdir(path.dirname(this.historyFile), { recursive: true });
|
|
638
|
+
await fs.writeFile(this.historyFile, JSON.stringify({ ...shared, history: merged }, null, 2));
|
|
639
|
+
}
|
|
640
|
+
|
|
536
641
|
async ensurePrimaryFile() {
|
|
537
642
|
let created = false;
|
|
538
643
|
try {
|
|
@@ -558,6 +663,9 @@ export class WorkflowState {
|
|
|
558
663
|
updatedAt: new Date().toISOString(),
|
|
559
664
|
};
|
|
560
665
|
await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
666
|
+
// History ditulis ke berkas bersama supaya sesi lain melihat task yang
|
|
667
|
+
// selesai di sini, sementara task yang sedang berjalan tetap milik sesi.
|
|
668
|
+
await this.writeHistoryFile();
|
|
561
669
|
await ensureCompatibilityLinks(this.stateFile);
|
|
562
670
|
await this.updateProjectSummary();
|
|
563
671
|
await this.syncToDatabase();
|