@programinglive/dev-workflow-mcp-server 1.13.15 → 1.14.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.
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Aturan Definition of Done, sebagai fungsi murni.
3
+ *
4
+ * Semua di sini sengaja tanpa MCP dan tanpa filesystem: aturan "kapan sebuah
5
+ * task benar-benar selesai" harus bisa dibaca dan diuji tanpa menyalakan
6
+ * server. Handler di `dod-handlers.js` hanya memvalidasi, mengubah state, lalu
7
+ * menyimpan.
8
+ *
9
+ * MENGAPA PER ITEM, BUKAN SATU TEKS
10
+ * ---------------------------------
11
+ * Factory menyimpan DoD sebagai teks checklist. Itu cukup untuk gerbang
12
+ * "tidak boleh kosong", tetapi tidak bisa membedakan "sudah dibuktikan" dari
13
+ * "ditulis lalu dilupakan". Di sini tiap item punya status sendiri, bukti
14
+ * sendiri, dan revisi saat ia dibuktikan.
15
+ */
16
+
17
+ export const MAX_ITEMS = 20;
18
+ export const MIN_TEXT = 3;
19
+ export const MAX_TEXT = 300;
20
+ export const MIN_EVIDENCE = 5;
21
+ export const MAX_EVIDENCE = 500;
22
+
23
+ const STATUSES = new Set(["open", "done", "waived"]);
24
+
25
+ /** Peringkat status untuk penggabungan antar-proses: makin tinggi makin kuat. */
26
+ const STATUS_RANK = { open: 0, waived: 1, done: 2 };
27
+
28
+ function clampText(value, limit = MAX_TEXT) {
29
+ const text = String(value ?? "").trim();
30
+ return text.length > limit ? text.slice(0, limit) : text;
31
+ }
32
+
33
+ /**
34
+ * Validasi daftar teks DoD yang datang dari agen.
35
+ *
36
+ * Dipakai `start_task` dan `set_definition_of_done`. Menolak dengan pesan yang
37
+ * menyebut aturannya, bukan sekadar "invalid input", karena agen yang ditolak
38
+ * harus tahu persis apa yang harus diperbaiki.
39
+ */
40
+ export function validateItemTexts(texts) {
41
+ if (!Array.isArray(texts)) {
42
+ return {
43
+ ok: false,
44
+ error:
45
+ "definitionOfDone harus berupa daftar teks, satu kondisi per item, " +
46
+ 'contoh: ["build lulus", "halaman login terverifikasi"].',
47
+ };
48
+ }
49
+
50
+ if (texts.length === 0) {
51
+ return {
52
+ ok: false,
53
+ error:
54
+ "definitionOfDone wajib berisi minimal satu item: kondisi konkret yang harus " +
55
+ "benar sebelum task boleh dianggap selesai.",
56
+ };
57
+ }
58
+
59
+ if (texts.length > MAX_ITEMS) {
60
+ return {
61
+ ok: false,
62
+ error: `definitionOfDone maksimal ${MAX_ITEMS} item; dikirim ${texts.length}. Gabungkan kondisi yang sejenis.`,
63
+ };
64
+ }
65
+
66
+ const items = [];
67
+ const seen = new Map();
68
+
69
+ for (const [index, raw] of texts.entries()) {
70
+ const text = clampText(raw);
71
+
72
+ if (text === "") {
73
+ return { ok: false, error: `Item DoD ke-${index + 1} kosong. Setiap item harus berisi kondisi yang bisa dibuktikan.` };
74
+ }
75
+
76
+ if (text.length < MIN_TEXT) {
77
+ return {
78
+ ok: false,
79
+ error: `Item DoD "${text}" terlalu pendek (minimal ${MIN_TEXT} karakter). Tulis kondisi yang bisa diperiksa, contoh: "build lulus".`,
80
+ };
81
+ }
82
+
83
+ if (String(raw ?? "").trim().length > MAX_TEXT) {
84
+ return {
85
+ ok: false,
86
+ error: `Item DoD ke-${index + 1} lebih dari ${MAX_TEXT} karakter. Ringkas jadi satu kondisi.`,
87
+ };
88
+ }
89
+
90
+ const key = text.toLowerCase();
91
+ if (seen.has(key)) {
92
+ return {
93
+ ok: false,
94
+ error: `Item DoD "${text}" duplikat dengan item ke-${seen.get(key) + 1}. Setiap kondisi hanya boleh muncul sekali.`,
95
+ };
96
+ }
97
+ seen.set(key, index);
98
+ items.push(text);
99
+ }
100
+
101
+ return { ok: true, items };
102
+ }
103
+
104
+ /**
105
+ * Normalisasi state yang dibaca dari disk.
106
+ *
107
+ * Berbeda dari `validateItemTexts`: di sini tujuannya JANGAN gagal. Berkas state
108
+ * ditulis versi lama, atau oleh proses lain yang sedang berjalan; satu entri
109
+ * rusak tidak boleh membuat seluruh workflow tidak bisa dimuat. Entri yang tidak
110
+ * bisa diselamatkan dibuang, sisanya diperbaiki.
111
+ */
112
+ export function normalizeItems(raw) {
113
+ if (!Array.isArray(raw)) {
114
+ return [];
115
+ }
116
+
117
+ const items = [];
118
+ const usedIds = new Set();
119
+
120
+ for (const entry of raw) {
121
+ const source = typeof entry === "string" ? { text: entry } : entry && typeof entry === "object" ? entry : null;
122
+ if (!source) continue;
123
+
124
+ const text = clampText(source.text);
125
+ if (text === "") continue;
126
+ if (items.length >= MAX_ITEMS) break;
127
+
128
+ let id = typeof source.id === "string" ? source.id.trim() : "";
129
+ if (id === "" || usedIds.has(id)) {
130
+ id = nextItemId(items.concat(Array.from(usedIds, (value) => ({ id: value }))));
131
+ }
132
+ usedIds.add(id);
133
+
134
+ const status = STATUSES.has(source.status) ? source.status : "open";
135
+ const revision = Number.isFinite(Number(source.revision)) && Number(source.revision) >= 0
136
+ ? Math.trunc(Number(source.revision))
137
+ : 0;
138
+
139
+ items.push({
140
+ id,
141
+ text,
142
+ status,
143
+ evidence: status === "done" ? clampText(source.evidence, MAX_EVIDENCE) : "",
144
+ waivedReason: status === "waived" ? clampText(source.waivedReason, MAX_EVIDENCE) : "",
145
+ revision,
146
+ updatedAt: typeof source.updatedAt === "string" ? source.updatedAt : "",
147
+ });
148
+ }
149
+
150
+ return items;
151
+ }
152
+
153
+ /**
154
+ * Membaca checklist papan Factory menjadi daftar teks.
155
+ *
156
+ * Butir `- [x]` tetap diimpor sebagai item BIASA (status `open` di sisi
157
+ * pemanggil): checklist papan adalah rencana kerja, bukan bukti. Kalau kotak
158
+ * centang papan dipercaya apa adanya, agen bisa mewarisi klaim orang lain tanpa
159
+ * pernah menjalankan apa pun di mesin ini.
160
+ */
161
+ export function parseFactoryChecklist(text) {
162
+ const lines = String(text ?? "").split(/\r?\n/);
163
+ const items = [];
164
+ const seen = new Set();
165
+ let truncated = false;
166
+
167
+ for (const line of lines) {
168
+ if (items.length >= MAX_ITEMS) {
169
+ truncated = true;
170
+ break;
171
+ }
172
+
173
+ const trimmed = line.trim();
174
+ if (trimmed === "") continue;
175
+ if (/^#{1,6}\s/.test(trimmed)) continue;
176
+
177
+ const checkbox = trimmed.match(/^[-*+]\s*\[[ xX]\]\s*(.+)$/);
178
+ const bullet = checkbox ? null : trimmed.match(/^[-*+]\s+(.+)$/);
179
+ const captured = checkbox?.[1] ?? bullet?.[1];
180
+ if (!captured) continue;
181
+
182
+ let item = captured.trim();
183
+ if (item === "") continue;
184
+
185
+ if (item.length > MAX_TEXT) {
186
+ item = item.slice(0, MAX_TEXT);
187
+ truncated = true;
188
+ }
189
+
190
+ const key = item.toLowerCase();
191
+ if (seen.has(key)) {
192
+ truncated = true;
193
+ continue;
194
+ }
195
+ seen.add(key);
196
+ items.push(item);
197
+ }
198
+
199
+ return { items, truncated };
200
+ }
201
+
202
+ /**
203
+ * Apakah satu item sudah memenuhi syaratnya.
204
+ *
205
+ * Item `done` hanya memenuhi bila dibuktikan pada revisi yang sedang berlaku.
206
+ * Saat perubahan kerja baru muncul setelah commit, `dodRevision` naik dan bukti
207
+ * lama otomatis tidak lagi berlaku - tanpa perlu menulis ulang item mana pun.
208
+ * Item `waived` tetap memenuhi, karena waiver adalah keputusan, bukan bukti.
209
+ */
210
+ export function isSatisfied(item, revision) {
211
+ if (!item) return false;
212
+ if (item.status === "waived") return true;
213
+ if (item.status !== "done") return false;
214
+ return Number(item.revision) === Number(revision);
215
+ }
216
+
217
+ function maxItemNumber(items) {
218
+ let max = 0;
219
+ for (const item of items) {
220
+ const match = /^dod-(\d+)$/.exec(String(item?.id ?? ""));
221
+ if (match) max = Math.max(max, Number(match[1]));
222
+ }
223
+ return max;
224
+ }
225
+
226
+ export function nextItemId(items) {
227
+ return `dod-${maxItemNumber(items) + 1}`;
228
+ }
229
+
230
+ export function openItems(items, revision) {
231
+ return (Array.isArray(items) ? items : []).filter((item) => !isSatisfied(item, revision));
232
+ }
233
+
234
+ export function summarize(items, revision) {
235
+ const list = Array.isArray(items) ? items : [];
236
+ const waivedReasons = [];
237
+ let done = 0;
238
+ let waived = 0;
239
+
240
+ for (const item of list) {
241
+ if (item.status === "waived") {
242
+ waived += 1;
243
+ if (item.waivedReason) waivedReasons.push(item.waivedReason);
244
+ continue;
245
+ }
246
+ if (isSatisfied(item, revision)) done += 1;
247
+ }
248
+
249
+ return {
250
+ total: list.length,
251
+ done,
252
+ waived,
253
+ open: list.length - done - waived,
254
+ waivedReasons,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Daftar item yang belum terpenuhi, LENGKAP dengan id-nya.
260
+ *
261
+ * `renderChecklist` sengaja hanya menampilkan kondisi (untuk dibaca manusia).
262
+ * Pesan penolakan butuh id, karena tanpa id agen tidak tahu item mana yang
263
+ * harus dibuktikan lewat `mark_dod_item`.
264
+ */
265
+ export function renderOutstanding(items, revision) {
266
+ return openItems(items, revision)
267
+ .map((item) => {
268
+ const state = item.status === "done" ? "bukti basi, perlu konfirmasi ulang" : "terbuka";
269
+ return `- ${item.id} [${state}]: ${item.text}`;
270
+ })
271
+ .join("\n");
272
+ }
273
+
274
+ export function renderChecklist(items, revision) { const list = Array.isArray(items) ? items : [];
275
+
276
+ return list
277
+ .map((item) => {
278
+ const text = String(item.text ?? "");
279
+ if (item.status === "waived") {
280
+ return `- [~] ${text} — dilewati: ${item.waivedReason || "(tanpa alasan)"}`;
281
+ }
282
+ if (item.status === "done" && !isSatisfied(item, revision)) {
283
+ return `- [!] ${text} — bukti basi, perlu konfirmasi ulang`;
284
+ }
285
+ if (item.status === "done") {
286
+ return `- [x] ${text} — bukti: ${item.evidence}`;
287
+ }
288
+ return `- [ ] ${text}`;
289
+ })
290
+ .join("\n");
291
+ }
292
+
293
+ /**
294
+ * Menggabungkan DoD milik proses ini dengan yang ada di disk.
295
+ *
296
+ * Dua proses MCP bisa melayani task yang sama. Kalau array digabung begitu saja
297
+ * dengan "yang terakhir menulis menang", proses yang memegang state basi akan
298
+ * menghapus bukti yang baru dicatat proses lain. Karena itu per id dipilih entri
299
+ * yang lebih kuat menurut (peringkat status, revisi), dan item yang hanya ada di
300
+ * salah satu sisi tetap dipertahankan.
301
+ */
302
+ export function mergeDefinitionOfDone(memoryItems, diskItems) {
303
+ const memory = Array.isArray(memoryItems) ? memoryItems : [];
304
+ const disk = Array.isArray(diskItems) ? diskItems : [];
305
+
306
+ const stronger = (a, b) => {
307
+ const rankA = STATUS_RANK[a.status] ?? 0;
308
+ const rankB = STATUS_RANK[b.status] ?? 0;
309
+ if (rankA !== rankB) return rankA > rankB ? a : b;
310
+ return Number(a.revision) >= Number(b.revision) ? a : b;
311
+ };
312
+
313
+ const diskById = new Map();
314
+ for (const item of disk) {
315
+ if (item?.id) diskById.set(item.id, item);
316
+ }
317
+
318
+ const merged = [];
319
+ const seen = new Set();
320
+
321
+ for (const item of memory) {
322
+ if (!item?.id) continue;
323
+ seen.add(item.id);
324
+ const fromDisk = diskById.get(item.id);
325
+ merged.push(fromDisk ? stronger(item, fromDisk) : item);
326
+ }
327
+
328
+ for (const item of disk) {
329
+ if (!item?.id || seen.has(item.id)) continue;
330
+ seen.add(item.id);
331
+ merged.push(item);
332
+ }
333
+
334
+ return merged;
335
+ }
@@ -1,4 +1,5 @@
1
1
  import { textResponse } from "./shared.js";
2
+ import { parseFactoryChecklist } from "./dod.js";
2
3
  import { startKeeping, stopKeeping } from "../../session-keeper.js";
3
4
  import {
4
5
  readFactoryConfig,
@@ -141,15 +142,32 @@ export async function handleClaimFactoryTask(args = {}, { workflowState } = {})
141
142
  // ada entri history, tidak ada peringatan, tidak disebut di jawaban.
142
143
  // `start_task` sudah mengarsipkan sejak awal; claim tidak.
143
144
  let archivedTask = null;
145
+ // Checklist papan diimpor sebagai RENCANA, bukan bukti: setiap item masuk
146
+ // sebagai `open` walau di papan sudah dicentang. Centang di papan berarti
147
+ // seseorang pernah menyatakannya, bukan bahwa kode di mesin ini terbukti.
148
+ const importedDod = parseFactoryChecklist(task.definitionOfDone);
144
149
  if (workflowState) {
145
150
  const archived = workflowState.reset({
146
151
  archive: true,
147
152
  reason: "Diganti claim_factory_task sebelum task selesai",
148
153
  });
149
154
  archivedTask = archived?.archivedTask || null;
155
+ const now = new Date().toISOString();
150
156
  workflowState.state.currentPhase = "coding";
151
157
  workflowState.state.taskDescription = `[${task.taskKey || task.id}] ${task.title}`;
152
158
  workflowState.state.taskType = "other";
159
+ workflowState.state.definitionOfDone = importedDod.items.map((text, index) => ({
160
+ id: `dod-${index + 1}`,
161
+ text,
162
+ status: "open",
163
+ evidence: "",
164
+ waivedReason: "",
165
+ revision: 0,
166
+ updatedAt: now,
167
+ }));
168
+ workflowState.state.dodSource = importedDod.items.length > 0 ? "factory" : "";
169
+ workflowState.state.dodFactoryTaskId = importedDod.items.length > 0 ? recordId : "";
170
+ workflowState.state.dodRevision = 0;
153
171
  await workflowState.save();
154
172
  }
155
173
 
@@ -165,7 +183,15 @@ export async function handleClaimFactoryTask(args = {}, { workflowState } = {})
165
183
  : "",
166
184
  `Factory status is now running (work session ${session.id}).`,
167
185
  task.projectName ? `Project: ${task.projectName}` : "",
168
- task.definitionOfDone ? `Definition of done:\n${task.definitionOfDone}` : "",
186
+ importedDod.items.length > 0
187
+ ? `Imported ${importedDod.items.length} definition-of-done item(s) from the board, all OPEN:\n` +
188
+ importedDod.items.map((text, index) => ` dod-${index + 1}: ${text}`).join("\n") +
189
+ (importedDod.truncated
190
+ ? "\n (Some board items were shortened or dropped: at most 20 items of 300 characters are kept.)"
191
+ : "") +
192
+ "\n Prove each one locally with 'mark_dod_item' { id, evidence }, or waive it with a reason."
193
+ : "This board task has no definition of done. Add one with 'set_definition_of_done' { items: [...] } - " +
194
+ "a task cannot be committed or closed without it.",
169
195
  "",
170
196
  "Local workflow started for this task.",
171
197
  "Use 'sync_task_status' with review, blocked or paused to close the session, or done to approve the task once it is in review.",
@@ -10,6 +10,7 @@ import { handleSkipRelease, handlePerformRelease } from "./release-handlers.js";
10
10
  import { handleCommitAndPush, handleCompleteTask, handleForceCompleteTask, handleDropTask } from "./commit-handlers.js";
11
11
  import { handleProjectSummary, handleProjectSummaryData, handleProjectSummaryDb, handleViewHistory } from "./summary-handlers.js";
12
12
  import { handleGetWorkflowStatus, handleReadyCheck, handleContinueWorkflow, handleRerunWorkflow } from "./status-handlers.js";
13
+ import { handleSetDefinitionOfDone, handleMarkDodItem, handleWaiveDodItem } from "./dod-handlers.js";
13
14
  import { handleListFactoryTasks, handleClaimFactoryTask, handleSyncTaskStatus, handleListFactoryProjects, handleCreateFactoryTask, handleUpdateFactoryTask } from "./factory-handlers.js";
14
15
 
15
16
  function summarizeResponse(label, response) {
@@ -197,6 +198,12 @@ async function dispatchToolCall({
197
198
  }
198
199
  case "start_task":
199
200
  return handleStartTask(args, workflowState);
201
+ case "set_definition_of_done":
202
+ return handleSetDefinitionOfDone(args, workflowState);
203
+ case "mark_dod_item":
204
+ return handleMarkDodItem(args, workflowState);
205
+ case "waive_dod_item":
206
+ return handleWaiveDodItem(args, workflowState);
200
207
  case "mark_bug_fixed":
201
208
  return handleMarkBugFixed(args, workflowState);
202
209
  case "create_feature_flow":
@@ -46,6 +46,11 @@ export async function resetToCommitIfWorkingChanges(workflowState, git) {
46
46
  workflowState.state.currentPhase = "commit";
47
47
  workflowState.state.releaseCommand = "";
48
48
  workflowState.state.releaseNotes = "";
49
+ // Perubahan kerja baru berarti bukti DoD yang lama tidak lagi membuktikan
50
+ // kode yang akan di-commit. Menaikkan revisi membuat setiap item `done`
51
+ // otomatis tidak memenuhi sampai dibuktikan ulang - tanpa menulis ulang
52
+ // statusnya, sehingga penggabungan antar-proses tidak saling menghapus.
53
+ workflowState.state.dodRevision = (Number(workflowState.state.dodRevision) || 0) + 1;
49
54
  await workflowState.save();
50
55
  return true;
51
56
  }
@@ -1,10 +1,10 @@
1
1
  import { textResponse, formatChecklist, isReleaseSatisfied, getContinueGuidance, resetToCommitIfWorkingChanges } from "./shared.js";
2
2
  import { handleCommitAndPush } from "./commit-handlers.js";
3
+ import { openItems, renderChecklist, renderOutstanding, summarize } from "./dod.js";
3
4
 
4
5
  export async function handleGetWorkflowStatus(workflowState) {
5
6
  const status = workflowState.state;
6
7
  const nextStep = status ? status.currentPhase : "idle";
7
-
8
8
  // Deteksi berkas state yang terakhir ditulis proses MCP lain. Tanpa
9
9
  // peringatan ini, agen bisa meng-commit atas nama task orang lain.
10
10
  let sharedWarning = "";
@@ -25,6 +25,15 @@ export async function handleGetWorkflowStatus(workflowState) {
25
25
  // Status tetap ditampilkan walau pemeriksaan berkas gagal.
26
26
  }
27
27
 
28
+ const dodItems = Array.isArray(status.definitionOfDone) ? status.definitionOfDone : [];
29
+ const dod = summarize(dodItems, status.dodRevision);
30
+ const dodSource = status.dodSource === "factory" ? " (dari papan Factory)" : "";
31
+ const dodBlock =
32
+ dodItems.length === 0
33
+ ? "DoD: belum ada item - isi dengan 'set_definition_of_done'"
34
+ : `DoD: ${dod.done}/${dod.total} selesai, ${dod.waived} dilewati, ${dod.open} terbuka${dodSource}\n` +
35
+ renderChecklist(dodItems, status.dodRevision);
36
+
28
37
  return textResponse(
29
38
  `📊 Current Workflow Status:
30
39
 
@@ -39,6 +48,8 @@ Ready check: ${status.readyCheckCompleted ? "✅" : "❌"}
39
48
  Commit & push: ${status.commitAndPushCompleted ? "✅" : "❌"}
40
49
  Released: ${status.released ? "✅" : "❌"}
41
50
 
51
+ ${dodBlock}
52
+
42
53
  Next step: ${nextStep}${sharedWarning}`
43
54
  );
44
55
  }
@@ -47,9 +58,18 @@ export async function handleReadyCheck(workflowState) {
47
58
  const status = workflowState.state;
48
59
  const testsCreatedDone = status.testsSkipped || status.testsCreated;
49
60
  const testsPassedDone = status.testsSkipped || status.testsPassed;
61
+
62
+ // Definition of Done adalah prasyarat, bukan langkah baru: ia tidak pernah
63
+ // memindahkan `currentPhase` sendiri. Ia hanya memutuskan lolos atau tidak.
64
+ const dodItems = Array.isArray(status.definitionOfDone) ? status.definitionOfDone : [];
65
+ const dod = summarize(dodItems, status.dodRevision);
66
+ const dodRecorded = dodItems.length > 0;
67
+ const dodSatisfied = dodRecorded && dod.open === 0;
68
+
50
69
  const checks = [
51
70
  { name: "Task started", done: status.currentPhase !== "idle" },
52
71
  { name: "Feature flow described", done: status.featureFlowCreated },
72
+ { name: "Definition of done recorded", done: dodRecorded },
53
73
  { name: "Feature/bug fixed", done: status.bugFixed },
54
74
  {
55
75
  name: status.testsSkipped ? "Tests skipped (manual QA documented)" : "Tests created",
@@ -60,6 +80,7 @@ export async function handleReadyCheck(workflowState) {
60
80
  done: testsPassedDone,
61
81
  },
62
82
  { name: "Documentation created", done: status.documentationCreated },
83
+ { name: "Definition of done satisfied", done: dodSatisfied },
63
84
  ];
64
85
 
65
86
  const allDone = checks.every((c) => c.done);
@@ -81,7 +102,24 @@ export async function handleReadyCheck(workflowState) {
81
102
  );
82
103
  }
83
104
 
84
- return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}\n\nPlease complete all steps before committing.`);
105
+ // Sebut item yang menghalangi, bukan hanya centang merah: agen yang ditolak
106
+ // harus tahu persis apa yang harus dibuktikan atau dikecualikan.
107
+ const hints = [];
108
+ if (!dodRecorded) {
109
+ hints.push(
110
+ "Belum ada item DoD untuk task ini. Isi dengan 'set_definition_of_done' { items: [...] } " +
111
+ "sebelum task bisa di-commit."
112
+ );
113
+ } else if (!dodSatisfied) {
114
+ hints.push(
115
+ `Item DoD yang belum terpenuhi:\n${renderOutstanding(dodItems, status.dodRevision)}\n` +
116
+ `Buktikan dengan 'mark_dod_item' { id, evidence } atau kecualikan dengan 'waive_dod_item' { id, reason }.`
117
+ );
118
+ }
119
+
120
+ const hintBlock = hints.length > 0 ? `\n\n${hints.join("\n\n")}` : "";
121
+
122
+ return textResponse(`⚠️ NOT READY TO COMMIT!\n\n${checkList}${hintBlock}\n\nPlease complete all steps before committing.`);
85
123
  }
86
124
 
87
125
  export async function handleContinueWorkflow(workflowState, context) {
@@ -138,6 +176,17 @@ export async function handleContinueWorkflow(workflowState, context) {
138
176
  export async function handleRerunWorkflow(workflowState) {
139
177
  const currentDescription = workflowState.state.taskDescription;
140
178
  const currentType = workflowState.state.taskType;
179
+ // DoD ikut melewati reset. Tanpa ini, `rerun_workflow` menghapus checklist
180
+ // dan meninggalkan task yang tidak akan pernah bisa ditutup, karena gerbang
181
+ // menolak task tanpa item DoD.
182
+ const dod = {
183
+ definitionOfDone: Array.isArray(workflowState.state.definitionOfDone)
184
+ ? workflowState.state.definitionOfDone.map((item) => ({ ...item }))
185
+ : [],
186
+ dodSource: workflowState.state.dodSource || "",
187
+ dodFactoryTaskId: workflowState.state.dodFactoryTaskId || "",
188
+ dodRevision: Number(workflowState.state.dodRevision) || 0,
189
+ };
141
190
 
142
191
  if (!currentDescription) {
143
192
  return textResponse("⚠️ No active task to rerun. Use 'start_task' to begin a new workflow.");
@@ -148,9 +197,18 @@ export async function handleRerunWorkflow(workflowState) {
148
197
  workflowState.state.currentPhase = "coding";
149
198
  workflowState.state.taskDescription = currentDescription;
150
199
  workflowState.state.taskType = currentType;
200
+ Object.assign(workflowState.state, dod);
151
201
  await workflowState.save();
152
202
 
203
+ const dodNote =
204
+ dod.definitionOfDone.length > 0
205
+ ? `\n\nDoD dipertahankan: ${dod.definitionOfDone.length} item` +
206
+ ` (${dod.definitionOfDone.filter((item) => item.status === "done").length} sudah terbukti).` +
207
+ `\nItem yang sudah terbukti TIDAK perlu dibuktikan ulang - kecualikan dengan 'waive_dod_item',` +
208
+ ` atau tambah item baru dengan 'set_definition_of_done' { items: [...], append: true }.`
209
+ : "";
210
+
153
211
  return textResponse(
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!`
212
+ `🔄 Rerunning workflow from the start!\n\nTask: ${currentDescription}\nType: ${currentType}${dodNote}\n\nWorkflow Steps:\n1. ✓ Start task (current)\n2. ⏳ Describe feature flow with Mermaid\n3. ⏳ Fix/implement the feature\n4. ⏳ Prove each definition-of-done item\n5. ⏳ Create tests\n6. ⏳ Run tests (must pass!)\n7. ⏳ Create documentation\n8. ⏳ Run 'check_ready_to_commit'\n9. ⏳ Run 'commit_and_push' (commits and pushes)\n10. ⏳ Run 'perform_release' (handles versioning and tags)\n11. ⏳ Complete task\n\n🎯 Be conscious about what you're coding!`
155
213
  );
156
214
  }
@@ -1,5 +1,6 @@
1
1
  import { classify } from '../../security/policy.js';
2
2
  import { textResponse, isReleaseSatisfied, isErrorResponse, ensurePrdExists } from "./shared.js";
3
+ import { validateItemTexts } from "./dod.js";
3
4
  import { resolveProjectDir, setActiveProjectDir } from "../../exec.js";
4
5
 
5
6
  /**
@@ -68,6 +69,19 @@ function pinProjectDir(args, workflowState) {
68
69
  }
69
70
 
70
71
  export async function handleStartTask(args, workflowState) {
72
+ // Validasi DoD SEBELUM menyentuh state apa pun. Task yang ditolak tidak boleh
73
+ // mengganti task yang sedang berjalan, dan tidak boleh meninggalkan state
74
+ // setengah jadi yang membuat `get_workflow_status` membingungkan.
75
+ const dodCheck = validateItemTexts(args.definitionOfDone);
76
+ if (!dodCheck.ok) {
77
+ return textResponse(
78
+ `⚠️ ${dodCheck.error}\n\n` +
79
+ `Contoh pemakaian:\n` +
80
+ ` definitionOfDone: ["build lulus", "halaman login terverifikasi"]\n\n` +
81
+ `Task BELUM dimulai; workflow sebelumnya tidak diubah.`
82
+ );
83
+ }
84
+
71
85
  // Selamatkan dulu pekerjaan yang mungkin tertimpa. Ini menangani dua kasus:
72
86
  // 1. Proses MCP LAIN menulis berkas state setelah kita memuatnya.
73
87
  // 2. Task kita sendiri yang belum selesai lalu diganti task baru.
@@ -78,6 +92,18 @@ export async function handleStartTask(args, workflowState) {
78
92
  workflowState.state.classification = classify(args);
79
93
  workflowState.state.taskDescription = args.description;
80
94
  workflowState.state.taskType = args.type;
95
+ workflowState.state.definitionOfDone = dodCheck.items.map((text, index) => ({
96
+ id: `dod-${index + 1}`,
97
+ text,
98
+ status: "open",
99
+ evidence: "",
100
+ waivedReason: "",
101
+ revision: 0,
102
+ updatedAt: new Date().toISOString(),
103
+ }));
104
+ workflowState.state.dodSource = "manual";
105
+ workflowState.state.dodFactoryTaskId = "";
106
+ workflowState.state.dodRevision = 0;
81
107
  workflowState.state.testsSkipped = false;
82
108
  workflowState.state.testsSkippedReason = "";
83
109
  workflowState.state.commitAndPushCompleted = false;
@@ -114,8 +140,16 @@ export async function handleStartTask(args, workflowState) {
114
140
 
115
141
  const warningBlock = warnings.length > 0 ? `\n\n${warnings.join("\n\n")}` : "";
116
142
 
143
+ const dodLines = workflowState.state.definitionOfDone
144
+ .map((item) => ` ${item.id}: ${item.text}`)
145
+ .join("\n");
146
+
117
147
  return textResponse(
118
- `✅ Task Started: ${args.description}\n\n${project.message}${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!`
148
+ `✅ Task Started: ${args.description}\n\n${project.message}${warningBlock}\n\n` +
149
+ `📋 Definition of Done (${workflowState.state.definitionOfDone.length} item, semua masih terbuka):\n${dodLines}\n` +
150
+ ` Buktikan tiap item dengan 'mark_dod_item' { id, evidence } setelah benar-benar terbukti,\n` +
151
+ ` atau kecualikan dengan 'waive_dod_item' { id, reason } bila memang tidak relevan.\n\n` +
152
+ `🎯 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. ⏳ Prove each definition-of-done item\n5. ⏳ Create tests\n6. ⏳ Run tests (must pass!)\n7. ⏳ Create documentation\n8. ⏳ Run 'check_ready_to_commit'\n9. ⏳ Run 'commit_and_push' (commits and pushes)\n10. ⏳ Run 'perform_release' or 'skip_release' (when no release is needed)\n11. ⏳ Complete task\n12. ✓ Workflow closed\n\nReminder: Focus on writing clean, maintainable code!`
119
153
  );
120
154
  }
121
155