@programinglive/dev-workflow-mcp-server 1.12.1 → 1.13.1
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/.dockerignore +24 -0
- package/.env.example +11 -31
- package/README.md +109 -6
- package/db/adapters/base.js +32 -0
- package/db/adapters/mysql.js +13 -7
- package/db/adapters/postgres.js +12 -7
- package/db/adapters/sqlite.js +17 -9
- package/db/index.js +2 -1
- package/docker-compose.yml +7 -5
- package/exec.js +78 -12
- package/factory-client.js +142 -10
- package/index.js +28 -19
- package/install.js +6 -5
- package/package.json +4 -1
- package/release-wrapper.js +32 -19
- package/scripts/check-review-guard.mjs +33 -0
- package/scripts/create-admin.js +11 -9
- package/scripts/install-husky.js +3 -2
- package/scripts/lint.mjs +20 -0
- package/scripts/release.cjs +25 -22
- package/scripts/release.js +6 -5
- package/scripts/setup-stack.js +42 -41
- package/scripts/update-release-notes.cjs +13 -12
- package/scripts/update-release-notes.js +8 -7
- package/scripts/verify-bundle.mjs +8 -7
- package/security/audit.js +86 -0
- package/security/commands.js +10 -0
- package/security/config.js +24 -0
- package/security/files.js +27 -0
- package/security/http.js +26 -0
- package/security/log-bridge.cjs +17 -0
- package/security/policy.js +37 -0
- package/security/references.js +21 -0
- package/security/runtime-secrets.js +42 -0
- package/security/sanitizer.js +42 -0
- package/security/scanner.js +108 -0
- package/tools/definitions.js +51 -3
- package/tools/handler-modules/commit-handlers.js +3 -7
- package/tools/handler-modules/factory-handlers.js +84 -0
- package/tools/handler-modules/index.js +78 -3
- package/tools/handler-modules/shared.js +5 -2
- package/tools/handler-modules/status-handlers.js +1 -1
- package/tools/handler-modules/summary-handlers.js +2 -1
- package/tools/handler-modules/workflow-handlers.js +6 -1
- package/utils.js +1 -0
- package/verify-build.js +7 -6
- package/vite.config.js +4 -0
- package/workflow-state.js +249 -34
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findTask,
|
|
7
7
|
claimTask,
|
|
8
8
|
updateTaskStatus,
|
|
9
|
+
updateTaskDetails,
|
|
9
10
|
createTask,
|
|
10
11
|
listProjects,
|
|
11
12
|
formatTaskList,
|
|
@@ -149,6 +150,7 @@ export async function handleClaimFactoryTask(args = {}, { workflowState } = {})
|
|
|
149
150
|
: "",
|
|
150
151
|
`Factory status is now running (work session ${session.id}).`,
|
|
151
152
|
task.projectName ? `Project: ${task.projectName}` : "",
|
|
153
|
+
task.definitionOfDone ? `Definition of done:\n${task.definitionOfDone}` : "",
|
|
152
154
|
"",
|
|
153
155
|
"Local workflow started for this task.",
|
|
154
156
|
"Use 'sync_task_status' with review, blocked or paused to close the session, or done to approve the task once it is in review.",
|
|
@@ -199,10 +201,23 @@ export async function handleSyncTaskStatus(args = {}) {
|
|
|
199
201
|
// ditolak server dan hanya mengaburkan keadaan papan.
|
|
200
202
|
stopKeeping();
|
|
201
203
|
|
|
204
|
+
// Checklist dibaca lagi setelah status berubah: DoD bisa saja dilengkapi
|
|
205
|
+
// sejak claim, dan agen perlu kriteria terbaru saat melaporkan selesai.
|
|
206
|
+
// Kegagalan bacaan tambahan ini tidak boleh membuat laporan status yang
|
|
207
|
+
// sudah berhasil terlihat gagal.
|
|
208
|
+
let definitionOfDone = "";
|
|
209
|
+
try {
|
|
210
|
+
const task = await findTask(config, recordId);
|
|
211
|
+
definitionOfDone = String(task?.definitionOfDone || "").trim();
|
|
212
|
+
} catch {
|
|
213
|
+
definitionOfDone = "";
|
|
214
|
+
}
|
|
215
|
+
|
|
202
216
|
return textResponse(
|
|
203
217
|
[
|
|
204
218
|
`Updated ${recordId} to ${result.status}.`,
|
|
205
219
|
result.session?.summary ? `Summary: ${result.session.summary}` : "",
|
|
220
|
+
definitionOfDone ? `Definition of done:\n${definitionOfDone}` : "",
|
|
206
221
|
result.status === "review" ? "Waiting for human approval in Factory." : "",
|
|
207
222
|
]
|
|
208
223
|
.filter(Boolean)
|
|
@@ -241,6 +256,7 @@ export async function handleCreateFactoryTask(args = {}) {
|
|
|
241
256
|
|
|
242
257
|
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
243
258
|
const acceptance = typeof args.acceptance === "string" ? args.acceptance.trim() : "";
|
|
259
|
+
const definitionOfDone = typeof args.definitionOfDone === "string" ? args.definitionOfDone.trim() : "";
|
|
244
260
|
|
|
245
261
|
if (!title) {
|
|
246
262
|
return textResponse("Provide 'title' for the task (3-140 characters).");
|
|
@@ -253,6 +269,17 @@ export async function handleCreateFactoryTask(args = {}) {
|
|
|
253
269
|
].join("\n"),
|
|
254
270
|
);
|
|
255
271
|
}
|
|
272
|
+
if (!definitionOfDone) {
|
|
273
|
+
return textResponse(
|
|
274
|
+
[
|
|
275
|
+
"Provide 'definitionOfDone': the checklist that must be true before this task can be marked done.",
|
|
276
|
+
"Use one condition per line, for example:",
|
|
277
|
+
"- [ ] build lulus",
|
|
278
|
+
"- [ ] halaman login terverifikasi",
|
|
279
|
+
"It is stored separately from 'acceptance', which also carries the ```run block for the worker.",
|
|
280
|
+
].join("\n"),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
256
283
|
if (!args.projectId) {
|
|
257
284
|
return textResponse(
|
|
258
285
|
"Provide 'projectId'. Run 'list_factory_projects' first - Factory needs a real project UUID.",
|
|
@@ -265,6 +292,7 @@ export async function handleCreateFactoryTask(args = {}) {
|
|
|
265
292
|
title,
|
|
266
293
|
description: args.description,
|
|
267
294
|
acceptance,
|
|
295
|
+
definitionOfDone,
|
|
268
296
|
priority: args.priority,
|
|
269
297
|
status: args.status,
|
|
270
298
|
});
|
|
@@ -286,3 +314,59 @@ export async function handleCreateFactoryTask(args = {}) {
|
|
|
286
314
|
return textResponse(`Could not create the task: ${error.message}`);
|
|
287
315
|
}
|
|
288
316
|
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Edits an existing task, mainly to fill the definition of done of a task that
|
|
320
|
+
* was created before Factory required it.
|
|
321
|
+
*
|
|
322
|
+
* Local validation runs first so an empty request never reaches the network.
|
|
323
|
+
*/
|
|
324
|
+
export async function handleUpdateFactoryTask(args = {}) {
|
|
325
|
+
const { config, response } = requireConfig();
|
|
326
|
+
if (!config) return response;
|
|
327
|
+
|
|
328
|
+
const recordId = typeof args.recordId === "string" ? args.recordId.trim() : "";
|
|
329
|
+
if (!recordId) {
|
|
330
|
+
return textResponse("Provide the 'recordId' of the task to update (see list_factory_tasks).");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const hasField = ["title", "description", "acceptance", "definitionOfDone"].some(
|
|
334
|
+
(field) => typeof args[field] === "string",
|
|
335
|
+
);
|
|
336
|
+
if (!hasField) {
|
|
337
|
+
return textResponse(
|
|
338
|
+
[
|
|
339
|
+
"Provide at least one field to update: title, description, acceptance or definitionOfDone.",
|
|
340
|
+
"To fill a missing definition of done, pass definitionOfDone as a checklist, one condition per line.",
|
|
341
|
+
].join("\n"),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const result = await updateTaskDetails(config, {
|
|
347
|
+
recordId,
|
|
348
|
+
title: args.title,
|
|
349
|
+
description: args.description,
|
|
350
|
+
acceptance: args.acceptance,
|
|
351
|
+
definitionOfDone: args.definitionOfDone,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return textResponse(
|
|
355
|
+
[
|
|
356
|
+
`Updated ${recordId}.`,
|
|
357
|
+
result.updatedAt ? `Task version is now ${result.updatedAt}.` : "",
|
|
358
|
+
]
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.join("\n"),
|
|
361
|
+
);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// Factory menjawab 409 kalau task berubah sejak dibaca. Menyebut ulang
|
|
364
|
+
// langkah yang harus diambil lebih berguna daripada kode HTTP saja.
|
|
365
|
+
if (error?.status === 409) {
|
|
366
|
+
return textResponse(
|
|
367
|
+
`Could not update the task: ${error.message}\nReload the task with list_factory_tasks and try again with the current version.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return textResponse(`Could not update the task: ${error.message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { textResponse, isReleaseSatisfied, isErrorResponse } from "./shared.js";
|
|
2
|
+
import { assertSafeKnowledge } from '../../security/policy.js';
|
|
3
|
+
import { safeLog, securityOutputFilter } from '../../security/sanitizer.js';
|
|
4
|
+
import { securityAudit } from '../../security/audit.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { assertSafeToolCommand } from '../../security/commands.js';
|
|
7
|
+
import { sessionKeyFromRequest } from "../../workflow-state.js";
|
|
2
8
|
import { handleStartTask, handleMarkBugFixed, handleCreateTests, handleSkipTests, handleRunTests, handleCreateDocumentation, handleCreateFeatureFlow } from "./workflow-handlers.js";
|
|
3
9
|
import { handleSkipRelease, handlePerformRelease } from "./release-handlers.js";
|
|
4
10
|
import { handleCommitAndPush, handleCompleteTask, handleForceCompleteTask, handleDropTask } from "./commit-handlers.js";
|
|
5
11
|
import { handleProjectSummary, handleProjectSummaryData, handleProjectSummaryDb, handleViewHistory } from "./summary-handlers.js";
|
|
6
12
|
import { handleGetWorkflowStatus, handleReadyCheck, handleContinueWorkflow, handleRerunWorkflow } from "./status-handlers.js";
|
|
7
|
-
import { handleListFactoryTasks, handleClaimFactoryTask, handleSyncTaskStatus, handleListFactoryProjects, handleCreateFactoryTask } from "./factory-handlers.js";
|
|
13
|
+
import { handleListFactoryTasks, handleClaimFactoryTask, handleSyncTaskStatus, handleListFactoryProjects, handleCreateFactoryTask, handleUpdateFactoryTask } from "./factory-handlers.js";
|
|
8
14
|
|
|
9
15
|
function summarizeResponse(label, response) {
|
|
10
16
|
const text = response?.content?.[0]?.text || "";
|
|
@@ -156,7 +162,7 @@ async function handleRunFullWorkflow(args, { workflowState, exec, git, utils })
|
|
|
156
162
|
return textResponse(message);
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
|
|
165
|
+
async function dispatchToolCall({
|
|
160
166
|
request,
|
|
161
167
|
normalizeRequestArgs,
|
|
162
168
|
workflowState,
|
|
@@ -166,6 +172,14 @@ export async function handleToolCall({
|
|
|
166
172
|
}) {
|
|
167
173
|
try {
|
|
168
174
|
const { name } = request.params;
|
|
175
|
+
|
|
176
|
+
// Identitas sesi datang dari request metadata, bukan dari environment.
|
|
177
|
+
// Tanpa ini, satu proses MCP yang dipakai bergantian oleh beberapa sesi
|
|
178
|
+
// akan berbagi satu state dan saling menimpa task.
|
|
179
|
+
const sessionId = sessionKeyFromRequest(request);
|
|
180
|
+
if (sessionId && workflowState?.useSession) {
|
|
181
|
+
await workflowState.useSession(sessionId);
|
|
182
|
+
}
|
|
169
183
|
const { args, error: argumentsError } = normalizeRequestArgs(request.params.arguments);
|
|
170
184
|
|
|
171
185
|
if (argumentsError) {
|
|
@@ -173,6 +187,14 @@ export async function handleToolCall({
|
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
switch (name) {
|
|
190
|
+
case 'security_audit': {
|
|
191
|
+
const file=workflowState?.stateFile;
|
|
192
|
+
const marker=file?.replaceAll('\\','/').lastIndexOf('/.state/');
|
|
193
|
+
const root=marker>=0 ? file.slice(0,marker) : file ? path.dirname(file) : process.cwd();
|
|
194
|
+
const userId=process.env.DEV_WORKFLOW_USER_ID || (marker>=0?path.basename(path.dirname(file)):'default');
|
|
195
|
+
const report=await securityAudit({root,state:workflowState?.state,stateFile:file,userId});
|
|
196
|
+
return textResponse(JSON.stringify(report));
|
|
197
|
+
}
|
|
176
198
|
case "start_task":
|
|
177
199
|
return handleStartTask(args, workflowState);
|
|
178
200
|
case "mark_bug_fixed":
|
|
@@ -223,6 +245,8 @@ export async function handleToolCall({
|
|
|
223
245
|
return handleListFactoryProjects(args);
|
|
224
246
|
case "create_factory_task":
|
|
225
247
|
return handleCreateFactoryTask(args);
|
|
248
|
+
case "update_factory_task":
|
|
249
|
+
return handleUpdateFactoryTask(args);
|
|
226
250
|
case "claim_factory_task":
|
|
227
251
|
return handleClaimFactoryTask(args, { workflowState });
|
|
228
252
|
case "sync_task_status":
|
|
@@ -231,9 +255,60 @@ export async function handleToolCall({
|
|
|
231
255
|
throw new Error(`Unknown tool: ${name}`);
|
|
232
256
|
}
|
|
233
257
|
} catch (error) {
|
|
234
|
-
|
|
258
|
+
safeLog('error', "Tool handler error:", error);
|
|
235
259
|
return textResponse(
|
|
236
260
|
"⚠️ Internal server error while processing the tool request. Please try again."
|
|
237
261
|
);
|
|
238
262
|
}
|
|
239
263
|
}
|
|
264
|
+
|
|
265
|
+
// Await every handler here so asynchronous rejections cannot bypass egress checks.
|
|
266
|
+
export async function handleToolCall(context) {
|
|
267
|
+
try {
|
|
268
|
+
const {args,error}=context.normalizeRequestArgs(context.request.params.arguments);
|
|
269
|
+
if(error) return securityOutputFilter(textResponse(error));
|
|
270
|
+
assertSafeKnowledge({tool:context.request.params.name,args});
|
|
271
|
+
assertSafeToolCommand(context.request.params.name,args);
|
|
272
|
+
if (context.workflowState) context.workflowState.lastConflict = null;
|
|
273
|
+
const response=await dispatchToolCall({...context,request:{...context.request,params:{...context.request.params,arguments:args}}});
|
|
274
|
+
return securityOutputFilter(withConflictNotice(response, context.workflowState));
|
|
275
|
+
} catch(error) {
|
|
276
|
+
if(['SECRET_CONTENT_DENIED','UNSAFE_COMMAND_DENIED'].includes(error.code)) {
|
|
277
|
+
return {isError:true,content:[{type:'text',text:JSON.stringify({error:error.code,message:error.message,findings:error.findings})}]};
|
|
278
|
+
}
|
|
279
|
+
safeLog('error','Tool request failed',error);
|
|
280
|
+
return {isError:true,content:[{type:'text',text:'⚠️ Internal server error while processing the tool request.'}]};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Tempelkan peringatan bila penyimpanan terakhir kalah dari proses lain.
|
|
286
|
+
*
|
|
287
|
+
* Tanpa ini, tool menjawab "berhasil" untuk state yang sebenarnya tidak
|
|
288
|
+
* tersimpan. Kejadian 29 Okt 2026: `start_task` menjawab "✅ Task Started",
|
|
289
|
+
* tetapi `get_workflow_status` berikutnya menampilkan task lain.
|
|
290
|
+
*/
|
|
291
|
+
function withConflictNotice(response, workflowState) {
|
|
292
|
+
const conflict = workflowState?.lastConflict;
|
|
293
|
+
if (!conflict) {
|
|
294
|
+
return response;
|
|
295
|
+
}
|
|
296
|
+
const text = response?.content?.[0]?.text;
|
|
297
|
+
if (typeof text !== "string") {
|
|
298
|
+
return response;
|
|
299
|
+
}
|
|
300
|
+
// Peringatan ditaruh DI DEPAN, dan menyebut terus terang bahwa aksi tadi
|
|
301
|
+
// tidak berlaku. Menempelkan "✅ berhasil" di atas peringatan membuat agen
|
|
302
|
+
// membaca baris pertama lalu melanjutkan seolah semuanya tersimpan.
|
|
303
|
+
const notice =
|
|
304
|
+
`⚠️ AKSI TIDAK BERLAKU - proses MCP lain memulai task yang lebih baru.\n` +
|
|
305
|
+
` Task di disk : "${conflict.diskTask}"\n` +
|
|
306
|
+
` Task Anda : "${conflict.staleTask || '(kosong)'}"\n` +
|
|
307
|
+
` State Anda sudah dimuat ulang dari disk, jadi langkah berikutnya mengikuti task di disk.\n` +
|
|
308
|
+
` Jalankan ulang aksi ini bila memang untuk task tersebut, atau 'start_task' untuk menggantinya.\n\n` +
|
|
309
|
+
`--- pesan asli tool (TIDAK tersimpan) ---\n`;
|
|
310
|
+
return {
|
|
311
|
+
...response,
|
|
312
|
+
content: [{ ...response.content[0], text: notice + text }, ...response.content.slice(1)],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { sanitize } from '../../security/sanitizer.js';
|
|
5
6
|
|
|
6
7
|
export const PROJECT_SUMMARY_FILENAME = "project-summary.json";
|
|
7
8
|
|
|
@@ -12,7 +13,7 @@ export const textResponse = (text) => ({
|
|
|
12
13
|
content: [
|
|
13
14
|
{
|
|
14
15
|
type: "text",
|
|
15
|
-
text,
|
|
16
|
+
text: sanitize(text),
|
|
16
17
|
},
|
|
17
18
|
],
|
|
18
19
|
});
|
|
@@ -54,6 +55,7 @@ export function formatChecklist(checks) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function summarizeHistory(history, limit) {
|
|
58
|
+
history = sanitize(history);
|
|
57
59
|
const recent = history.slice(-limit).reverse();
|
|
58
60
|
if (recent.length === 0) {
|
|
59
61
|
return null;
|
|
@@ -70,6 +72,7 @@ export function summarizeHistory(history, limit) {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export function generateProjectSummary(history) {
|
|
75
|
+
history = sanitize(history);
|
|
73
76
|
if (!history || history.length === 0) {
|
|
74
77
|
return "No workflow history available.";
|
|
75
78
|
}
|
|
@@ -137,7 +140,7 @@ export async function loadProjectSummary(workflowState) {
|
|
|
137
140
|
const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
|
|
138
141
|
try {
|
|
139
142
|
const data = await fs.readFile(summaryPath, "utf-8");
|
|
140
|
-
return JSON.parse(data);
|
|
143
|
+
return sanitize(JSON.parse(data));
|
|
141
144
|
} catch (error) {
|
|
142
145
|
if (error?.code === "ENOENT") {
|
|
143
146
|
return null;
|
|
@@ -14,7 +14,7 @@ export async function handleGetWorkflowStatus(workflowState) {
|
|
|
14
14
|
const owner = disk.owner || {};
|
|
15
15
|
sharedWarning =
|
|
16
16
|
`\n\n⚠️ PERINGATAN: berkas state ini terakhir ditulis proses MCP LAIN ` +
|
|
17
|
-
`(${owner.
|
|
17
|
+
`(${owner.processMarker || "tanpa-token"}, pid ${owner.pid ?? "?"}).\n` +
|
|
18
18
|
` State workflow dipakai bersama antar sesi di mesin ini.\n` +
|
|
19
19
|
` Periksa apakah "Task:" di atas benar-benar milik Anda SEBELUM commit_and_push.`;
|
|
20
20
|
if (disk.currentPhase && disk.currentPhase !== "idle" && disk.taskDescription) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { sanitize } from '../../security/sanitizer.js';
|
|
3
4
|
import { textResponse, generateProjectSummary, summarizeHistory, PROJECT_SUMMARY_FILENAME } from "./shared.js";
|
|
4
5
|
|
|
5
6
|
export function handleProjectSummary(workflowState) {
|
|
@@ -11,7 +12,7 @@ export async function handleProjectSummaryData(workflowState) {
|
|
|
11
12
|
const summaryPath = path.join(path.dirname(workflowState.stateFile), PROJECT_SUMMARY_FILENAME);
|
|
12
13
|
try {
|
|
13
14
|
const data = await fs.readFile(summaryPath, "utf-8");
|
|
14
|
-
const parsed = JSON.parse(data);
|
|
15
|
+
const parsed = sanitize(JSON.parse(data));
|
|
15
16
|
const formatted = `🧠 Project Knowledge Summary (from persisted data)\n\n📊 Stats\n- Total tasks completed: ${parsed.totalTasks}\n- Recent task mix: ${Object.entries(parsed.taskTypes).map(([t, c]) => `${t}: ${c}`).join(", ")}\n- Last active: ${parsed.lastActive ? new Date(parsed.lastActive).toLocaleDateString() : "Never"}\n- Updated: ${new Date(parsed.updatedAt).toLocaleString()}\n\n📝 Recent tasks (last 5)\n${parsed.recentTasks.map((e, i) => `${i + 1}. ${e.description} (${e.type})`).join("\n")}`;
|
|
16
17
|
return textResponse(formatted);
|
|
17
18
|
} catch (error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classify } from '../../security/policy.js';
|
|
1
2
|
import { textResponse, isReleaseSatisfied, isErrorResponse, ensurePrdExists } from "./shared.js";
|
|
2
3
|
|
|
3
4
|
export async function handleStartTask(args, workflowState) {
|
|
@@ -8,6 +9,7 @@ export async function handleStartTask(args, workflowState) {
|
|
|
8
9
|
const { archivedTask } = workflowState.reset({ archive: true });
|
|
9
10
|
|
|
10
11
|
workflowState.state.currentPhase = "coding";
|
|
12
|
+
workflowState.state.classification = classify(args);
|
|
11
13
|
workflowState.state.taskDescription = args.description;
|
|
12
14
|
workflowState.state.taskType = args.type;
|
|
13
15
|
workflowState.state.testsSkipped = false;
|
|
@@ -15,7 +17,10 @@ export async function handleStartTask(args, workflowState) {
|
|
|
15
17
|
workflowState.state.commitAndPushCompleted = false;
|
|
16
18
|
workflowState.state.lastCommitMessage = "";
|
|
17
19
|
workflowState.state.lastPushBranch = "";
|
|
18
|
-
|
|
20
|
+
// start_task memang bermaksud MENGGANTI task, jadi ia boleh menang atas
|
|
21
|
+
// task berjalan. Task yang diganti tetap diarsipkan oleh reconcileWithDisk,
|
|
22
|
+
// bukan dihapus.
|
|
23
|
+
const reconciled = await workflowState.save({ allowReplaceRunningTask: true });
|
|
19
24
|
|
|
20
25
|
const warnings = [];
|
|
21
26
|
const preservedForeign = reconciled?.preservedTask || preservedBeforeSave?.preservedTask;
|
package/utils.js
CHANGED
|
@@ -3,6 +3,7 @@ const isWindows = process.platform === "win32";
|
|
|
3
3
|
export function shellEscape(value) {
|
|
4
4
|
const stringValue = String(value);
|
|
5
5
|
if (isWindows) {
|
|
6
|
+
if(/["%!\r\n]/.test(stringValue)) throw new Error('Unsafe Windows shell argument denied; use a trusted operator session for complex messages.');
|
|
6
7
|
// Windows cmd.exe requires double quotes for arguments with spaces.
|
|
7
8
|
// Escape existing double quotes with backslash.
|
|
8
9
|
return `"${stringValue.replace(/"/g, '\\"')}"`;
|
package/verify-build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { safeLog } from './security/sanitizer.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build Verification Script
|
|
@@ -22,16 +23,16 @@ let failedTests = 0;
|
|
|
22
23
|
function test(name, fn) {
|
|
23
24
|
try {
|
|
24
25
|
fn();
|
|
25
|
-
|
|
26
|
+
safeLog('info', `✓ ${name}`);
|
|
26
27
|
passedTests++;
|
|
27
28
|
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
safeLog('error', `✗ ${name}`);
|
|
30
|
+
safeLog('error', ` Error: ${error.message}`);
|
|
30
31
|
failedTests++;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
safeLog('info', '\n🧪 Running Build Verification Tests...\n');
|
|
35
36
|
|
|
36
37
|
// Test 1: Build succeeds
|
|
37
38
|
test('Build completes successfully', () => {
|
|
@@ -166,11 +167,11 @@ test('Vertical monitor responsive design', () => {
|
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
safeLog('info', `\n📊 Test Results: ${passedTests} passed, ${failedTests} failed\n`);
|
|
170
171
|
|
|
171
172
|
if (failedTests > 0) {
|
|
172
173
|
process.exit(1);
|
|
173
174
|
} else {
|
|
174
|
-
|
|
175
|
+
safeLog('info', '✅ All tests passed!\n');
|
|
175
176
|
process.exit(0);
|
|
176
177
|
}
|
package/vite.config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { builtinModules } from 'node:module';
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = path.dirname(__filename);
|
|
@@ -17,6 +18,9 @@ export default defineConfig({
|
|
|
17
18
|
format: 'es',
|
|
18
19
|
},
|
|
19
20
|
external: [
|
|
21
|
+
...builtinModules,
|
|
22
|
+
...builtinModules.map(name=>`node:${name}`),
|
|
23
|
+
'pg','mysql2','mysql2/promise','better-sqlite3',
|
|
20
24
|
'@modelcontextprotocol/sdk',
|
|
21
25
|
// dotenv ships CommonJS only (dist/index.cjs). Bundling it into an ES
|
|
22
26
|
// module leaks its `require.main === module` guard, which throws
|