@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.
- package/.env.example +21 -1
- package/AGENTS.md +25 -0
- package/README.md +372 -649
- package/factory-client.js +554 -0
- package/git-facade.js +35 -0
- package/index.js +9 -34
- package/package.json +21 -20
- package/scripts/bootstrap-unix.sh +55 -0
- package/scripts/bootstrap-windows.ps1 +75 -0
- package/scripts/install-husky.js +43 -0
- package/scripts/setup-stack.js +436 -0
- package/scripts/setup-test-dbs.sh +61 -0
- package/scripts/test-db-adapters.sh +71 -0
- package/scripts/verify-bundle.mjs +92 -0
- package/tools/definitions.js +143 -0
- package/tools/handler-modules/commit-handlers.js +2 -1
- package/tools/handler-modules/factory-handlers.js +276 -0
- package/tools/handler-modules/index.js +24 -7
- package/tools/handler-modules/shared.js +7 -41
- package/tools/handler-modules/status-handlers.js +25 -3
- package/tools/handler-modules/workflow-handlers.js +57 -10
- package/vite.config.js +6 -0
- package/workflow-state.js +176 -2
- package/.agent/workflows/dev-workflow-bugfix.md +0 -250
- package/.agent/workflows/dev-workflow-feature.md +0 -201
- package/.agent/workflows/dev-workflow-full-cycle.md +0 -400
- package/.agent/workflows/dev-workflow-refactor.md +0 -238
- package/.env.production +0 -16
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/pull_request_template.md +0 -22
- package/.github/workflows/github-release.yml +0 -131
- package/.husky/commit-msg +0 -3
- package/.husky/pre-commit +0 -2
- package/CHANGELOG.md +0 -1145
- package/docs/GCP_DEPLOYMENT.md +0 -298
- package/docs/NETLIFY_DEPLOYMENT.md +0 -125
- package/docs/PLESK_DEPLOYMENT.md +0 -170
- package/docs/PRD.md +0 -86
- package/docs/WORKFLOW-BEST-PRACTICES.md +0 -58
- package/docs/antigravity/EXAMPLES.md +0 -535
- package/docs/antigravity/GETTING-STARTED.md +0 -292
- package/docs/antigravity/TROUBLESHOOTING.md +0 -742
- package/docs/antigravity/WALKTHROUGH.md +0 -25
- package/docs/antigravity/WORKFLOW-TEMPLATES.md +0 -670
- package/docs/release-notes/RELEASE_NOTES.md +0 -524
- package/docs/web-dashboard.md +0 -175
- package/scripts/test-auth-flow.js +0 -75
- package/tailwind.config.js +0 -8
- package/tests/build.test.js +0 -74
- package/tests/db-adapters.test.js +0 -108
- package/tests/env-support.test.js +0 -46
- package/tests/exec.test.js +0 -35
- package/tests/handlers-commit.test.js +0 -174
- package/tests/handlers-release.test.js +0 -212
- package/tests/handlers-workflow.test.js +0 -68
- package/tests/handlers.test.js +0 -1142
- package/tests/hello-world.test.js +0 -13
- package/tests/mcpRegistry.test.js +0 -65
- package/tests/patch-release.test.js +0 -7
- package/tests/release-wrapper.test.js +0 -107
- package/tests/startup-resilience.test.js +0 -73
- package/tests/test-helpers.js +0 -50
- package/tests/workflowState.test.js +0 -520
- package/web/Dockerfile.api +0 -25
- package/web/README.md +0 -133
- package/web/__tests__/api/workflow-history.test.js +0 -63
- package/web/api-server.js +0 -211
- package/web/app/docs/page.tsx +0 -202
- package/web/app/favicon.ico +0 -0
- package/web/app/globals.css +0 -91
- package/web/app/history/page.tsx +0 -220
- package/web/app/layout.tsx +0 -39
- package/web/app/login/page.tsx +0 -142
- package/web/app/page.tsx +0 -13
- package/web/components/ApiReference.tsx +0 -20
- package/web/components/Features.tsx +0 -61
- package/web/components/Footer.tsx +0 -9
- package/web/components/Hero.tsx +0 -96
- package/web/components/History.tsx +0 -175
- package/web/components/Navbar.tsx +0 -208
- package/web/components/Terminal.tsx +0 -114
- package/web/components/ThemeProvider.tsx +0 -58
- package/web/eslint.config.mjs +0 -18
- package/web/lib/api-client.ts +0 -123
- package/web/lib/auth.js +0 -75
- package/web/lib/db.js +0 -131
- package/web/lib/docs.js +0 -73
- package/web/middleware/auth.js +0 -26
- package/web/netlify.toml +0 -8
- package/web/next.config.ts +0 -19
- package/web/package-lock.json +0 -10911
- package/web/package.json +0 -42
- package/web/postcss.config.mjs +0 -9
- package/web/public/assets/android-chrome-192x192.png +0 -0
- package/web/public/assets/android-chrome-512x512.png +0 -0
- package/web/public/assets/apple-touch-icon.png +0 -0
- package/web/public/assets/favicon-16x16.png +0 -0
- package/web/public/assets/favicon-32x32.png +0 -0
- package/web/public/assets/favicon.ico +0 -0
- package/web/public/assets/favicon.png +0 -0
- package/web/public/assets/logo.png +0 -0
- package/web/public/assets/og-preview.png +0 -0
- package/web/public/assets/site.webmanifest +0 -1
- package/web/public/file.svg +0 -1
- package/web/public/globe.svg +0 -1
- package/web/public/next.svg +0 -1
- package/web/public/vercel.svg +0 -1
- package/web/public/window.svg +0 -1
- package/web/scripts/db-query-history.js +0 -120
- package/web/scripts/smoke-test.js +0 -30
- package/web/server.js +0 -184
- package/web/tailwind.config.ts +0 -20
- 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 `
|
|
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, "..", "
|
|
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\
|
|
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
|
|
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
|
-
|
|
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\
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
+
featureFlowCreated: false,
|
|
810
|
+
featureFlowDescription: "",
|
|
811
|
+
history,
|
|
812
|
+
taskType: "",
|
|
641
813
|
};
|
|
814
|
+
|
|
815
|
+
return { archivedTask };
|
|
642
816
|
}
|
|
643
817
|
|
|
644
818
|
addToHistory(entry) {
|