@programinglive/dev-workflow-mcp-server 1.8.1 → 1.10.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.
Files changed (114) hide show
  1. package/.env.example +21 -1
  2. package/AGENTS.md +25 -0
  3. package/README.md +372 -649
  4. package/factory-client.js +554 -0
  5. package/git-facade.js +35 -0
  6. package/index.js +9 -34
  7. package/package.json +21 -20
  8. package/scripts/bootstrap-unix.sh +55 -0
  9. package/scripts/bootstrap-windows.ps1 +75 -0
  10. package/scripts/install-husky.js +43 -0
  11. package/scripts/setup-stack.js +436 -0
  12. package/scripts/setup-test-dbs.sh +61 -0
  13. package/scripts/test-db-adapters.sh +71 -0
  14. package/scripts/verify-bundle.mjs +92 -0
  15. package/tools/definitions.js +143 -0
  16. package/tools/handler-modules/commit-handlers.js +2 -1
  17. package/tools/handler-modules/factory-handlers.js +276 -0
  18. package/tools/handler-modules/index.js +24 -7
  19. package/tools/handler-modules/shared.js +7 -41
  20. package/tools/handler-modules/status-handlers.js +25 -3
  21. package/tools/handler-modules/workflow-handlers.js +57 -10
  22. package/vite.config.js +6 -0
  23. package/workflow-state.js +176 -2
  24. package/.agent/workflows/dev-workflow-bugfix.md +0 -250
  25. package/.agent/workflows/dev-workflow-feature.md +0 -201
  26. package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
  27. package/.agent/workflows/dev-workflow-refactor.md +0 -238
  28. package/.env.production +0 -16
  29. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
  30. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  31. package/.github/pull_request_template.md +0 -22
  32. package/.github/workflows/github-release.yml +0 -131
  33. package/.husky/commit-msg +0 -3
  34. package/.husky/pre-commit +0 -2
  35. package/CHANGELOG.md +0 -1145
  36. package/docs/GCP_DEPLOYMENT.md +0 -298
  37. package/docs/NETLIFY_DEPLOYMENT.md +0 -125
  38. package/docs/PLESK_DEPLOYMENT.md +0 -170
  39. package/docs/PRD.md +0 -86
  40. package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
  41. package/docs/antigravity/EXAMPLES.md +0 -535
  42. package/docs/antigravity/GETTING-STARTED.md +0 -292
  43. package/docs/antigravity/TROUBLESHOOTING.md +0 -742
  44. package/docs/antigravity/WALKTHROUGH.md +0 -25
  45. package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
  46. package/docs/release-notes/RELEASE_NOTES.md +0 -524
  47. package/docs/web-dashboard.md +0 -175
  48. package/scripts/test-auth-flow.js +0 -75
  49. package/tailwind.config.js +0 -8
  50. package/tests/build.test.js +0 -74
  51. package/tests/db-adapters.test.js +0 -108
  52. package/tests/env-support.test.js +0 -46
  53. package/tests/exec.test.js +0 -35
  54. package/tests/handlers-commit.test.js +0 -174
  55. package/tests/handlers-release.test.js +0 -212
  56. package/tests/handlers-workflow.test.js +0 -68
  57. package/tests/handlers.test.js +0 -1142
  58. package/tests/hello-world.test.js +0 -13
  59. package/tests/mcpRegistry.test.js +0 -65
  60. package/tests/patch-release.test.js +0 -7
  61. package/tests/release-wrapper.test.js +0 -107
  62. package/tests/startup-resilience.test.js +0 -73
  63. package/tests/test-helpers.js +0 -50
  64. package/tests/workflowState.test.js +0 -520
  65. package/web/Dockerfile.api +0 -25
  66. package/web/README.md +0 -133
  67. package/web/__tests__/api/workflow-history.test.js +0 -63
  68. package/web/api-server.js +0 -211
  69. package/web/app/docs/page.tsx +0 -202
  70. package/web/app/favicon.ico +0 -0
  71. package/web/app/globals.css +0 -91
  72. package/web/app/history/page.tsx +0 -220
  73. package/web/app/layout.tsx +0 -39
  74. package/web/app/login/page.tsx +0 -142
  75. package/web/app/page.tsx +0 -13
  76. package/web/components/ApiReference.tsx +0 -20
  77. package/web/components/Features.tsx +0 -61
  78. package/web/components/Footer.tsx +0 -9
  79. package/web/components/Hero.tsx +0 -96
  80. package/web/components/History.tsx +0 -175
  81. package/web/components/Navbar.tsx +0 -208
  82. package/web/components/Terminal.tsx +0 -114
  83. package/web/components/ThemeProvider.tsx +0 -58
  84. package/web/eslint.config.mjs +0 -18
  85. package/web/lib/api-client.ts +0 -123
  86. package/web/lib/auth.js +0 -75
  87. package/web/lib/db.js +0 -131
  88. package/web/lib/docs.js +0 -73
  89. package/web/middleware/auth.js +0 -26
  90. package/web/netlify.toml +0 -8
  91. package/web/next.config.ts +0 -19
  92. package/web/package-lock.json +0 -10911
  93. package/web/package.json +0 -42
  94. package/web/postcss.config.mjs +0 -9
  95. package/web/public/assets/android-chrome-192x192.png +0 -0
  96. package/web/public/assets/android-chrome-512x512.png +0 -0
  97. package/web/public/assets/apple-touch-icon.png +0 -0
  98. package/web/public/assets/favicon-16x16.png +0 -0
  99. package/web/public/assets/favicon-32x32.png +0 -0
  100. package/web/public/assets/favicon.ico +0 -0
  101. package/web/public/assets/favicon.png +0 -0
  102. package/web/public/assets/logo.png +0 -0
  103. package/web/public/assets/og-preview.png +0 -0
  104. package/web/public/assets/site.webmanifest +0 -1
  105. package/web/public/file.svg +0 -1
  106. package/web/public/globe.svg +0 -1
  107. package/web/public/next.svg +0 -1
  108. package/web/public/vercel.svg +0 -1
  109. package/web/public/window.svg +0 -1
  110. package/web/scripts/db-query-history.js +0 -120
  111. package/web/scripts/smoke-test.js +0 -30
  112. package/web/server.js +0 -184
  113. package/web/tailwind.config.ts +0 -20
  114. package/web/tsconfig.json +0 -42
@@ -1,14 +1,10 @@
1
- import { textResponse, isReleaseSatisfied } from "./shared.js";
2
- import { handleStartTask, handleMarkBugFixed, handleCreateTests, handleSkipTests, handleRunTests, handleCreateDocumentation } from "./workflow-handlers.js";
1
+ import { textResponse, isReleaseSatisfied, isErrorResponse } from "./shared.js";
2
+ import { handleStartTask, handleMarkBugFixed, handleCreateTests, handleSkipTests, handleRunTests, handleCreateDocumentation, handleCreateFeatureFlow } from "./workflow-handlers.js";
3
3
  import { handleSkipRelease, handlePerformRelease } from "./release-handlers.js";
4
4
  import { handleCommitAndPush, handleCompleteTask, handleForceCompleteTask, handleDropTask } from "./commit-handlers.js";
5
5
  import { handleProjectSummary, handleProjectSummaryData, handleProjectSummaryDb, handleViewHistory } from "./summary-handlers.js";
6
6
  import { handleGetWorkflowStatus, handleReadyCheck, handleContinueWorkflow, handleRerunWorkflow } from "./status-handlers.js";
7
-
8
- function isErrorResponse(response) {
9
- const text = response?.content?.[0]?.text || "";
10
- return /^\s*[⚠️❌]/u.test(text);
11
- }
7
+ import { handleListFactoryTasks, handleClaimFactoryTask, handleSyncTaskStatus, handleListFactoryProjects, handleCreateFactoryTask } from "./factory-handlers.js";
12
8
 
13
9
  function summarizeResponse(label, response) {
14
10
  const text = response?.content?.[0]?.text || "";
@@ -84,6 +80,15 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
84
80
  continue;
85
81
  }
86
82
 
83
+ if (!state.featureFlowCreated) {
84
+ if (!args.mermaidCode || !args.featureFlowDescription) {
85
+ return textResponse("⚠️ 'run_full_workflow' now requires 'mermaidCode' and 'featureFlowDescription' to support the Mermaid step.");
86
+ }
87
+ const resp = await executeStep("create_feature_flow", handleCreateFeatureFlow, [{ mermaidCode: args.mermaidCode, description: args.featureFlowDescription }, workflowState]);
88
+ if (isErrorResponse(resp)) return resp;
89
+ continue;
90
+ }
91
+
87
92
  if (!state.testsCreated) {
88
93
  const resp = await executeStep("create_tests", handleCreateTests, [workflowState]);
89
94
  if (isErrorResponse(resp)) return resp;
@@ -172,6 +177,8 @@ export async function handleToolCall({
172
177
  return handleStartTask(args, workflowState);
173
178
  case "mark_bug_fixed":
174
179
  return handleMarkBugFixed(args, workflowState);
180
+ case "create_feature_flow":
181
+ return handleCreateFeatureFlow(args, workflowState);
175
182
  case "create_tests":
176
183
  return handleCreateTests(workflowState);
177
184
  case "skip_tests":
@@ -210,6 +217,16 @@ export async function handleToolCall({
210
217
  return handleRerunWorkflow(workflowState);
211
218
  case "run_full_workflow":
212
219
  return handleRunFullWorkflow(args, { workflowState, exec, git, utils });
220
+ case "list_factory_tasks":
221
+ return handleListFactoryTasks(args);
222
+ case "list_factory_projects":
223
+ return handleListFactoryProjects(args);
224
+ case "create_factory_task":
225
+ return handleCreateFactoryTask(args);
226
+ case "claim_factory_task":
227
+ return handleClaimFactoryTask(args, { workflowState });
228
+ case "sync_task_status":
229
+ return handleSyncTaskStatus(args);
213
230
  default:
214
231
  throw new Error(`Unknown tool: ${name}`);
215
232
  }
@@ -21,6 +21,11 @@ export function isReleaseSatisfied(state) {
21
21
  return Boolean(state?.released || state?.releaseSkipped);
22
22
  }
23
23
 
24
+ export function isErrorResponse(response) {
25
+ const text = response?.content?.[0]?.text || "";
26
+ return /^\s*[⚠️❌]/u.test(text);
27
+ }
28
+
24
29
  export async function resetToCommitIfWorkingChanges(workflowState, git) {
25
30
  if (!workflowState.state.commitAndPushCompleted) {
26
31
  return false;
@@ -92,7 +97,7 @@ export function getContinueGuidance(status) {
92
97
  switch (status.currentPhase) {
93
98
  case "coding": {
94
99
  const task = status.taskDescription ? ` "${status.taskDescription}"` : "";
95
- return `Finish implementing${task} and then run 'mark_bug_fixed' with a brief summary of the change.`;
100
+ return `Describe your feature flow with Mermaid using 'create_feature_flow', then finish implementing${task} and run 'mark_bug_fixed' with a brief summary of the change.`;
96
101
  }
97
102
  case "testing":
98
103
  if (!status.testsCreated) {
@@ -127,45 +132,6 @@ export function getContinueGuidance(status) {
127
132
  }
128
133
  }
129
134
 
130
- export async function handleReadyCheck(workflowState) {
131
- const status = workflowState.state;
132
- const testsCreatedDone = status.testsSkipped || status.testsCreated;
133
- const testsPassedDone = status.testsSkipped || status.testsPassed;
134
- const checks = [
135
- { name: "Task started", done: status.currentPhase !== "idle" },
136
- { name: "Feature/bug fixed", done: status.bugFixed },
137
- {
138
- name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
139
- done: testsCreatedDone,
140
- },
141
- {
142
- name: status.testsSkipped ? "Tests skipped acknowledged" : "Tests passed",
143
- done: testsPassedDone,
144
- },
145
- { name: "Documentation created", done: status.documentationCreated },
146
- ];
147
-
148
- const allDone = checks.every((c) => c.done);
149
- const checkList = formatChecklist(checks);
150
-
151
- workflowState.state.readyCheckCompleted = allDone;
152
- if (allDone) {
153
- if (workflowState.state.commitAndPushCompleted) {
154
- workflowState.state.currentPhase = isReleaseSatisfied(workflowState.state) ? "ready_to_complete" : "release";
155
- } else {
156
- workflowState.state.currentPhase = "commit";
157
- }
158
- }
159
- await workflowState.save();
160
-
161
- if (allDone) {
162
- return textResponse(
163
- `🎉 ALL CHECKS PASSED!\n\n${checkList}\n\n✅ Next actions:\n1. Run 'commit_and_push' (commits and pushes your changes)\n2. Run 'perform_release' (handles versioning, tags, and final push)\n3. Finish with 'complete_task'\n\nTip: Provide the optional 'branch' argument to 'commit_and_push' to push to a non-default branch.\n\nTask: ${status.taskDescription}`
164
- );
165
- }
166
-
167
- return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
168
- }
169
135
 
170
136
  export async function loadProjectSummary(workflowState) {
171
137
  const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
@@ -180,7 +146,7 @@ export async function loadProjectSummary(workflowState) {
180
146
  }
181
147
  }
182
148
 
183
- export const prdPath = path.join(__dirname, "..", "docs", "product", "PRD.md");
149
+ export const prdPath = path.join(__dirname, "..", "..", "docs", "PRD.md");
184
150
 
185
151
  export function ensurePrdExists() {
186
152
  return existsSync(prdPath);
@@ -1,15 +1,36 @@
1
1
  import { textResponse, formatChecklist, isReleaseSatisfied, getContinueGuidance, resetToCommitIfWorkingChanges } from "./shared.js";
2
2
  import { handleCommitAndPush } from "./commit-handlers.js";
3
3
 
4
- export function handleGetWorkflowStatus(workflowState) {
4
+ export async function handleGetWorkflowStatus(workflowState) {
5
5
  const status = workflowState.state;
6
6
  const nextStep = status ? status.currentPhase : "idle";
7
7
 
8
+ // Deteksi berkas state yang terakhir ditulis proses MCP lain. Tanpa
9
+ // peringatan ini, agen bisa meng-commit atas nama task orang lain.
10
+ let sharedWarning = "";
11
+ try {
12
+ const disk = await workflowState.readDiskState();
13
+ if (workflowState.isForeignWrite(disk)) {
14
+ const owner = disk.owner || {};
15
+ sharedWarning =
16
+ `\n\n⚠️ PERINGATAN: berkas state ini terakhir ditulis proses MCP LAIN ` +
17
+ `(${owner.token || "tanpa-token"}, pid ${owner.pid ?? "?"}).\n` +
18
+ ` State workflow dipakai bersama antar sesi di mesin ini.\n` +
19
+ ` Periksa apakah "Task:" di atas benar-benar milik Anda SEBELUM commit_and_push.`;
20
+ if (disk.currentPhase && disk.currentPhase !== "idle" && disk.taskDescription) {
21
+ sharedWarning += `\n Proses itu sedang mengerjakan: "${disk.taskDescription}"`;
22
+ }
23
+ }
24
+ } catch {
25
+ // Status tetap ditampilkan walau pemeriksaan berkas gagal.
26
+ }
27
+
8
28
  return textResponse(
9
29
  `📊 Current Workflow Status:
10
30
 
11
31
  Phase: ${status.currentPhase}
12
32
  Task: ${status.taskDescription || "(none)"}
33
+ Feature flow: ${status.featureFlowCreated ? "✅" : "❌"}
13
34
  Bug fixed: ${status.bugFixed ? "✅" : "❌"}
14
35
  Tests created: ${status.testsCreated ? "✅" : "❌"}
15
36
  Tests passed: ${status.testsPassed ? "✅" : "❌"}
@@ -18,7 +39,7 @@ Ready check: ${status.readyCheckCompleted ? "✅" : "❌"}
18
39
  Commit & push: ${status.commitAndPushCompleted ? "✅" : "❌"}
19
40
  Released: ${status.released ? "✅" : "❌"}
20
41
 
21
- Next step: ${nextStep}`
42
+ Next step: ${nextStep}${sharedWarning}`
22
43
  );
23
44
  }
24
45
 
@@ -28,6 +49,7 @@ export async function handleReadyCheck(workflowState) {
28
49
  const testsPassedDone = status.testsSkipped || status.testsPassed;
29
50
  const checks = [
30
51
  { name: "Task started", done: status.currentPhase !== "idle" },
52
+ { name: "Feature flow described", done: status.featureFlowCreated },
31
53
  { name: "Feature/bug fixed", done: status.bugFixed },
32
54
  {
33
55
  name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
@@ -129,6 +151,6 @@ export async function handleRerunWorkflow(workflowState) {
129
151
  await workflowState.save();
130
152
 
131
153
  return textResponse(
132
- `🔄 Rerunning workflow from the start!\n\nTask: ${currentDescription}\nType: ${currentType}\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Run 'commit_and_push' (commits and pushes)\n8. ⏳ Run 'perform_release' (handles versioning and tags)\n9. ⏳ Complete task\n\n🎯 Be conscious about what you're coding!`
154
+ `🔄 Rerunning workflow from the start!\n\nTask: ${currentDescription}\nType: ${currentType}\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Describe feature flow with Mermaid\n3. ⏳ Fix/implement the feature\n4. ⏳ Create tests\n5. ⏳ Run tests (must pass!)\n6. ⏳ Create documentation\n7. ⏳ Run 'check_ready_to_commit'\n8. ⏳ Run 'commit_and_push' (commits and pushes)\n9. ⏳ Run 'perform_release' (handles versioning and tags)\n10. ⏳ Complete task\n\n🎯 Be conscious about what you're coding!`
133
155
  );
134
156
  }
@@ -1,9 +1,12 @@
1
- import path from "path";
2
- import { existsSync } from "fs";
3
- import { textResponse, isReleaseSatisfied, resetToCommitIfWorkingChanges } from "./shared.js";
1
+ import { textResponse, isReleaseSatisfied, isErrorResponse, ensurePrdExists } from "./shared.js";
4
2
 
5
3
  export async function handleStartTask(args, workflowState) {
6
- workflowState.reset();
4
+ // Selamatkan dulu pekerjaan yang mungkin tertimpa. Ini menangani dua kasus:
5
+ // 1. Proses MCP LAIN menulis berkas state setelah kita memuatnya.
6
+ // 2. Task kita sendiri yang belum selesai lalu diganti task baru.
7
+ const preservedBeforeSave = await workflowState.reconcileWithDisk();
8
+ const { archivedTask } = workflowState.reset({ archive: true });
9
+
7
10
  workflowState.state.currentPhase = "coding";
8
11
  workflowState.state.taskDescription = args.description;
9
12
  workflowState.state.taskType = args.type;
@@ -12,10 +15,34 @@ export async function handleStartTask(args, workflowState) {
12
15
  workflowState.state.commitAndPushCompleted = false;
13
16
  workflowState.state.lastCommitMessage = "";
14
17
  workflowState.state.lastPushBranch = "";
15
- await workflowState.save();
18
+ const reconciled = await workflowState.save();
19
+
20
+ const warnings = [];
21
+ const preservedForeign = reconciled?.preservedTask || preservedBeforeSave?.preservedTask;
22
+ if (preservedForeign) {
23
+ warnings.push(
24
+ `⚠️ Ada task yang SEDANG BERJALAN dari proses agen lain dan hampir tertimpa:\n` +
25
+ ` "${preservedForeign}"\n` +
26
+ ` Task itu sudah disimpan ke history sebagai 'interrupted', bukan dihapus.\n` +
27
+ ` State workflow di mesin ini dipakai bersama antar sesi - periksa sebelum commit.`
28
+ );
29
+ }
30
+ if (archivedTask) {
31
+ warnings.push(
32
+ `ℹ️ Task sebelumnya belum selesai dan diganti: "${archivedTask}".\n` +
33
+ ` Disimpan ke history sebagai 'interrupted'.`
34
+ );
35
+ }
36
+ if (reconciled?.mergedHistory > 0) {
37
+ warnings.push(
38
+ `ℹ️ ${reconciled.mergedHistory} entri history dari proses lain digabungkan, tidak ditimpa.`
39
+ );
40
+ }
41
+
42
+ const warningBlock = warnings.length > 0 ? `\n\n${warnings.join("\n\n")}` : "";
16
43
 
17
44
  return textResponse(
18
- `✅ Task Started: ${args.description}\n\n🎯 Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Fix/implement the feature\n3. ⏳ Create tests\n4. ⏳ Run tests (must pass!)\n5. ⏳ Create documentation\n6. ⏳ Run 'check_ready_to_commit'\n7. ⏳ Run 'commit_and_push' (commits and pushes)\n8. ⏳ Run 'perform_release' (handles versioning and tags) or 'skip_release' (when no release is needed)\n9. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
45
+ `✅ Task Started: ${args.description}${warningBlock}\n\n🎯 Be conscious about what you're coding!\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Describe feature flow with Mermaid\n3. ⏳ Fix/implement the feature\n4. ⏳ Create tests\n5. ⏳ Run tests (must pass!)\n6. ⏳ Create documentation\n7. ⏳ Run 'check_ready_to_commit'\n8. ⏳ Run 'commit_and_push' (commits and pushes)\n9. ⏳ Run 'perform_release' (handles versioning and tags) or 'skip_release' (when no release is needed)\n10. ⏳ Complete task\n\nReminder: Focus on writing clean, maintainable code!`
19
46
  );
20
47
  }
21
48
 
@@ -67,6 +94,29 @@ export async function handleMarkBugFixed(args, workflowState) {
67
94
  );
68
95
  }
69
96
 
97
+ export async function handleCreateFeatureFlow(args, workflowState) {
98
+ if (workflowState.state.currentPhase === "idle") {
99
+ return textResponse("⚠️ Please start a task first using 'start_task'!");
100
+ }
101
+
102
+ const mermaidCode = typeof args.mermaidCode === "string" ? args.mermaidCode.trim() : "";
103
+ const description = typeof args.description === "string" ? args.description.trim() : "";
104
+
105
+ if (!mermaidCode || !description) {
106
+ return textResponse("⚠️ Please provide both 'mermaidCode' and 'description' when creating a feature flow.");
107
+ }
108
+
109
+ workflowState.state.featureFlowCreated = true;
110
+ workflowState.state.mermaidCode = mermaidCode;
111
+ workflowState.state.featureFlowDescription = description;
112
+ workflowState.state.readyCheckCompleted = false;
113
+ await workflowState.save();
114
+
115
+ return textResponse(
116
+ `✅ Feature flow recorded!\n\nDescription: ${description}\n\nNext Steps:\n1. ✓ Start task\n2. ✓ Describe feature flow\n3. ⏳ Fix/implement the feature\n4. ⏳ Create tests\n5. ⏳ Run tests\n6. ⏳ Create documentation\n7. ⏳ Run 'check_ready_to_commit'\n8. ⏳ Run 'commit_and_push'\n9. ⏳ Run 'perform_release'\n10. ⏳ Complete task`
117
+ );
118
+ }
119
+
70
120
  export async function handleCreateTests(workflowState) {
71
121
  if (!workflowState.state.bugFixed) {
72
122
  return textResponse("⚠️ Please mark your feature/bug as fixed first using 'mark_bug_fixed'!");
@@ -190,10 +240,7 @@ export async function handleCreateDocumentation(args, workflowState) {
190
240
  return textResponse("⚠️ Please ensure tests are passing first! Run 'run_tests' with passed=true.");
191
241
  }
192
242
 
193
- const prdPath = path.join(process.cwd(), "docs", "PRD.md");
194
- const prdExists = existsSync(prdPath);
195
-
196
- if (!prdExists) {
243
+ if (!ensurePrdExists()) {
197
244
  return textResponse(
198
245
  `⚠️ PRD file not found!\n\nExpected location: docs/PRD.md\n\nPlease create or update the PRD before marking documentation as complete.`
199
246
  );
package/vite.config.js CHANGED
@@ -18,6 +18,12 @@ export default defineConfig({
18
18
  },
19
19
  external: [
20
20
  '@modelcontextprotocol/sdk',
21
+ // dotenv ships CommonJS only (dist/index.cjs). Bundling it into an ES
22
+ // module leaks its `require.main === module` guard, which throws
23
+ // "require is not defined in ES module scope" at startup. Keeping it
24
+ // external lets Node load it as CJS, which is how it is designed to run.
25
+ 'dotenv',
26
+ 'dotenv/config',
21
27
  // Node.js built-in modules
22
28
  'node:fs',
23
29
  'node:path',
package/workflow-state.js CHANGED
@@ -14,6 +14,22 @@ const USER_ID_FILENAME = "user-id";
14
14
  let cachedUserId = null;
15
15
  let cachedUserStateDir = null;
16
16
 
17
+ /**
18
+ * Identitas proses MCP ini.
19
+ *
20
+ * MENGAPA ADA
21
+ * -----------
22
+ * Berkas state disimpan per USER (`user-id`), bukan per sesi agen. Dua sesi
23
+ * opencode di mesin yang sama berbagi satu `workflow-state.json`. Akibatnya
24
+ * sesi B menimpa pekerjaan sesi A tanpa jejak: task A tidak pernah masuk
25
+ * `history` karena history hanya ditulis saat task DITUTUP.
26
+ *
27
+ * Token ini dicap ke setiap penulisan supaya kita bisa mengetahui bahwa isi
28
+ * berkas di disk terakhir ditulis oleh proses LAIN, lalu menyelamatkan task
29
+ * yang sedang berjalan itu sebelum kita menimpanya.
30
+ */
31
+ const PROCESS_TOKEN = `proc-${process.pid}-${Math.random().toString(16).slice(2, 10)}`;
32
+
17
33
  function generateUserId() {
18
34
  const timestamp = Date.now().toString(16);
19
35
  const randomSuffix = Math.random().toString(16).slice(2, 10);
@@ -447,6 +463,8 @@ export class WorkflowState {
447
463
  commitAndPushCompleted: false,
448
464
  lastCommitMessage: "",
449
465
  lastPushBranch: "",
466
+ featureFlowCreated: false,
467
+ featureFlowDescription: "",
450
468
  history: [],
451
469
  taskType: "",
452
470
  };
@@ -500,6 +518,12 @@ export class WorkflowState {
500
518
  if (typeof this.state.lastPushBranch !== "string") {
501
519
  this.state.lastPushBranch = "";
502
520
  }
521
+ if (typeof this.state.featureFlowCreated !== "boolean") {
522
+ this.state.featureFlowCreated = false;
523
+ }
524
+ if (typeof this.state.featureFlowDescription !== "string") {
525
+ this.state.featureFlowDescription = "";
526
+ }
503
527
  } catch (error) {
504
528
  if (error?.code !== "ENOENT") {
505
529
  throw error;
@@ -527,10 +551,119 @@ export class WorkflowState {
527
551
  }
528
552
 
529
553
  async save() {
554
+ const reconciled = await this.reconcileWithDisk();
555
+ this.state.owner = {
556
+ token: PROCESS_TOKEN,
557
+ pid: process.pid,
558
+ updatedAt: new Date().toISOString(),
559
+ };
530
560
  await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
531
561
  await ensureCompatibilityLinks(this.stateFile);
532
562
  await this.updateProjectSummary();
533
563
  await this.syncToDatabase();
564
+ this.lastReconcile = reconciled;
565
+ return reconciled;
566
+ }
567
+
568
+ /**
569
+ * Baca isi berkas state APA ADANYA dari disk (bukan dari memori).
570
+ * Mengembalikan null kalau belum ada / rusak.
571
+ */
572
+ async readDiskState() {
573
+ try {
574
+ const data = await fs.readFile(this.stateFile, "utf-8");
575
+ return JSON.parse(data);
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+
581
+ /** True kalau berkas di disk terakhir ditulis proses MCP lain. */
582
+ isForeignWrite(diskState) {
583
+ const token = diskState?.owner?.token;
584
+ return typeof token === "string" && token.length > 0 && token !== PROCESS_TOKEN;
585
+ }
586
+
587
+ historyKey(entry) {
588
+ return `${entry?.timestamp || ""}|${entry?.taskDescription || ""}`;
589
+ }
590
+
591
+ mergeDiskHistory(diskState) {
592
+ if (!diskState || !Array.isArray(diskState.history)) {
593
+ return 0;
594
+ }
595
+ if (!Array.isArray(this.state.history)) {
596
+ this.state.history = [];
597
+ }
598
+ const seen = new Set(this.state.history.map((entry) => this.historyKey(entry)));
599
+ let added = 0;
600
+ for (const entry of diskState.history) {
601
+ const key = this.historyKey(entry);
602
+ if (!seen.has(key)) {
603
+ this.state.history.push(entry);
604
+ seen.add(key);
605
+ added += 1;
606
+ }
607
+ }
608
+ return added;
609
+ }
610
+
611
+ /**
612
+ * Rekonsiliasi sebelum menulis: jangan buang pekerjaan proses lain.
613
+ *
614
+ * 1. Gabungkan `history` dari disk supaya catatan task lain tidak hilang.
615
+ * 2. Kalau disk memuat task yang SEDANG BERJALAN milik proses lain, arsipkan
616
+ * ke `history` sebagai `interrupted` sebelum kita menimpanya.
617
+ *
618
+ * Tanpa langkah 2, task itu lenyap tanpa jejak - itulah bug yang menyebabkan
619
+ * pekerjaan resize docker-alma tidak pernah muncul di view_history.
620
+ */
621
+ async reconcileWithDisk() {
622
+ const result = { mergedHistory: 0, preservedTask: null, foreignToken: null };
623
+ const disk = await this.readDiskState();
624
+ if (!disk) {
625
+ return result;
626
+ }
627
+
628
+ result.mergedHistory = this.mergeDiskHistory(disk);
629
+
630
+ if (!this.isForeignWrite(disk)) {
631
+ return result;
632
+ }
633
+
634
+ result.foreignToken = disk.owner.token;
635
+
636
+ const hasRunningTask =
637
+ typeof disk.taskDescription === "string" &&
638
+ disk.taskDescription.trim() !== "" &&
639
+ disk.currentPhase &&
640
+ disk.currentPhase !== "idle";
641
+
642
+ if (!hasRunningTask) {
643
+ return result;
644
+ }
645
+
646
+ const alreadyArchived = this.state.history.some(
647
+ (entry) => entry.interrupted && entry.taskDescription === disk.taskDescription
648
+ );
649
+ if (alreadyArchived) {
650
+ return result;
651
+ }
652
+
653
+ this.state.history.push({
654
+ taskDescription: disk.taskDescription,
655
+ taskType: disk.taskType || "other",
656
+ commitMessage: disk.lastCommitMessage || "",
657
+ interrupted: true,
658
+ dropped: true,
659
+ dropReason:
660
+ "Ditimpa proses agen lain: state workflow dipakai bersama antar sesi di mesin ini",
661
+ interruptedAtPhase: disk.currentPhase,
662
+ interruptedBy: disk.owner.token,
663
+ timestamp: new Date().toISOString(),
664
+ });
665
+ result.preservedTask = disk.taskDescription;
666
+ return result;
534
667
  }
535
668
 
536
669
  async updateProjectSummary() {
@@ -617,7 +750,43 @@ export class WorkflowState {
617
750
  };
618
751
  }
619
752
 
620
- reset() {
753
+ /**
754
+ * Reset ke keadaan idle.
755
+ *
756
+ * `archive` sengaja OPT-IN (default false). Pemanggil yang sudah menulis
757
+ * `addToHistory` sendiri - complete_task, force_complete_task, drop_task -
758
+ * akan menghasilkan entri ganda kalau reset ikut mengarsipkan.
759
+ *
760
+ * `start_task` memakai `{ archive: true }` supaya task yang sedang berjalan
761
+ * tidak lenyap saat diganti task baru.
762
+ */
763
+ reset(options = {}) {
764
+ const archive = options.archive === true;
765
+ const previous = this.state || {};
766
+ const history = Array.isArray(previous.history) ? previous.history.slice() : [];
767
+
768
+ const hasRunningTask =
769
+ typeof previous.taskDescription === "string" &&
770
+ previous.taskDescription.trim() !== "" &&
771
+ previous.currentPhase &&
772
+ previous.currentPhase !== "idle";
773
+
774
+ let archivedTask = null;
775
+ if (archive && hasRunningTask) {
776
+ archivedTask = previous.taskDescription;
777
+ history.push({
778
+ taskDescription: previous.taskDescription,
779
+ taskType: previous.taskType || "other",
780
+ commitMessage: previous.lastCommitMessage || "",
781
+ interrupted: true,
782
+ dropped: true,
783
+ dropReason:
784
+ options.reason || "Diganti start_task berikutnya sebelum task selesai",
785
+ interruptedAtPhase: previous.currentPhase,
786
+ timestamp: new Date().toISOString(),
787
+ });
788
+ }
789
+
621
790
  this.state = {
622
791
  currentPhase: "idle",
623
792
  taskDescription: "",
@@ -637,8 +806,13 @@ export class WorkflowState {
637
806
  commitAndPushCompleted: false,
638
807
  lastCommitMessage: "",
639
808
  lastPushBranch: "",
640
- history: this.state.history,
809
+ featureFlowCreated: false,
810
+ featureFlowDescription: "",
811
+ history,
812
+ taskType: "",
641
813
  };
814
+
815
+ return { archivedTask };
642
816
  }
643
817
 
644
818
  addToHistory(entry) {